Files
voyage/backend/server/chat/llm_client.py
alex fd3ca360de fix(chat): sanitize error responses and add tool kwargs allowlist
Prevent API key and sensitive info leakage through exception messages:
- Replace str(exc) with generic error messages in all catch-all handlers
- Add server-side exception logging via logger.exception()
- Add ALLOWED_KWARGS per-tool allowlist to filter untrusted LLM kwargs
- Bound tool execution loop to MAX_TOOL_ITERATIONS=10
- Fix tool_call delta merge to use tool_call index
2026-03-08 18:54:35 +00:00

147 lines
4.9 KiB
Python

import json
import logging
import litellm
from integrations.models import UserAPIKey
logger = logging.getLogger(__name__)
PROVIDER_MODELS = {
"openai": "gpt-4o",
"anthropic": "anthropic/claude-sonnet-4-20250514",
"gemini": "gemini/gemini-2.0-flash",
"ollama": "ollama/llama3.1",
"groq": "groq/llama-3.3-70b-versatile",
"mistral": "mistral/mistral-large-latest",
"github_models": "github/gpt-4o",
"openrouter": "openrouter/auto",
}
def _safe_get(obj, key, default=None):
if obj is None:
return default
if isinstance(obj, dict):
return obj.get(key, default)
return getattr(obj, key, default)
def get_llm_api_key(user, provider):
"""Get the user's API key for the given provider."""
normalized_provider = (provider or "").strip().lower()
try:
key_obj = UserAPIKey.objects.get(user=user, provider=normalized_provider)
return key_obj.get_api_key()
except UserAPIKey.DoesNotExist:
return None
def get_system_prompt(user, collection=None):
"""Build the system prompt with user context."""
from integrations.models import UserRecommendationPreferenceProfile
base_prompt = """You are a helpful travel planning assistant for the Voyage travel app. You help users discover places, plan trips, and organize their itineraries.
Your capabilities:
- Search for interesting places (restaurants, tourist attractions, hotels) near any location
- View and manage the user's trip collections and itineraries
- Add new locations to trip itineraries
- Check weather/temperature data for travel dates
When suggesting places:
- Be specific with names, addresses, and why a place is worth visiting
- Consider the user's travel dates and weather conditions
- Group suggestions logically (by area, by type, by day)
When modifying itineraries:
- Always confirm with the user before adding items
- Suggest logical ordering based on geography
- Consider travel time between locations
Be conversational, helpful, and enthusiastic about travel. Keep responses concise but informative."""
try:
profile = UserRecommendationPreferenceProfile.objects.get(user=user)
prefs = []
if profile.cuisines:
prefs.append(f"Cuisine preferences: {profile.cuisines}")
if profile.interests:
prefs.append(f"Interests: {profile.interests}")
if profile.trip_style:
prefs.append(f"Travel style: {profile.trip_style}")
if profile.notes:
prefs.append(f"Additional notes: {profile.notes}")
if prefs:
base_prompt += "\n\nUser preferences:\n" + "\n".join(prefs)
except UserRecommendationPreferenceProfile.DoesNotExist:
pass
return base_prompt
async def stream_chat_completion(user, messages, provider, tools=None):
"""Stream a chat completion using LiteLLM.
Yields SSE-formatted strings.
"""
normalized_provider = (provider or "").strip().lower()
api_key = get_llm_api_key(user, normalized_provider)
if not api_key:
payload = {
"error": f"No API key found for provider: {normalized_provider}. Please add one in Settings."
}
yield f"data: {json.dumps(payload)}\n\n"
return
model = PROVIDER_MODELS.get(normalized_provider, "gpt-4o")
try:
response = await litellm.acompletion(
model=model,
messages=messages,
tools=tools,
tool_choice="auto" if tools else None,
stream=True,
api_key=api_key,
)
async for chunk in response:
choices = _safe_get(chunk, "choices", []) or []
if not choices:
continue
delta = _safe_get(choices[0], "delta")
if not delta:
continue
chunk_data = {}
content = _safe_get(delta, "content")
if content:
chunk_data["content"] = content
tool_calls = _safe_get(delta, "tool_calls") or []
if tool_calls:
serialized = []
for tool_call in tool_calls:
function = _safe_get(tool_call, "function")
serialized.append(
{
"id": _safe_get(tool_call, "id"),
"type": _safe_get(tool_call, "type"),
"function": {
"name": _safe_get(function, "name", "") or "",
"arguments": _safe_get(function, "arguments", "") or "",
},
}
)
chunk_data["tool_calls"] = serialized
if chunk_data:
yield f"data: {json.dumps(chunk_data)}\n\n"
yield "data: [DONE]\n\n"
except Exception:
logger.exception("LLM streaming error")
yield f"data: {json.dumps({'error': 'An error occurred while processing your request. Please try again.'})}\n\n"