- Delete MAX_AI_CHARS constant and _client() method from anthropic_provider.py
- Add self._client = AsyncAnthropic(...) singleton in __init__ (D-07)
- Add _truncate() with 60/40 split using self._context_chars (D-13)
- Add _CLASSIFICATION_SCHEMA and _SUGGESTIONS_SCHEMA module-level constants
- classify() and suggest_topics() pass output_config with json_schema format (D-03)
- stop_reason != "end_turn" degrades to parse_classification("") (T-07-08)
- Widen __init__ signature to (api_key, model, context_chars, base_url) (uniform ctor)
- Update ai/__init__.py to pass context_chars + base_url to AnthropicProvider
- Promote test_anthropic_structured_output + add test_anthropic_stop_reason_fallback
92 lines
3.4 KiB
Python
92 lines
3.4 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, 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,
|
|
)
|