Files
kite/backend/ai/provider_config.py
curo1305andClaude Sonnet 4.6 ac2dded35b fix(celery+lmstudio): PYTHONPATH for forked workers + qwen3.5-9b thinking model support
Three root causes fixed:

1. docker-compose.yml — celery-worker and celery-beat missing PYTHONPATH=/app
   ForkPoolWorker processes inherit sys.path with '' (CWD), but fork can change
   CWD so lazy imports like `from db.session import ...` raised ModuleNotFoundError.
   Adding PYTHONPATH=/app ensures /app is always explicit in the path.

2. provider_config.py — lmstudio SUPPORTS_JSON_MODE set to False
   LM Studio only accepts response_format type 'json_schema' or 'text', not
   'json_object'. GenericOpenAIProvider now omits response_format for lmstudio
   and relies on parse_classification() fallback (same path as Gemini).

3. generic_openai_provider.py — max_tokens raised 1024→4096 (classify) / 256→1024 (suggest)
   Qwen3 thinking models (like qwen/qwen3.5-9b) consume reasoning tokens from the
   same budget. With max_tokens=1024 the model exhausts the budget in the thinking
   trace, leaving content=''. 4096 gives room for both thinking and the JSON output.

Also updates lmstudio PROVIDER_DEFAULTS model to qwen/qwen3.5-9b (confirmed loaded
in LM Studio) and context_chars to 32_000 (Qwen3 context window).

Tested: classify(invoice_text, ['Finance','Legal','HR']) → topics=['Finance'] ✓
Backend tests: 376 passed ✓

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-05 00:00:26 +02:00

109 lines
3.5 KiB
Python

"""ProviderConfig Pydantic model and per-provider defaults.
Loaded by get_provider() in ai/__init__.py; populated by load_provider_config()
in services/ai_config.py.
This file is a pure data module — it does NOT import any provider class.
"""
from __future__ import annotations
from typing import Optional
from pydantic import BaseModel
class ProviderConfig(BaseModel):
"""Typed configuration for a single AI provider instance.
Fields:
provider_id: One of the keys in PROVIDER_DEFAULTS (e.g. "openai", "groq").
api_key: Decrypted API key; empty string for local providers (Ollama, LMStudio).
base_url: Override for the provider's base URL; None means use the default.
model: Model name; empty string means use the default from PROVIDER_DEFAULTS.
context_chars: Character budget for input truncation; 0 means use the default.
"""
model_config = {"extra": "forbid"}
provider_id: str
api_key: str = ""
base_url: Optional[str] = None
model: str = ""
context_chars: int = 0 # 0 means "unset — use PROVIDER_DEFAULTS in get_provider()"
# Named preset defaults for all 10 supported providers.
# Values are [ASSUMED] approximations based on well-known context window sizes.
# Admins can override all fields via the system_settings DB table (D-04).
PROVIDER_DEFAULTS: dict[str, dict] = {
"openai": {
"base_url": None,
"model": "gpt-4o",
"context_chars": 120_000,
},
"anthropic": {
"base_url": None,
"model": "claude-sonnet-4-6",
"context_chars": 180_000,
},
"gemini": {
"base_url": "https://generativelanguage.googleapis.com/v1beta/openai/",
"model": "gemini-2.0-flash",
"context_chars": 800_000,
},
"groq": {
"base_url": "https://api.groq.com/openai/v1",
"model": "llama-3.3-70b-versatile",
"context_chars": 128_000,
},
"xai": {
"base_url": "https://api.x.ai/v1",
"model": "grok-3-mini",
"context_chars": 128_000,
},
"deepseek": {
"base_url": "https://api.deepseek.com",
"model": "deepseek-chat",
"context_chars": 60_000,
},
"openrouter": {
"base_url": "https://openrouter.ai/api/v1",
"model": "anthropic/claude-3.5-sonnet",
"context_chars": 180_000,
},
"mistral": {
"base_url": "https://api.mistral.ai/v1",
"model": "mistral-large-latest",
"context_chars": 128_000,
},
"ollama": {
"base_url": "http://host.docker.internal:11434/v1",
"model": "llama3.2",
"context_chars": 8_000,
},
"lmstudio": {
"base_url": "http://host.docker.internal:1234/v1",
"model": "qwen/qwen3.5-9b",
"context_chars": 32_000,
},
}
# Whether the provider honours response_format={"type": "json_object"}.
# Gemini's OpenAI-compat endpoint does NOT support the string form (D-02/D-03).
# LMStudio only accepts "json_schema" or "text" (not "json_object") — set False
# so GenericOpenAIProvider omits response_format and relies on parse_classification().
# Ollama accepts the parameter but some models ignore it — left True; the
# GenericOpenAIProvider always wraps the raw response with parse_classification().
SUPPORTS_JSON_MODE: dict[str, bool] = {
"openai": True,
"anthropic": True,
"gemini": False,
"groq": True,
"xai": True,
"deepseek": True,
"openrouter": True,
"mistral": True,
"ollama": True,
"lmstudio": False,
}