Files
provenance/backend/tests/test_model_providers.py
T
justin 330543f9ce Fix #215: pluggable LLM + embedding provider abstraction
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>
2026-06-09 12:51:01 -04:00

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"])