"""AI provider factory — registry-based O(1) lookup. Usage: from ai import get_provider from ai.provider_config import ProviderConfig config = ProviderConfig(provider_id="groq", api_key="sk-...") provider = get_provider(config) result = await provider.classify(text, topics, system_prompt) """ from __future__ import annotations from ai.base import AIProvider from ai.anthropic_provider import AnthropicProvider from ai.openai_provider import OpenAIProvider from ai.generic_openai_provider import GenericOpenAIProvider from ai.provider_config import ProviderConfig, PROVIDER_DEFAULTS, SUPPORTS_JSON_MODE # Registry: maps provider_id → provider class # "openai" uses OpenAIProvider (no response_format override needed — plain OpenAI) # "anthropic" uses AnthropicProvider (native output_config, Plan 03 widened ctor accepts context_chars+base_url) # All 8 OpenAI-compat vendors use GenericOpenAIProvider (D-16/D-17/D-18) _REGISTRY: dict[str, type[AIProvider]] = { "openai": OpenAIProvider, "anthropic": AnthropicProvider, "gemini": GenericOpenAIProvider, "groq": GenericOpenAIProvider, "xai": GenericOpenAIProvider, "deepseek": GenericOpenAIProvider, "openrouter": GenericOpenAIProvider, "mistral": GenericOpenAIProvider, "ollama": GenericOpenAIProvider, "lmstudio": GenericOpenAIProvider, } def get_provider(config: ProviderConfig) -> AIProvider: """Instantiate and return an AI provider for the given ProviderConfig. Resolves defaults from PROVIDER_DEFAULTS when config fields are absent, normalises an empty api_key to "not-needed" (OpenAI SDK 2.34+ rejects ""), and sets supports_json_mode from the SUPPORTS_JSON_MODE lookup for GenericOpenAIProvider instances. Args: config: A ProviderConfig with at minimum provider_id set. Returns: A fully-constructed AIProvider instance. Raises: ValueError: If config.provider_id is not in the registry. """ cls = _REGISTRY.get(config.provider_id) if cls is None: raise ValueError(f"Unknown AI provider: {config.provider_id!r}") defaults = PROVIDER_DEFAULTS[config.provider_id] # Resolve effective values — config fields take precedence over defaults effective_api_key = config.api_key or "not-needed" effective_model = config.model or defaults["model"] effective_base_url = config.base_url if config.base_url is not None else defaults["base_url"] effective_context_chars = config.context_chars or defaults["context_chars"] if config.provider_id == "anthropic": # AnthropicProvider accepts context_chars and base_url (Plan 03 widened ctor). # base_url is accepted for uniform factory signature but unused by the SDK. return cls( api_key=effective_api_key, model=effective_model, context_chars=effective_context_chars, base_url=effective_base_url, ) elif cls is GenericOpenAIProvider: return cls( api_key=effective_api_key, model=effective_model, base_url=effective_base_url, context_chars=effective_context_chars, supports_json_mode=SUPPORTS_JSON_MODE[config.provider_id], ) else: # OpenAIProvider return cls( api_key=effective_api_key, model=effective_model, base_url=effective_base_url, context_chars=effective_context_chars, )