de50f2c803
Extends the #215 abstraction: - OpenAICompatibleLLMProvider / OpenAICompatibleEmbeddingProvider — one impl (via the official openai SDK) covers OpenAI, xAI (api.x.ai/v1), Ollama (…:11434/v1), OpenRouter, etc.; they differ only by base_url, key, and model. - Registry factory: build_llm_providers() / build_embedding_providers() return every provider whose credentials are configured, so you can run several concurrently. get_llm_provider(name)/get_embedding_provider(name) select by name, falling back to default_*_provider, then Null. - Per-provider env config (ANTHROPIC_*, OPENAI_*, XAI_*, OLLAMA_*) + DEFAULT_LLM_PROVIDER / DEFAULT_EMBEDDING_PROVIDER; documented in .env.example. Defaults keep AI off (empty registry). Embeddings now have real backends (OpenAI/Ollama), still separate from the LLM since Anthropic offers no embeddings endpoint. Tests cover multi-provider selection, default resolution, disabled-without-credentials, and null fail-loud. Full suite 87 passed. Relates to #215. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Justin Paul <justin@jpaul.me>
85 lines
3.3 KiB
Python
85 lines
3.3 KiB
Python
"""Model-provider registry: configure several vendors at once, select by name,
|
|
default selection, and the null fail-loud behavior. No network — we only assert
|
|
which provider the factory returns and that null providers raise.
|
|
"""
|
|
|
|
import pytest
|
|
|
|
from app.api.deps import (
|
|
build_embedding_providers,
|
|
build_llm_providers,
|
|
get_embedding_provider,
|
|
get_llm_provider,
|
|
)
|
|
from app.core.config import get_settings
|
|
from app.integrations.models.anthropic_provider import AnthropicLLMProvider
|
|
from app.integrations.models.base import ModelProviderNotConfigured
|
|
from app.integrations.models.null import NullEmbeddingProvider, NullLLMProvider
|
|
from app.integrations.models.openai_compat import (
|
|
OpenAICompatibleEmbeddingProvider,
|
|
OpenAICompatibleLLMProvider,
|
|
)
|
|
|
|
|
|
def _reset(monkeypatch):
|
|
s = get_settings()
|
|
for attr, val in {
|
|
"default_llm_provider": "null",
|
|
"default_embedding_provider": "null",
|
|
"anthropic_api_key": None,
|
|
"openai_api_key": None,
|
|
"xai_api_key": None,
|
|
"ollama_enabled": False,
|
|
}.items():
|
|
monkeypatch.setattr(s, attr, val)
|
|
return s
|
|
|
|
|
|
async def test_default_is_null_and_fails_loud(monkeypatch):
|
|
_reset(monkeypatch)
|
|
provider = get_llm_provider()
|
|
assert isinstance(provider, NullLLMProvider)
|
|
with pytest.raises(ModelProviderNotConfigured):
|
|
await provider.complete(prompt="hello")
|
|
assert isinstance(get_embedding_provider(), NullEmbeddingProvider)
|
|
|
|
|
|
async def test_multiple_llm_providers_at_once(monkeypatch):
|
|
s = _reset(monkeypatch)
|
|
monkeypatch.setattr(s, "anthropic_api_key", "sk-ant-x")
|
|
monkeypatch.setattr(s, "openai_api_key", "sk-openai-x")
|
|
monkeypatch.setattr(s, "xai_api_key", "xai-x")
|
|
monkeypatch.setattr(s, "ollama_enabled", True)
|
|
monkeypatch.setattr(s, "default_llm_provider", "anthropic")
|
|
|
|
registry = build_llm_providers()
|
|
assert set(registry) == {"anthropic", "openai", "xai", "ollama"}
|
|
# Select any by name.
|
|
assert isinstance(get_llm_provider("anthropic"), AnthropicLLMProvider)
|
|
assert isinstance(get_llm_provider("openai"), OpenAICompatibleLLMProvider)
|
|
assert isinstance(get_llm_provider("xai"), OpenAICompatibleLLMProvider)
|
|
assert isinstance(get_llm_provider("ollama"), OpenAICompatibleLLMProvider)
|
|
# Default resolves to the configured default.
|
|
assert isinstance(get_llm_provider(), AnthropicLLMProvider)
|
|
# Unknown name → null.
|
|
assert isinstance(get_llm_provider("nope"), NullLLMProvider)
|
|
|
|
|
|
async def test_provider_disabled_without_credentials(monkeypatch):
|
|
s = _reset(monkeypatch)
|
|
monkeypatch.setattr(s, "default_llm_provider", "openai") # default names openai…
|
|
# …but no openai key → registry empty → null fallback.
|
|
assert build_llm_providers() == {}
|
|
assert isinstance(get_llm_provider(), NullLLMProvider)
|
|
|
|
|
|
async def test_embedding_providers(monkeypatch):
|
|
s = _reset(monkeypatch)
|
|
monkeypatch.setattr(s, "openai_api_key", "sk-openai-x")
|
|
monkeypatch.setattr(s, "ollama_enabled", True)
|
|
monkeypatch.setattr(s, "default_embedding_provider", "openai")
|
|
registry = build_embedding_providers()
|
|
assert set(registry) == {"openai", "ollama"}
|
|
assert isinstance(get_embedding_provider(), OpenAICompatibleEmbeddingProvider)
|
|
assert isinstance(get_embedding_provider("ollama"), OpenAICompatibleEmbeddingProvider)
|