feat(07-02): registry-based get_provider(config: ProviderConfig) — D-06
- 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)
This commit is contained in:
+81
-27
@@ -1,35 +1,89 @@
|
||||
from ai.base import AIProvider, ClassificationResult
|
||||
"""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.ollama_provider import OllamaProvider
|
||||
from ai.lmstudio_provider import LMStudioProvider
|
||||
from ai.generic_openai_provider import GenericOpenAIProvider
|
||||
from ai.provider_config import ProviderConfig, PROVIDER_DEFAULTS, SUPPORTS_JSON_MODE
|
||||
|
||||
|
||||
def get_provider(settings: dict) -> AIProvider:
|
||||
active = settings.get("active_provider", "lmstudio")
|
||||
providers = settings.get("providers", {})
|
||||
cfg = providers.get(active, {})
|
||||
# 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,
|
||||
}
|
||||
|
||||
if active == "anthropic":
|
||||
return AnthropicProvider(
|
||||
api_key=cfg.get("api_key", ""),
|
||||
model=cfg.get("model", "claude-sonnet-4-6"),
|
||||
|
||||
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 active == "openai":
|
||||
return OpenAIProvider(
|
||||
api_key=cfg.get("api_key", ""),
|
||||
model=cfg.get("model", "gpt-4o"),
|
||||
base_url=cfg.get("base_url") or None,
|
||||
)
|
||||
elif active == "ollama":
|
||||
return OllamaProvider(
|
||||
base_url=cfg.get("base_url", "http://host.docker.internal:11434"),
|
||||
model=cfg.get("model", "llama3.2"),
|
||||
)
|
||||
elif active == "lmstudio":
|
||||
return LMStudioProvider(
|
||||
base_url=cfg.get("base_url", "http://host.docker.internal:1234"),
|
||||
model=cfg.get("model", "gemma-4-e4b-it"),
|
||||
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:
|
||||
raise ValueError(f"Unknown AI provider: {active}")
|
||||
# OpenAIProvider
|
||||
return cls(
|
||||
api_key=effective_api_key,
|
||||
model=effective_model,
|
||||
base_url=effective_base_url,
|
||||
context_chars=effective_context_chars,
|
||||
)
|
||||
|
||||
@@ -29,7 +29,7 @@ class ProviderConfig(BaseModel):
|
||||
api_key: str = ""
|
||||
base_url: Optional[str] = None
|
||||
model: str = ""
|
||||
context_chars: int = 8000
|
||||
context_chars: int = 0 # 0 means "unset — use PROVIDER_DEFAULTS in get_provider()"
|
||||
|
||||
|
||||
# Named preset defaults for all 10 supported providers.
|
||||
|
||||
Reference in New Issue
Block a user