From 2b17f1ec88405c3e3ac994dcc15f2a75842e33b8 Mon Sep 17 00:00:00 2001 From: ZHOUKAILIAN <101053705+ZHOUKAILIAN@users.noreply.github.com> Date: Sun, 20 Sep 2026 20:37:36 +0800 Subject: [PATCH] fix(streamlake): complete provider discovery and test request flows --- .../provider_endpoints_support_backup.json | 17 ++++ .../openai_like/test_streamlake_provider.py | 86 +++++++++++++++++++ .../public_endpoints/test_public_endpoints.py | 23 +++++ .../panels/AddModelPanel.integration.test.tsx | 36 +++++++- .../components/provider_info_helpers.test.tsx | 14 +++ .../src/components/provider_info_helpers.tsx | 3 + 6 files changed, 178 insertions(+), 1 deletion(-) diff --git a/litellm/provider_endpoints_support_backup.json b/litellm/provider_endpoints_support_backup.json index 30c1e0b894e..248b0ba99e9 100644 --- a/litellm/provider_endpoints_support_backup.json +++ b/litellm/provider_endpoints_support_backup.json @@ -2606,6 +2606,23 @@ "messages": true, "responses": true } + }, + "streamlake": { + "display_name": "StreamLake (`streamlake`)", + "url": "https://docs.litellm.ai/docs/providers/openai_compatible", + "endpoints": { + "chat_completions": true, + "messages": false, + "responses": false, + "embeddings": false, + "image_generations": false, + "audio_transcriptions": false, + "audio_speech": false, + "moderations": false, + "batches": false, + "rerank": false, + "a2a": false + } } }, "endpoints": { diff --git a/tests/test_litellm/llms/openai_like/test_streamlake_provider.py b/tests/test_litellm/llms/openai_like/test_streamlake_provider.py index c41da775e2d..719dc1594a5 100644 --- a/tests/test_litellm/llms/openai_like/test_streamlake_provider.py +++ b/tests/test_litellm/llms/openai_like/test_streamlake_provider.py @@ -1,7 +1,10 @@ import json from pathlib import Path +from typing import Final +import httpx import pytest +import respx import litellm from litellm.litellm_core_utils.get_llm_provider_logic import get_llm_provider @@ -215,6 +218,89 @@ class TestStreamlakeProviderConfig: assert "reasoning_effort" not in supported +@pytest.mark.asyncio +@pytest.mark.parametrize( + "is_async, api_base", + ( + (False, None), + (False, "https://custom.streamlake.example/v1"), + (False, "https://custom.streamlake.example/v1/"), + (True, None), + (True, "https://custom.streamlake.example/v1"), + (True, "https://custom.streamlake.example/v1/"), + ), + ids=( + "sync-default", + "sync-override", + "sync-override-trailing-slash", + "async-default", + "async-override", + "async-override-trailing-slash", + ), +) +async def test_streamlake_completion_uses_openai_compatible_http( + is_async: bool, api_base: str | None, monkeypatch: pytest.MonkeyPatch +) -> None: + response_body: Final[dict[str, object]] = { + "id": "chatcmpl-streamlake-test", + "object": "chat.completion", + "created": 1710000000, + "model": "ep-test-endpoint", + "choices": [ + { + "index": 0, + "message": {"role": "assistant", "content": "StreamLake response"}, + "finish_reason": "stop", + } + ], + "usage": {"prompt_tokens": 3, "completion_tokens": 2, "total_tokens": 5}, + } + expected_url: Final[str] = f"{(api_base or STREAMLAKE_BASE_URL).rstrip('/')}/chat/completions" + expected_request_body: Final[dict[str, object]] = { + "messages": [{"role": "user", "content": "hello"}], + "model": "ep-test-endpoint", + "max_tokens": 512, + } + + def handler(request: httpx.Request) -> httpx.Response: + assert request.method == "POST" + assert str(request.url) == expected_url + assert request.headers["authorization"] == "Bearer sk-streamlake-test" + assert request.headers["content-type"] == "application/json" + assert json.loads(request.content) == expected_request_body + return httpx.Response(200, json=response_body, request=request) + + monkeypatch.setattr(litellm, "disable_aiohttp_transport", True) + litellm.in_memory_llm_clients_cache.flush_cache() + with respx.mock(assert_all_called=True) as respx_mock: + route: Final = respx_mock.post(expected_url).mock(side_effect=handler) + if is_async: + response: Final = await litellm.acompletion( + model="streamlake/ep-test-endpoint", + messages=[{"role": "user", "content": "hello"}], + api_key="sk-streamlake-test", + api_base=api_base, + max_completion_tokens=512, + ) + else: + response: Final = litellm.completion( + model="streamlake/ep-test-endpoint", + messages=[{"role": "user", "content": "hello"}], + api_key="sk-streamlake-test", + api_base=api_base, + max_completion_tokens=512, + ) + + assert route.call_count == 1 + assert len(response.choices) == 1 + assert response.choices[0].message.role == "assistant" + assert response.choices[0].message.content == "StreamLake response" + assert response.usage is not None + assert response.usage.prompt_tokens == 3 + assert response.usage.completion_tokens == 2 + assert response.usage.total_tokens == 5 + + class TestStreamlakeNoHardcodedEndpointIds: def test_provider_config_never_hardcodes_an_endpoint_id(self): path = Path(litellm.__file__).parent / "llms" / "openai_like" / "providers.json" diff --git a/tests/test_litellm/proxy/public_endpoints/test_public_endpoints.py b/tests/test_litellm/proxy/public_endpoints/test_public_endpoints.py index 680dd4df0ae..083ad56e8f9 100644 --- a/tests/test_litellm/proxy/public_endpoints/test_public_endpoints.py +++ b/tests/test_litellm/proxy/public_endpoints/test_public_endpoints.py @@ -963,6 +963,29 @@ def test_get_supported_endpoints_chat_completions_present(reset_endpoints_cache) assert len(chat["providers"]) > 0 +def test_get_supported_endpoints_streamlake_only_advertises_chat_completions( + reset_endpoints_cache: None, +) -> None: + app: Final[FastAPI] = FastAPI() + app.include_router(router) + client: Final[TestClient] = TestClient(app) + with client: + response: Final = client.get("/public/endpoints") + assert response.status_code == 200, response.text + + endpoints: Final = response.json()["endpoints"] + streamlake_endpoints: Final = { + endpoint["key"] + for endpoint in endpoints + if any(provider["slug"] == "streamlake" for provider in endpoint["providers"]) + } + assert streamlake_endpoints == {"chat_completions"} + + chat_completions: Final = next(endpoint for endpoint in endpoints if endpoint["key"] == "chat_completions") + assert chat_completions["endpoint"] == "/chat/completions" + assert "streamlake" in {provider["slug"] for provider in chat_completions["providers"]} + + def test_get_supported_endpoints_display_names_have_no_slug_suffix( reset_endpoints_cache, ): diff --git a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/panels/AddModelPanel.integration.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/panels/AddModelPanel.integration.test.tsx index efba26734ff..bf6e04c87cb 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/panels/AddModelPanel.integration.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/panels/AddModelPanel.integration.test.tsx @@ -22,7 +22,13 @@ vi.mock("@/app/(dashboard)/hooks/uiSettings/usePtuCostAttributionEnabled", () => usePtuCostAttributionEnabled: () => mockPtuEnabled(), })); -vi.mock("@/app/(dashboard)/hooks/models/useModelCostMap", () => ({ useModelCostMap: () => ({ data: {} }) })); +vi.mock("@/app/(dashboard)/hooks/models/useModelCostMap", () => ({ + useModelCostMap: () => ({ + data: { + "streamlake/GLM-5.3": { litellm_provider: "streamlake" }, + }, + }), +})); vi.mock("@/app/(dashboard)/hooks/credentials/useCredentials", () => ({ useCredentials: () => ({ data: { credentials: [] } }), @@ -60,6 +66,13 @@ vi.mock("@/app/(dashboard)/hooks/providers/useProviderFields", () => ({ { key: "api_base", label: "API Base", field_type: "text", required: false }, ], }, + { + provider: "StreamLake", + provider_display_name: "StreamLake", + litellm_provider: "streamlake", + default_model_placeholder: "streamlake/", + credential_fields: [{ key: "api_key", label: "API Key", field_type: "password", required: false }], + }, ], isLoading: false, error: null, @@ -154,6 +167,27 @@ describe("AddModelPanel submit payload contract", () => { }); }); + it("creates a StreamLake model from a public model selection and API key", async () => { + const { user, submit } = await setup(); + + await user.click(screen.getByRole("combobox", { name: /provider/i })); + await user.click(await screen.findByText("StreamLake")); + await user.click(screen.getByRole("combobox", { name: "Select models" })); + await user.click(await screen.findByText("streamlake/GLM-5.3")); + await user.type(screen.getByLabelText("API Key"), "sk-streamlake-test"); + await submit(); + + expect(lastCreatedModel()).toStrictEqual({ + model_name: "streamlake/GLM-5.3", + litellm_params: { + model: "streamlake/GLM-5.3", + custom_llm_provider: "streamlake", + api_key: "sk-streamlake-test", + }, + model_info: { ...baseModelInfo }, + }); + }); + it("registers four more keys as undefined once Advanced Settings opens", async () => { const { openAdvanced, fillRequired, submit } = await setup(); await fillRequired(); diff --git a/ui/litellm-dashboard/src/components/provider_info_helpers.test.tsx b/ui/litellm-dashboard/src/components/provider_info_helpers.test.tsx index 4c68e302267..207c6744c66 100644 --- a/ui/litellm-dashboard/src/components/provider_info_helpers.test.tsx +++ b/ui/litellm-dashboard/src/components/provider_info_helpers.test.tsx @@ -176,6 +176,7 @@ describe("provider_info_helpers", () => { Providers.PETALS, Providers.PG_VECTOR, Providers.PREDIBASE, + Providers.StreamLake, Providers.WANDB, Providers.ZAI, ]; @@ -328,6 +329,19 @@ describe("provider_info_helpers", () => { expect(result).toEqual(["gpt-3.5-turbo", "gpt-4"]); }); + it("should return StreamLake models and exclude models from other providers", () => { + const modelMap = { + "streamlake/GLM-5.3": { litellm_provider: "streamlake" }, + "streamlake/DeepSeek-V3": { litellm_provider: "streamlake" }, + "gpt-4o": { litellm_provider: "openai" }, + }; + const result = getProviderModels(Providers.StreamLake, modelMap); + + expect(provider_map.StreamLake).toBe("streamlake"); + expect(result).toEqual(["streamlake/GLM-5.3", "streamlake/DeepSeek-V3"]); + expect(result).not.toContain("gpt-4o"); + }); + it("should return models whose litellm_provider is a prefix-anchored variant of the provider", () => { const modelMap = { "anthropic-text-model": { litellm_provider: "anthropic_text" }, diff --git a/ui/litellm-dashboard/src/components/provider_info_helpers.tsx b/ui/litellm-dashboard/src/components/provider_info_helpers.tsx index 1a1a2fd73aa..3e7247f4bda 100644 --- a/ui/litellm-dashboard/src/components/provider_info_helpers.tsx +++ b/ui/litellm-dashboard/src/components/provider_info_helpers.tsx @@ -163,6 +163,7 @@ export enum Providers { SCX_AI = "SCX.ai", Snowflake = "Snowflake", Soniox = "Soniox", + StreamLake = "StreamLake", TEXT_COMPLETION_CODESTRAL = "Text-Completion-Codestral", TogetherAI = "TogetherAI", TOPAZ = "Topaz", @@ -280,6 +281,7 @@ export const provider_map: Record = { SCX_AI: "scx-ai", Snowflake: "snowflake", Soniox: "soniox", + StreamLake: "streamlake", TEXT_COMPLETION_CODESTRAL: "text-completion-codestral", TogetherAI: "together_ai", TOPAZ: "topaz", @@ -445,6 +447,7 @@ const providerPlaceholderMap: Partial> = { [Providers.SageMaker]: "sagemaker/jumpstart-dft-meta-textgeneration-llama-2-7b", [Providers.SCX_AI]: "scx-ai/GLM-5.2", [Providers.Snowflake]: "snowflake/mistral-7b", + [Providers.StreamLake]: "streamlake/", [Providers.Vertex_AI]: "gemini-pro", [Providers.VolcEngine]: "volcengine/", [Providers.Voyage]: "voyage/",