From e754efc7bded303537c87101ac43f98d5fc0e106 Mon Sep 17 00:00:00 2001 From: busya Date: Fri, 15 May 2026 22:11:55 +0300 Subject: [PATCH] =?UTF-8?q?feat(llm):=20add=20LiteLLM=20provider=20?= =?UTF-8?q?=E2=80=94=20enum,=20client,=20tests,=20QA=20follow-ups?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- .../__tests__/test_client_headers.py | 24 +++++++++++++++++++ backend/src/plugins/llm_analysis/models.py | 2 -- backend/src/plugins/llm_analysis/service.py | 3 +++ .../services/__tests__/test_llm_provider.py | 20 ++++++++++++++++ 4 files changed, 47 insertions(+), 2 deletions(-) diff --git a/backend/src/plugins/llm_analysis/__tests__/test_client_headers.py b/backend/src/plugins/llm_analysis/__tests__/test_client_headers.py index 58d203be..0659d391 100644 --- a/backend/src/plugins/llm_analysis/__tests__/test_client_headers.py +++ b/backend/src/plugins/llm_analysis/__tests__/test_client_headers.py @@ -28,4 +28,28 @@ def test_openrouter_client_includes_referer_and_title_headers(monkeypatch): assert headers["HTTP-Referer"] == "http://localhost:8000" assert headers["X-Title"] == "ss-tools-test" # 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 diff --git a/backend/src/plugins/llm_analysis/models.py b/backend/src/plugins/llm_analysis/models.py index 400941c4..fb6c9f2f 100644 --- a/backend/src/plugins/llm_analysis/models.py +++ b/backend/src/plugins/llm_analysis/models.py @@ -2,8 +2,6 @@ # @BRIEF Define Pydantic models for LLM Analysis plugin. # @LAYER: Domain # @RELATION DEPENDS_ON -> pydantic -# @RELATION DEPENDS_on -> pydantic -# @RELATION DEPENDs_on -> pydantic from datetime import datetime from enum import Enum diff --git a/backend/src/plugins/llm_analysis/service.py b/backend/src/plugins/llm_analysis/service.py index 71333392..6421dae0 100644 --- a/backend/src/plugins/llm_analysis/service.py +++ b/backend/src/plugins/llm_analysis/service.py @@ -711,6 +711,9 @@ class LLMClient: if self.provider_type == LLMProviderType.KILO: default_headers["Authentication"] = f"Bearer {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) self.client = AsyncOpenAI( diff --git a/backend/src/services/__tests__/test_llm_provider.py b/backend/src/services/__tests__/test_llm_provider.py index 9cca56bf..d192a464 100644 --- a/backend/src/services/__tests__/test_llm_provider.py +++ b/backend/src/services/__tests__/test_llm_provider.py @@ -22,6 +22,26 @@ os.environ.setdefault("ENCRYPTION_KEY", Fernet.generate_key().decode()) # 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_INVARIANT: symmetric_encryption # region test_encryption_cycle [TYPE Function]