- Rewrites ai/__init__.py with _REGISTRY dict (10 entries) replacing the if/elif chain - get_provider() accepts ProviderConfig (not raw dict), raises ValueError on unknown - Resolves effective values: api_key fallback to "not-needed", model/base_url/context_chars from PROVIDER_DEFAULTS when config fields are empty/zero - SUPPORTS_JSON_MODE lookup sets supports_json_mode on GenericOpenAIProvider instances - AnthropicProvider instantiated without base_url (Plan 03 will widen the ctor) - provider_config.py: context_chars default changed to 0 (sentinel for PROVIDER_DEFAULTS resolution); 8000 default was inconsistent with O(1) factory pattern (Rule 1 fix)
90 lines
3.3 KiB
Python
90 lines
3.3 KiB
Python
"""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, no base_url ctor arg until Plan 03)
|
|
# 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 does not accept base_url until Plan 03 refactors it;
|
|
# pass only the args its current __init__ accepts.
|
|
return cls(
|
|
api_key=effective_api_key,
|
|
model=effective_model,
|
|
)
|
|
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,
|
|
)
|