330543f9ce
Adds the vendor-agnostic seam the AI assistant + match-ranking plug into: - LLMProvider / EmbeddingProvider ABCs (base.py). LLM and embeddings are SEPARATE abstractions — Anthropic has no embeddings endpoint, so each is configured independently and either can be off. - NullLLMProvider / NullEmbeddingProvider — the default; fail loud with a clear "not configured" error so AI-off deployments don't silently no-op. - AnthropicLLMProvider — first concrete LLM impl, via the official anthropic SDK (default model claude-opus-4-8). A local provider (e.g. Ollama) would be another subclass of the same interface. - Factory in deps.py (get_llm_provider / get_embedding_provider) selects by env (MODEL_PROVIDER / EMBEDDING_PROVIDER); documented in .env.example. Providers are read-only text/vector producers — they never touch the DB, so the "AI never writes autonomously" invariant (CLAUDE.md #1) holds; writes will go through ChangeProposal (#214). Tests: provider selection (null default, anthropic when keyed, fallback without key) + null providers raise. 81 passed. Closes #215 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Justin Paul <justin@jpaul.me>
44 lines
1.7 KiB
Python
44 lines
1.7 KiB
Python
"""Model-provider selection + the null-provider fail-loud behavior.
|
|
|
|
No network: we only assert which provider the factory returns and that the null
|
|
providers raise a clear error. (Live LLM/embedding calls aren't unit-tested.)
|
|
"""
|
|
|
|
import pytest
|
|
|
|
from app.api.deps import 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
|
|
|
|
|
|
async def test_default_llm_is_null_and_fails_loud(monkeypatch):
|
|
monkeypatch.setattr(get_settings(), "model_provider", "null")
|
|
provider = get_llm_provider()
|
|
assert isinstance(provider, NullLLMProvider)
|
|
with pytest.raises(ModelProviderNotConfigured):
|
|
await provider.complete(prompt="hello")
|
|
|
|
|
|
async def test_anthropic_selected_when_configured(monkeypatch):
|
|
s = get_settings()
|
|
monkeypatch.setattr(s, "model_provider", "anthropic")
|
|
monkeypatch.setattr(s, "anthropic_api_key", "sk-ant-test-key")
|
|
monkeypatch.setattr(s, "llm_model", "claude-opus-4-8")
|
|
assert isinstance(get_llm_provider(), AnthropicLLMProvider) # no network call
|
|
|
|
|
|
async def test_anthropic_without_key_falls_back_to_null(monkeypatch):
|
|
s = get_settings()
|
|
monkeypatch.setattr(s, "model_provider", "anthropic")
|
|
monkeypatch.setattr(s, "anthropic_api_key", None)
|
|
assert isinstance(get_llm_provider(), NullLLMProvider)
|
|
|
|
|
|
async def test_embedding_default_is_null_and_fails_loud():
|
|
provider = get_embedding_provider()
|
|
assert isinstance(provider, NullEmbeddingProvider)
|
|
with pytest.raises(ModelProviderNotConfigured):
|
|
await provider.embed(["text"])
|