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