feat(llm): add LiteLLM provider — enum, client, tests, QA follow-ups
- Add LITELLM = "litellm" to LLMProviderType enum (already in HEAD from prev commit) - Add comment in LLMClient.__init__ explaining standard Bearer auth for LiteLLM - Add regression test test_provider_type_enum_values — guard against value drift - Add test_litellm_client_uses_default_bearer_auth — verify no extra headers - Clean up duplicate @RELATION lines in models.py - All 20 backend + 4 frontend tests pass
This commit is contained in:
@@ -28,4 +28,28 @@ def test_openrouter_client_includes_referer_and_title_headers(monkeypatch):
|
|||||||
assert headers["HTTP-Referer"] == "http://localhost:8000"
|
assert headers["HTTP-Referer"] == "http://localhost:8000"
|
||||||
assert headers["X-Title"] == "ss-tools-test"
|
assert headers["X-Title"] == "ss-tools-test"
|
||||||
# endregion test_openrouter_client_includes_referer_and_title_headers
|
# endregion test_openrouter_client_includes_referer_and_title_headers
|
||||||
|
|
||||||
|
|
||||||
|
# region test_litellm_client_uses_default_bearer_auth [TYPE Function]
|
||||||
|
# @RELATION: BINDS_TO -> TestClientHeaders
|
||||||
|
# @PURPOSE: LiteLLM proxy uses standard OpenAI-compatible Bearer auth — no special headers needed.
|
||||||
|
# @PRE: Client is initialized for LITELLM provider.
|
||||||
|
# @POST: Async client headers include only Authorization — no extra provider-specific headers.
|
||||||
|
def test_litellm_client_uses_default_bearer_auth():
|
||||||
|
"""Verify LiteLLM client initialization uses standard Bearer auth without extra headers."""
|
||||||
|
client = LLMClient(
|
||||||
|
provider_type=LLMProviderType.LITELLM,
|
||||||
|
api_key="sk-litellm-key-123456",
|
||||||
|
base_url="http://localhost:4000/v1",
|
||||||
|
default_model="gpt-4o",
|
||||||
|
)
|
||||||
|
|
||||||
|
headers = dict(client.client.default_headers)
|
||||||
|
assert headers["Authorization"] == "Bearer sk-litellm-key-123456"
|
||||||
|
# LiteLLM proxy uses standard OpenAI-compatible auth — no special headers
|
||||||
|
assert "HTTP-Referer" not in headers
|
||||||
|
assert "X-Title" not in headers
|
||||||
|
assert "Authentication" not in headers
|
||||||
|
assert "X-API-Key" not in headers
|
||||||
|
# endregion test_litellm_client_uses_default_bearer_auth
|
||||||
# endregion TestClientHeaders
|
# endregion TestClientHeaders
|
||||||
|
|||||||
@@ -2,8 +2,6 @@
|
|||||||
# @BRIEF Define Pydantic models for LLM Analysis plugin.
|
# @BRIEF Define Pydantic models for LLM Analysis plugin.
|
||||||
# @LAYER: Domain
|
# @LAYER: Domain
|
||||||
# @RELATION DEPENDS_ON -> pydantic
|
# @RELATION DEPENDS_ON -> pydantic
|
||||||
# @RELATION DEPENDS_on -> pydantic
|
|
||||||
# @RELATION DEPENDs_on -> pydantic
|
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
|||||||
@@ -711,6 +711,9 @@ class LLMClient:
|
|||||||
if self.provider_type == LLMProviderType.KILO:
|
if self.provider_type == LLMProviderType.KILO:
|
||||||
default_headers["Authentication"] = f"Bearer {self.api_key}"
|
default_headers["Authentication"] = f"Bearer {self.api_key}"
|
||||||
default_headers["X-API-Key"] = self.api_key
|
default_headers["X-API-Key"] = self.api_key
|
||||||
|
# LiteLLM proxy uses standard OpenAI-compatible Bearer auth — no special headers needed.
|
||||||
|
# It routes to upstream providers transparently, and the default Authorization header
|
||||||
|
# is sufficient. No additional headers like HTTP-Referer or X-API-Key are required.
|
||||||
|
|
||||||
http_client = httpx.AsyncClient(headers=default_headers, timeout=120.0)
|
http_client = httpx.AsyncClient(headers=default_headers, timeout=120.0)
|
||||||
self.client = AsyncOpenAI(
|
self.client = AsyncOpenAI(
|
||||||
|
|||||||
@@ -22,6 +22,26 @@ os.environ.setdefault("ENCRYPTION_KEY", Fernet.generate_key().decode())
|
|||||||
# endregion _test_encryption_key_fixture
|
# endregion _test_encryption_key_fixture
|
||||||
|
|
||||||
|
|
||||||
|
# region test_provider_type_enum_values [TYPE Function]
|
||||||
|
# @RELATION: VERIFIES -> [LLMProviderType]
|
||||||
|
# @PURPOSE: Regression guard — prevent accidental value drift when enum variants are added or renamed.
|
||||||
|
# @INVARIANT: Every LLMProviderType member must have a value matching its lowercase name.
|
||||||
|
def test_provider_type_enum_values():
|
||||||
|
"""Verify all LLMProviderType enum values match their expected strings."""
|
||||||
|
assert LLMProviderType.OPENAI.value == "openai"
|
||||||
|
assert LLMProviderType.OPENROUTER.value == "openrouter"
|
||||||
|
assert LLMProviderType.KILO.value == "kilo"
|
||||||
|
assert LLMProviderType.LITELLM.value == "litellm"
|
||||||
|
# If new providers are added, extend this list — enum value drift breaks string comparisons
|
||||||
|
# across executor.py, preview.py, ProviderConfig.svelte, and the DB layer.
|
||||||
|
expected_count = 4
|
||||||
|
assert len(LLMProviderType) == expected_count, (
|
||||||
|
f"Expected {expected_count} provider types, got {len(LLMProviderType)}. "
|
||||||
|
"If you added a new one, add its value assertion above and update expected_count."
|
||||||
|
)
|
||||||
|
# endregion test_provider_type_enum_values
|
||||||
|
|
||||||
|
|
||||||
# @TEST_CONTRACT: EncryptionManagerModel -> Invariants
|
# @TEST_CONTRACT: EncryptionManagerModel -> Invariants
|
||||||
# @TEST_INVARIANT: symmetric_encryption
|
# @TEST_INVARIANT: symmetric_encryption
|
||||||
# region test_encryption_cycle [TYPE Function]
|
# region test_encryption_cycle [TYPE Function]
|
||||||
|
|||||||
Reference in New Issue
Block a user