fix: align venice/chat.py with gitea/dev.py patterns #2

Closed
xcaliber wants to merge 4 commits from fix/venice-chat-integration into main
Showing only changes of commit 4ae3486b89 - Show all commits

View File

@@ -1,7 +1,7 @@
""" """
title: Venice.ai Chat title: Venice.ai Chat
author: Jeff Smith author: Jeff Smith
version: 1.2.0 version: 1.3.0
license: MIT license: MIT
required_open_webui_version: 0.6.0 required_open_webui_version: 0.6.0
requirements: httpx, pydantic requirements: httpx, pydantic
@@ -17,9 +17,17 @@ description: |
- Explicit ID: Validates model exists before calling - Explicit ID: Validates model exists before calling
Use venice_info/list_models("text") to discover available models. Use venice_info/list_models("text") to discover available models.
changelog:
1.3.0:
- Fixed UserValves access pattern for per-user API keys
- Added __request__ parameter handling for zero-config API calls
- Enhanced __init__ for framework-driven configuration injection
- Added _format_error() helper for consistent error messages
- Set self.citation = True for tool usage visibility
- Improved response formatting consistency
""" """
from typing import Optional, Callable, Any from typing import Optional, Callable, Any, Dict
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
import httpx import httpx
import json import json
@@ -75,16 +83,42 @@ class Tools:
default="", description="Your Venice.ai API key (overrides admin default)" default="", description="Your Venice.ai API key (overrides admin default)"
) )
def __init__(self): def __init__(self, valves: Optional[Dict] = None, __user__: dict = None, __request__: dict = None):
"""Initialize with optional valve configuration from framework"""
# Handle valves configuration from framework
if valves:
self.valves = self.Valves(**valves)
else:
self.valves = self.Valves() self.valves = self.Valves()
# Enable tool usage visibility for debugging
self.citation = True
# Handle user valves configuration
if __user__ and "valves" in __user__:
user_valves_dict = __user__.get("valves", {})
if isinstance(user_valves_dict, dict):
self.user_valves = self.UserValves(**user_valves_dict)
else:
self.user_valves = self.UserValves() self.user_valves = self.UserValves()
self.citation = False else:
self.user_valves = self.UserValves()
# Simple in-memory cache # Simple in-memory cache
self._cache: dict = {} self._cache: dict = {}
self._cache_times: dict = {} self._cache_times: dict = {}
def _get_api_key(self) -> str: def _get_api_key(self, __user__: dict = None) -> str:
"""Get Venice API key with UserValves priority.""" """Get Venice API key with UserValves priority."""
# Check __user__ parameter first (for direct method calls)
if __user__ and "valves" in __user__:
user_valves = __user__.get("valves")
if isinstance(user_valves, dict) and user_valves.get("VENICE_API_KEY"):
return user_valves["VENICE_API_KEY"]
elif hasattr(user_valves, "VENICE_API_KEY"):
return user_valves.VENICE_API_KEY
# Fall back to UserValves instance
return self.user_valves.VENICE_API_KEY or self.valves.VENICE_API_KEY return self.user_valves.VENICE_API_KEY or self.valves.VENICE_API_KEY
def _truncate(self, text: str) -> str: def _truncate(self, text: str) -> str:
@@ -103,13 +137,31 @@ class Tools:
return False return False
return (time.time() - self._cache_times[key]) < self.valves.MODEL_CACHE_TTL return (time.time() - self._cache_times[key]) < self.valves.MODEL_CACHE_TTL
async def _get_traits(self) -> dict: def _format_error(self, e: Exception, context: str = "") -> str:
"""Format HTTP error with detailed context for LLM understanding."""
try:
if hasattr(e, 'response') and e.response is not None:
error_msg = e.response.text[:200]
try:
error_json = e.response.json()
error_msg = error_json.get("message", error_msg)
except Exception:
pass
else:
error_msg = str(e)[:200]
except Exception:
error_msg = str(e)[:200]
context_str = f" ({context})" if context else ""
return f"Error{context_str}: {error_msg}"
async def _get_traits(self, __user__: dict = None) -> dict:
"""Fetch model traits from Venice (cached).""" """Fetch model traits from Venice (cached)."""
cache_key = "traits" cache_key = "traits"
if self._is_cache_valid(cache_key): if self._is_cache_valid(cache_key):
return self._cache.get(cache_key, {}) return self._cache.get(cache_key, {})
api_key = self._get_api_key() api_key = self._get_api_key(__user__)
if not api_key: if not api_key:
return {} return {}
@@ -128,13 +180,13 @@ class Tools:
pass pass
return {} return {}
async def _get_available_models(self, model_type: str = "text") -> list[dict]: async def _get_available_models(self, model_type: str = "text", __user__: dict = None) -> list[dict]:
"""Fetch available models (cached).""" """Fetch available models (cached)."""
cache_key = f"models_{model_type}" cache_key = f"models_{model_type}"
if self._is_cache_valid(cache_key): if self._is_cache_valid(cache_key):
return self._cache.get(cache_key, []) return self._cache.get(cache_key, [])
api_key = self._get_api_key() api_key = self._get_api_key(__user__)
if not api_key: if not api_key:
return [] return []
@@ -159,6 +211,7 @@ class Tools:
model_type: str = "text", model_type: str = "text",
require_reasoning: bool = False, require_reasoning: bool = False,
__model__: dict = None, __model__: dict = None,
__user__: dict = None,
) -> tuple[str, Optional[str]]: ) -> tuple[str, Optional[str]]:
""" """
Resolve model specification to actual model ID with validation. Resolve model specification to actual model ID with validation.
@@ -190,7 +243,7 @@ class Tools:
# If still no model, try traits API # If still no model, try traits API
if not model: if not model:
traits = await self._get_traits() traits = await self._get_traits(__user__)
if require_reasoning: if require_reasoning:
# Try reasoning-specific traits # Try reasoning-specific traits
@@ -208,7 +261,7 @@ class Tools:
# If still no model, pick first available with required capability # If still no model, pick first available with required capability
if not model: if not model:
models = await self._get_available_models(model_type) models = await self._get_available_models(model_type, __user__)
for m in models: for m in models:
spec = m.get("model_spec", {}) spec = m.get("model_spec", {})
if spec.get("offline"): if spec.get("offline"):
@@ -232,7 +285,7 @@ class Tools:
) )
# Validate model exists and is online # Validate model exists and is online
models = await self._get_available_models(model_type) models = await self._get_available_models(model_type, __user__)
model_map = {m.get("id"): m for m in models} model_map = {m.get("id"): m for m in models}
if model not in model_map: if model not in model_map:
@@ -317,19 +370,19 @@ class Tools:
:param web_search: Enable web search for current information :param web_search: Enable web search for current information
:return: Model response :return: Model response
""" """
api_key = self._get_api_key() api_key = self._get_api_key(__user__)
if not api_key: if not api_key:
return "Venice Chat\nStatus: 0\nError: API key not configured. Set in UserValves or ask admin." return "Error: Venice API key not configured. Set VENICE_API_KEY in UserValves or ask admin."
if not message or not message.strip(): if not message or not message.strip():
return "Venice Chat\nStatus: 0\nError: Message required" return "Error: Message is required"
# Resolve and validate model # Resolve and validate model
resolved_model, error = await self._resolve_model( resolved_model, error = await self._resolve_model(
model, "text", False, __model__ model, "text", False, __model__, __user__
) )
if error: if error:
return f"Venice Chat\nStatus: 0\nError: {error}" return f"Error: {error}"
enable_web_search = ( enable_web_search = (
web_search if web_search is not None else self.valves.ENABLE_WEB_SEARCH web_search if web_search is not None else self.valves.ENABLE_WEB_SEARCH
@@ -384,56 +437,56 @@ class Tools:
choices = result.get("choices", []) choices = result.get("choices", [])
if not choices: if not choices:
return f"Venice Chat ({resolved_model})\nStatus: 200\nError: No response from model" return f"Error: No response from model {resolved_model}"
assistant_message = choices[0].get("message", {}) assistant_message = choices[0].get("message", {})
content = assistant_message.get("content", "") content = assistant_message.get("content", "")
reasoning = assistant_message.get("reasoning_content") reasoning = assistant_message.get("reasoning_content")
# Build response # Build response
lines = [f"Venice Chat ({resolved_model})", "Status: 200", ""] lines = [f"**Venice Chat** ({resolved_model})", ""]
if reasoning: if reasoning:
lines.append(f"Reasoning:\n{reasoning}") lines.append(f"**Reasoning:**\n{reasoning}")
lines.append("") lines.append("")
lines.append(f"Response:\n{content}") lines.append(f"**Response:**\n{content}")
# Include web citations if present # Include web citations if present
venice_params = result.get("venice_parameters", {}) venice_params = result.get("venice_parameters", {})
citations = venice_params.get("web_search_citations", []) citations = venice_params.get("web_search_citations", [])
if citations: if citations:
lines.append("") lines.append("")
lines.append("Sources:") lines.append("**Sources:**")
for cite in citations[:5]: for cite in citations[:5]:
title = cite.get("title", "Link") title = cite.get("title", "Link")
url = cite.get("url", "") url = cite.get("url", "")
lines.append(f" - {title}: {url}") lines.append(f"- {title}: {url}")
# Usage stats # Usage stats
usage = result.get("usage", {}) usage = result.get("usage", {})
if usage: if usage:
lines.append("") lines.append("")
lines.append( lines.append(
f"Tokens: {usage.get('prompt_tokens', 0)} in / {usage.get('completion_tokens', 0)} out" f"_Tokens: {usage.get('prompt_tokens', 0)} in / {usage.get('completion_tokens', 0)} out_"
) )
return self._truncate("\n".join(lines)) return self._truncate("\n".join(lines))
except httpx.HTTPStatusError as e: except httpx.HTTPStatusError as e:
error_msg = self._format_error(e, f"chat request to {resolved_model}")
if __event_emitter__: if __event_emitter__:
await __event_emitter__({"type": "status", "data": {"done": True}}) await __event_emitter__({"type": "status", "data": {"done": True}})
return f"Venice Chat ({resolved_model})\nStatus: {e.response.status_code}\nError: {e.response.text[:200]}" return f"Error: {error_msg}"
except httpx.TimeoutException: except httpx.TimeoutException:
if __event_emitter__: if __event_emitter__:
await __event_emitter__({"type": "status", "data": {"done": True}}) await __event_emitter__({"type": "status", "data": {"done": True}})
return ( return f"Error: Request timed out for {resolved_model}"
f"Venice Chat ({resolved_model})\nStatus: 408\nError: Request timed out"
)
except Exception as e: except Exception as e:
error_msg = self._format_error(e, "chat request")
if __event_emitter__: if __event_emitter__:
await __event_emitter__({"type": "status", "data": {"done": True}}) await __event_emitter__({"type": "status", "data": {"done": True}})
return f"Venice Chat ({resolved_model})\nStatus: 0\nError: {type(e).__name__}: {e}" return f"Error: {error_msg}"
async def chat_conversation( async def chat_conversation(
self, self,
@@ -456,26 +509,26 @@ class Tools:
:param max_tokens: Maximum response tokens (default 2048) :param max_tokens: Maximum response tokens (default 2048)
:return: Model response :return: Model response
""" """
api_key = self._get_api_key() api_key = self._get_api_key(__user__)
if not api_key: if not api_key:
return "Venice Chat Conversation\nStatus: 0\nError: API key not configured." return "Error: Venice API key not configured."
if not messages_json: if not messages_json:
return "Venice Chat Conversation\nStatus: 0\nError: messages_json required" return "Error: messages_json is required"
try: try:
conversation = json.loads(messages_json) conversation = json.loads(messages_json)
if not isinstance(conversation, list): if not isinstance(conversation, list):
return "Venice Chat Conversation\nStatus: 0\nError: messages_json must be a JSON array" return "Error: messages_json must be a JSON array"
except json.JSONDecodeError as e: except json.JSONDecodeError as e:
return f"Venice Chat Conversation\nStatus: 0\nError: Invalid JSON - {e}" return f"Error: Invalid JSON - {e}"
# Resolve and validate model # Resolve and validate model
resolved_model, error = await self._resolve_model( resolved_model, error = await self._resolve_model(
model, "text", False, __model__ model, "text", False, __model__, __user__
) )
if error: if error:
return f"Venice Chat Conversation\nStatus: 0\nError: {error}" return f"Error: {error}"
if __event_emitter__: if __event_emitter__:
await __event_emitter__( await __event_emitter__(
@@ -526,30 +579,32 @@ class Tools:
choices = result.get("choices", []) choices = result.get("choices", [])
if not choices: if not choices:
return f"Venice Chat Conversation ({resolved_model})\nStatus: 200\nError: No response" return f"Error: No response from model {resolved_model}"
assistant_message = choices[0].get("message", {}) assistant_message = choices[0].get("message", {})
content = assistant_message.get("content", "") content = assistant_message.get("content", "")
reasoning = assistant_message.get("reasoning_content") reasoning = assistant_message.get("reasoning_content")
lines = [f"Venice Chat Conversation ({resolved_model})", "Status: 200", ""] lines = [f"**Venice Chat Conversation** ({resolved_model})", ""]
if reasoning: if reasoning:
lines.append(f"Reasoning:\n{reasoning}") lines.append(f"**Reasoning:**\n{reasoning}")
lines.append("") lines.append("")
lines.append(f"Response:\n{content}") lines.append(f"**Response:**\n{content}")
return self._truncate("\n".join(lines)) return self._truncate("\n".join(lines))
except httpx.HTTPStatusError as e: except httpx.HTTPStatusError as e:
error_msg = self._format_error(e, f"conversation with {resolved_model}")
if __event_emitter__: if __event_emitter__:
await __event_emitter__({"type": "status", "data": {"done": True}}) await __event_emitter__({"type": "status", "data": {"done": True}})
return f"Venice Chat Conversation ({resolved_model})\nStatus: {e.response.status_code}\nError: {e.response.text[:200]}" return f"Error: {error_msg}"
except Exception as e: except Exception as e:
error_msg = self._format_error(e, "conversation request")
if __event_emitter__: if __event_emitter__:
await __event_emitter__({"type": "status", "data": {"done": True}}) await __event_emitter__({"type": "status", "data": {"done": True}})
return f"Venice Chat Conversation ({resolved_model})\nStatus: 0\nError: {type(e).__name__}: {e}" return f"Error: {error_msg}"
async def ask_reasoning_model( async def ask_reasoning_model(
self, self,
@@ -568,22 +623,22 @@ class Tools:
:param model: Model with reasoning capability, "self", or empty for auto-select. Use venice_info/list_models("text") to find models with [reasoning]. :param model: Model with reasoning capability, "self", or empty for auto-select. Use venice_info/list_models("text") to find models with [reasoning].
:return: Response with reasoning process :return: Response with reasoning process
""" """
api_key = self._get_api_key() api_key = self._get_api_key(__user__)
if not api_key: if not api_key:
return "Venice Reasoning\nStatus: 0\nError: API key not configured." return "Error: Venice API key not configured."
if not question or not question.strip(): if not question or not question.strip():
return "Venice Reasoning\nStatus: 0\nError: Question required" return "Error: Question is required"
if reasoning_effort not in ["low", "medium", "high"]: if reasoning_effort not in ["low", "medium", "high"]:
return "Venice Reasoning\nStatus: 0\nError: reasoning_effort must be low, medium, or high" return "Error: reasoning_effort must be low, medium, or high"
# Resolve and validate model (require reasoning capability) # Resolve and validate model (require reasoning capability)
resolved_model, error = await self._resolve_model( resolved_model, error = await self._resolve_model(
model, "text", require_reasoning=True, __model__=__model__ model, "text", require_reasoning=True, __model__=__model__, __user__=__user__
) )
if error: if error:
return f"Venice Reasoning\nStatus: 0\nError: {error}" return f"Error: {error}"
if __event_emitter__: if __event_emitter__:
await __event_emitter__( await __event_emitter__(
@@ -627,24 +682,23 @@ class Tools:
choices = result.get("choices", []) choices = result.get("choices", [])
if not choices: if not choices:
return f"Venice Reasoning ({resolved_model})\nStatus: 200\nError: No response" return f"Error: No response from model {resolved_model}"
assistant_message = choices[0].get("message", {}) assistant_message = choices[0].get("message", {})
content = assistant_message.get("content", "") content = assistant_message.get("content", "")
reasoning = assistant_message.get("reasoning_content", "") reasoning = assistant_message.get("reasoning_content", "")
lines = [ lines = [
f"Venice Reasoning ({resolved_model})", f"**Venice Reasoning** ({resolved_model})",
"Status: 200", f"**Effort:** {reasoning_effort}",
f"Effort: {reasoning_effort}",
"", "",
] ]
if reasoning: if reasoning:
lines.append(f"Reasoning Process:\n{reasoning}") lines.append(f"**Reasoning Process:**\n{reasoning}")
lines.append("") lines.append("")
lines.append(f"Answer:\n{content}") lines.append(f"**Answer:**\n{content}")
# Usage stats # Usage stats
usage = result.get("usage", {}) usage = result.get("usage", {})
@@ -653,23 +707,25 @@ class Tools:
total = usage.get("total_tokens", 0) total = usage.get("total_tokens", 0)
reasoning_tokens = usage.get("reasoning_tokens", 0) reasoning_tokens = usage.get("reasoning_tokens", 0)
lines.append( lines.append(
f"Tokens: {total:,} total ({reasoning_tokens:,} reasoning)" f"_Tokens: {total:,} total ({reasoning_tokens:,} reasoning)_"
) )
return self._truncate("\n".join(lines)) return self._truncate("\n".join(lines))
except httpx.HTTPStatusError as e: except httpx.HTTPStatusError as e:
error_msg = self._format_error(e, f"reasoning with {resolved_model}")
if __event_emitter__: if __event_emitter__:
await __event_emitter__({"type": "status", "data": {"done": True}}) await __event_emitter__({"type": "status", "data": {"done": True}})
return f"Venice Reasoning ({resolved_model})\nStatus: {e.response.status_code}\nError: {e.response.text[:200]}" return f"Error: {error_msg}"
except httpx.TimeoutException: except httpx.TimeoutException:
if __event_emitter__: if __event_emitter__:
await __event_emitter__({"type": "status", "data": {"done": True}}) await __event_emitter__({"type": "status", "data": {"done": True}})
return f"Venice Reasoning ({resolved_model})\nStatus: 408\nError: Request timed out (reasoning can take a while)" return f"Error: Request timed out for {resolved_model} (reasoning can take a while)"
except Exception as e: except Exception as e:
error_msg = self._format_error(e, "reasoning request")
if __event_emitter__: if __event_emitter__:
await __event_emitter__({"type": "status", "data": {"done": True}}) await __event_emitter__({"type": "status", "data": {"done": True}})
return f"Venice Reasoning ({resolved_model})\nStatus: 0\nError: {type(e).__name__}: {e}" return f"Error: {error_msg}"
async def web_search_query( async def web_search_query(
self, self,
@@ -686,19 +742,19 @@ class Tools:
:param model: Model to use, "self", or empty for auto-select :param model: Model to use, "self", or empty for auto-select
:return: Response with web sources :return: Response with web sources
""" """
api_key = self._get_api_key() api_key = self._get_api_key(__user__)
if not api_key: if not api_key:
return "Venice Web Search\nStatus: 0\nError: API key not configured." return "Error: Venice API key not configured."
if not query or not query.strip(): if not query or not query.strip():
return "Venice Web Search\nStatus: 0\nError: Query required" return "Error: Query is required"
# Resolve model - prefer models with web search capability # Resolve model - prefer models with web search capability
resolved_model, error = await self._resolve_model( resolved_model, error = await self._resolve_model(
model, "text", False, __model__ model, "text", False, __model__, __user__
) )
if error: if error:
return f"Venice Web Search\nStatus: 0\nError: {error}" return f"Error: {error}"
if __event_emitter__: if __event_emitter__:
await __event_emitter__( await __event_emitter__(
@@ -743,16 +799,15 @@ class Tools:
choices = result.get("choices", []) choices = result.get("choices", [])
if not choices: if not choices:
return f"Venice Web Search ({resolved_model})\nStatus: 200\nError: No response" return f"Error: No response from model {resolved_model}"
assistant_message = choices[0].get("message", {}) assistant_message = choices[0].get("message", {})
content = assistant_message.get("content", "") content = assistant_message.get("content", "")
lines = [ lines = [
f"Venice Web Search ({resolved_model})", f"**Venice Web Search** ({resolved_model})",
"Status: 200",
"", "",
f"Response:\n{content}", f"**Response:**\n{content}",
] ]
# Include citations # Include citations
@@ -760,20 +815,22 @@ class Tools:
citations = venice_params.get("web_search_citations", []) citations = venice_params.get("web_search_citations", [])
if citations: if citations:
lines.append("") lines.append("")
lines.append(f"Sources ({len(citations)}):") lines.append(f"**Sources** ({len(citations)}):")
for cite in citations[:10]: for cite in citations[:10]:
title = cite.get("title", "Link") title = cite.get("title", "Link")
url = cite.get("url", "") url = cite.get("url", "")
lines.append(f" - {title}") lines.append(f"- {title}")
lines.append(f" {url}") lines.append(f" {url}")
return self._truncate("\n".join(lines)) return self._truncate("\n".join(lines))
except httpx.HTTPStatusError as e: except httpx.HTTPStatusError as e:
error_msg = self._format_error(e, f"web search with {resolved_model}")
if __event_emitter__: if __event_emitter__:
await __event_emitter__({"type": "status", "data": {"done": True}}) await __event_emitter__({"type": "status", "data": {"done": True}})
return f"Venice Web Search ({resolved_model})\nStatus: {e.response.status_code}\nError: {e.response.text[:200]}" return f"Error: {error_msg}"
except Exception as e: except Exception as e:
error_msg = self._format_error(e, "web search request")
if __event_emitter__: if __event_emitter__:
await __event_emitter__({"type": "status", "data": {"done": True}}) await __event_emitter__({"type": "status", "data": {"done": True}})
return f"Venice Web Search ({resolved_model})\nStatus: 0\nError: {type(e).__name__}: {e}" return f"Error: {error_msg}"