From beb5b5e49d6704b52f368ca14b161d3c502fd941 Mon Sep 17 00:00:00 2001 From: curo1305 Date: Thu, 4 Jun 2026 18:56:31 +0200 Subject: [PATCH] feat(07-02): ProviderConfig Pydantic model + PROVIDER_DEFAULTS + SUPPORTS_JSON_MODE - Creates backend/ai/provider_config.py as a pure data module (no provider imports) - ProviderConfig(BaseModel) with extra=forbid: provider_id, api_key, base_url, model, context_chars - PROVIDER_DEFAULTS dict with all 10 providers and verbatim base_url/model/context_chars from RESEARCH.md - SUPPORTS_JSON_MODE dict with gemini=False per JSON Compatibility Matrix (D-02) --- backend/ai/provider_config.py | 107 ++++++++++++++++++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 backend/ai/provider_config.py diff --git a/backend/ai/provider_config.py b/backend/ai/provider_config.py new file mode 100644 index 0000000..1ae20af --- /dev/null +++ b/backend/ai/provider_config.py @@ -0,0 +1,107 @@ +"""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 = 8000 + + +# 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": "gemma-4-e4b-it", + "context_chars": 8_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). +# Ollama and LMStudio accept the parameter but some models ignore it — the +# GenericOpenAIProvider always wraps the raw response with parse_classification() +# regardless, so they are left as True (the parameter is still sent). +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": True, +}