diff --git a/litellm/llms/zai/responses/transformation.py b/litellm/llms/zai/responses/transformation.py index fc562bd2cb3..c47b8b5943a 100644 --- a/litellm/llms/zai/responses/transformation.py +++ b/litellm/llms/zai/responses/transformation.py @@ -20,6 +20,8 @@ class ZAIResponsesAPIConfig(OpenAIResponsesAPIConfig): ``Authorization: Bearer`` header. """ + _ZAI_CHAT_API_BASE_SUFFIXES: Final = ("/api/paas/v4", "/api/coding/paas/v4") + @property def custom_llm_provider(self) -> LlmProviders: return LlmProviders.ZAI @@ -44,7 +46,17 @@ class ZAIResponsesAPIConfig(OpenAIResponsesAPIConfig): api_base: str | None, litellm_params: dict, ) -> str: - base_url = api_base or get_secret_str("ZAI_RESPONSES_API_BASE") or "https://api.z.ai/api/v1" + # ``litellm_params.api_base`` can carry the Z.AI chat-completions base + # (``/api/paas/v4``) when the generic provider resolver pre-fills it from + # the chat config. Z.AI serves Responses on a different base, so ignore + # the chat-only bases and use the Responses base instead. + normalized_api_base = (api_base or "").rstrip("/") + chat_base_passed_in: Final = normalized_api_base.endswith(self._ZAI_CHAT_API_BASE_SUFFIXES) + base_url = ( + api_base + if api_base and not chat_base_passed_in + else get_secret_str("ZAI_RESPONSES_API_BASE") or "https://api.z.ai/api/v1" + ) base_url = base_url.rstrip("/") if base_url.endswith("/responses"): diff --git a/tests/test_litellm/llms/zai/test_zai_responses_transformation.py b/tests/test_litellm/llms/zai/test_zai_responses_transformation.py index 0925ff21a22..064bc21d9b5 100644 --- a/tests/test_litellm/llms/zai/test_zai_responses_transformation.py +++ b/tests/test_litellm/llms/zai/test_zai_responses_transformation.py @@ -30,6 +30,46 @@ def test_zai_responses_url_defaults_to_responses_endpoint(monkeypatch): assert config.get_complete_url(api_base=api_base, litellm_params={}) == expected_url +def test_zai_responses_url_ignores_chat_completions_api_base(monkeypatch): + monkeypatch.delenv("ZAI_RESPONSES_API_BASE", raising=False) + config = ZAIResponsesAPIConfig() + + chat_bases = ( + "https://api.z.ai/api/paas/v4", + "https://api.z.ai/api/paas/v4/", + "https://api.z.ai/api/coding/paas/v4", + ) + + for chat_base in chat_bases: + assert config.get_complete_url(api_base=chat_base, litellm_params={}) == "https://api.z.ai/api/v1/responses" + + +def test_zai_responses_url_keeps_custom_api_base(monkeypatch): + monkeypatch.delenv("ZAI_RESPONSES_API_BASE", raising=False) + config = ZAIResponsesAPIConfig() + + assert ( + config.get_complete_url( + api_base="https://gateway.example.com/openai/v1", + litellm_params={}, + ) + == "https://gateway.example.com/openai/v1/responses" + ) + + +def test_zai_responses_url_env_overrides_chat_completions_api_base(monkeypatch): + monkeypatch.setenv("ZAI_RESPONSES_API_BASE", "https://gateway.example.com/responses-root") + config = ZAIResponsesAPIConfig() + + assert ( + config.get_complete_url( + api_base="https://api.z.ai/api/paas/v4", + litellm_params={}, + ) + == "https://gateway.example.com/responses-root/responses" + ) + + def test_zai_responses_headers_use_bearer_token(): config = ZAIResponsesAPIConfig() litellm_params = GenericLiteLLMParams(api_key="sk-zai")