fix(streamlake): complete provider discovery and test request flows

This commit is contained in:
ZHOUKAILIAN 2026-09-20 20:37:36 +08:00
parent 18e5b988f6
commit 2b17f1ec88
6 changed files with 178 additions and 1 deletions

View file

@ -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": {

View file

@ -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"

View file

@ -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,
):

View file

@ -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/<model-or-endpoint>",
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();

View file

@ -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" },

View file

@ -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<string, string> = {
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<Record<Providers, string>> = {
[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/<model-or-endpoint>",
[Providers.Vertex_AI]: "gemini-pro",
[Providers.VolcEngine]: "volcengine/<any-model-on-volcengine>",
[Providers.Voyage]: "voyage/",