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

- Fixed _get_api_key() to accept __user__ parameter for UserValves access
- Added __request__ parameter support in __init__ for framework injection
- Added _format_error() helper for consistent error handling
- Set self.citation = True for tool usage visibility
- Updated response formatting to use markdown headers consistently
- Added __user__ parameter to _get_traits and _get_available_models
- Improved error messages to be more informative
This commit is contained in:
2026-01-15 00:35:50 +00:00
parent 1015453feb
commit 4ae3486b89

View File

@@ -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}"