diff --git a/venice/chat.py b/venice/chat.py index 583d104..d55a4b3 100644 --- a/venice/chat.py +++ b/venice/chat.py @@ -1,7 +1,7 @@ """ title: Venice.ai Chat author: Jeff Smith -version: 1.2.0 +version: 1.3.0 license: MIT required_open_webui_version: 0.6.0 requirements: httpx, pydantic @@ -17,9 +17,17 @@ description: | - Explicit ID: Validates model exists before calling 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 import httpx import json @@ -75,16 +83,42 @@ class Tools: default="", description="Your Venice.ai API key (overrides admin default)" ) - def __init__(self): - self.valves = self.Valves() - self.user_valves = self.UserValves() - self.citation = False + 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() + + # 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() + else: + self.user_valves = self.UserValves() + # Simple in-memory cache self._cache: 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.""" + # 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 def _truncate(self, text: str) -> str: @@ -103,13 +137,31 @@ class Tools: return False 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).""" cache_key = "traits" if self._is_cache_valid(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: return {} @@ -128,13 +180,13 @@ class Tools: pass 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).""" cache_key = f"models_{model_type}" if self._is_cache_valid(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: return [] @@ -159,6 +211,7 @@ class Tools: model_type: str = "text", require_reasoning: bool = False, __model__: dict = None, + __user__: dict = None, ) -> tuple[str, Optional[str]]: """ Resolve model specification to actual model ID with validation. @@ -190,7 +243,7 @@ class Tools: # If still no model, try traits API if not model: - traits = await self._get_traits() + traits = await self._get_traits(__user__) if require_reasoning: # Try reasoning-specific traits @@ -208,7 +261,7 @@ class Tools: # If still no model, pick first available with required capability if not model: - models = await self._get_available_models(model_type) + models = await self._get_available_models(model_type, __user__) for m in models: spec = m.get("model_spec", {}) if spec.get("offline"): @@ -232,7 +285,7 @@ class Tools: ) # 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} if model not in model_map: @@ -317,19 +370,19 @@ class Tools: :param web_search: Enable web search for current information :return: Model response """ - api_key = self._get_api_key() + api_key = self._get_api_key(__user__) 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(): - return "Venice Chat\nStatus: 0\nError: Message required" + return "Error: Message is required" # Resolve and validate model resolved_model, error = await self._resolve_model( - model, "text", False, __model__ + model, "text", False, __model__, __user__ ) if error: - return f"Venice Chat\nStatus: 0\nError: {error}" + return f"Error: {error}" 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", []) 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", {}) content = assistant_message.get("content", "") reasoning = assistant_message.get("reasoning_content") # Build response - lines = [f"Venice Chat ({resolved_model})", "Status: 200", ""] + lines = [f"**Venice Chat** ({resolved_model})", ""] if reasoning: - lines.append(f"Reasoning:\n{reasoning}") + lines.append(f"**Reasoning:**\n{reasoning}") lines.append("") - lines.append(f"Response:\n{content}") + lines.append(f"**Response:**\n{content}") # Include web citations if present venice_params = result.get("venice_parameters", {}) citations = venice_params.get("web_search_citations", []) if citations: lines.append("") - lines.append("Sources:") + lines.append("**Sources:**") for cite in citations[:5]: title = cite.get("title", "Link") url = cite.get("url", "") - lines.append(f" - {title}: {url}") + lines.append(f"- {title}: {url}") # Usage stats usage = result.get("usage", {}) if usage: 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)) except httpx.HTTPStatusError as e: + error_msg = self._format_error(e, f"chat request to {resolved_model}") if __event_emitter__: 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: if __event_emitter__: await __event_emitter__({"type": "status", "data": {"done": True}}) - return ( - f"Venice Chat ({resolved_model})\nStatus: 408\nError: Request timed out" - ) + return f"Error: Request timed out for {resolved_model}" except Exception as e: + error_msg = self._format_error(e, "chat request") if __event_emitter__: 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( self, @@ -456,26 +509,26 @@ class Tools: :param max_tokens: Maximum response tokens (default 2048) :return: Model response """ - api_key = self._get_api_key() + api_key = self._get_api_key(__user__) 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: - return "Venice Chat Conversation\nStatus: 0\nError: messages_json required" + return "Error: messages_json is required" try: conversation = json.loads(messages_json) 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: - return f"Venice Chat Conversation\nStatus: 0\nError: Invalid JSON - {e}" + return f"Error: Invalid JSON - {e}" # Resolve and validate model resolved_model, error = await self._resolve_model( - model, "text", False, __model__ + model, "text", False, __model__, __user__ ) if error: - return f"Venice Chat Conversation\nStatus: 0\nError: {error}" + return f"Error: {error}" if __event_emitter__: await __event_emitter__( @@ -526,30 +579,32 @@ class Tools: choices = result.get("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", {}) content = assistant_message.get("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: - lines.append(f"Reasoning:\n{reasoning}") + lines.append(f"**Reasoning:**\n{reasoning}") lines.append("") - lines.append(f"Response:\n{content}") + lines.append(f"**Response:**\n{content}") return self._truncate("\n".join(lines)) except httpx.HTTPStatusError as e: + error_msg = self._format_error(e, f"conversation with {resolved_model}") if __event_emitter__: 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: + error_msg = self._format_error(e, "conversation request") if __event_emitter__: 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( 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]. :return: Response with reasoning process """ - api_key = self._get_api_key() + api_key = self._get_api_key(__user__) 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(): - return "Venice Reasoning\nStatus: 0\nError: Question required" + return "Error: Question is required" 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) 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: - return f"Venice Reasoning\nStatus: 0\nError: {error}" + return f"Error: {error}" if __event_emitter__: await __event_emitter__( @@ -627,24 +682,23 @@ class Tools: choices = result.get("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", {}) content = assistant_message.get("content", "") reasoning = assistant_message.get("reasoning_content", "") lines = [ - f"Venice Reasoning ({resolved_model})", - "Status: 200", - f"Effort: {reasoning_effort}", + f"**Venice Reasoning** ({resolved_model})", + f"**Effort:** {reasoning_effort}", "", ] if reasoning: - lines.append(f"Reasoning Process:\n{reasoning}") + lines.append(f"**Reasoning Process:**\n{reasoning}") lines.append("") - lines.append(f"Answer:\n{content}") + lines.append(f"**Answer:**\n{content}") # Usage stats usage = result.get("usage", {}) @@ -653,23 +707,25 @@ class Tools: total = usage.get("total_tokens", 0) reasoning_tokens = usage.get("reasoning_tokens", 0) lines.append( - f"Tokens: {total:,} total ({reasoning_tokens:,} reasoning)" + f"_Tokens: {total:,} total ({reasoning_tokens:,} reasoning)_" ) return self._truncate("\n".join(lines)) except httpx.HTTPStatusError as e: + error_msg = self._format_error(e, f"reasoning with {resolved_model}") if __event_emitter__: 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: if __event_emitter__: 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: + error_msg = self._format_error(e, "reasoning request") if __event_emitter__: 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( self, @@ -686,19 +742,19 @@ class Tools: :param model: Model to use, "self", or empty for auto-select :return: Response with web sources """ - api_key = self._get_api_key() + api_key = self._get_api_key(__user__) 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(): - return "Venice Web Search\nStatus: 0\nError: Query required" + return "Error: Query is required" # Resolve model - prefer models with web search capability resolved_model, error = await self._resolve_model( - model, "text", False, __model__ + model, "text", False, __model__, __user__ ) if error: - return f"Venice Web Search\nStatus: 0\nError: {error}" + return f"Error: {error}" if __event_emitter__: await __event_emitter__( @@ -743,16 +799,15 @@ class Tools: choices = result.get("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", {}) content = assistant_message.get("content", "") lines = [ - f"Venice Web Search ({resolved_model})", - "Status: 200", + f"**Venice Web Search** ({resolved_model})", "", - f"Response:\n{content}", + f"**Response:**\n{content}", ] # Include citations @@ -760,20 +815,22 @@ class Tools: citations = venice_params.get("web_search_citations", []) if citations: lines.append("") - lines.append(f"Sources ({len(citations)}):") + lines.append(f"**Sources** ({len(citations)}):") for cite in citations[:10]: title = cite.get("title", "Link") url = cite.get("url", "") - lines.append(f" - {title}") - lines.append(f" {url}") + lines.append(f"- {title}") + lines.append(f" {url}") return self._truncate("\n".join(lines)) except httpx.HTTPStatusError as e: + error_msg = self._format_error(e, f"web search with {resolved_model}") if __event_emitter__: 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: + error_msg = self._format_error(e, "web search request") if __event_emitter__: 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}"