Files
kite/backend/ai/__init__.py
T
curo1305 efc177a155 feat(07-03): AnthropicProvider singleton + output_config + truncation — D-03/D-07/D-12/D-13
- 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
2026-06-04 19:10:05 +02:00

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,
)