mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-28 01:32:17 +00:00
Merge 234a4b6908 into 2dccc0dc79
This commit is contained in:
commit
a5edff1d67
7 changed files with 342 additions and 0 deletions
1
litellm/llms/zai/messages/__init__.py
Normal file
1
litellm/llms/zai/messages/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
|
||||
72
litellm/llms/zai/messages/transformation.py
Normal file
72
litellm/llms/zai/messages/transformation.py
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
"""
|
||||
Z.AI Anthropic-compatible messages transformation config.
|
||||
"""
|
||||
|
||||
from collections.abc import Mapping
|
||||
from typing import Final
|
||||
|
||||
import litellm
|
||||
from litellm.llms.anthropic.experimental_pass_through.messages.transformation import (
|
||||
AnthropicMessagesConfig,
|
||||
)
|
||||
from litellm.secret_managers.main import get_secret_str
|
||||
|
||||
|
||||
class ZAIAnthropicMessagesConfig(AnthropicMessagesConfig):
|
||||
"""
|
||||
Z.AI exposes an Anthropic-compatible Messages API at
|
||||
https://api.z.ai/api/anthropic (see
|
||||
https://docs.z.ai/guides/llm/glm-5.3).
|
||||
|
||||
The endpoint accepts the native Anthropic Messages conversation shape
|
||||
and authenticates with the Z.AI API key sent as the Anthropic
|
||||
``x-api-key`` header.
|
||||
"""
|
||||
|
||||
@property
|
||||
def custom_llm_provider(self) -> str | None:
|
||||
return "zai"
|
||||
|
||||
@staticmethod
|
||||
def get_api_key(api_key: str | None = None) -> str | None:
|
||||
return api_key or get_secret_str("ZAI_API_KEY") or litellm.api_key
|
||||
|
||||
@staticmethod
|
||||
def get_api_base(api_base: str | None = None) -> str:
|
||||
return api_base or "https://api.z.ai/api/anthropic"
|
||||
|
||||
def validate_anthropic_messages_environment(
|
||||
self,
|
||||
headers: Mapping[str, str],
|
||||
model: str,
|
||||
messages: list[Mapping[str, object]], # mutable-ok: matches the pass-through handler's message list contract
|
||||
optional_params: Mapping[str, object],
|
||||
litellm_params: Mapping[str, object],
|
||||
api_key: str | None = None,
|
||||
api_base: str | None = None,
|
||||
) -> tuple[dict[str, str], str | None]: # mutable-ok: the handler owns and mutates the returned headers dict
|
||||
return super().validate_anthropic_messages_environment(
|
||||
headers=headers,
|
||||
model=model,
|
||||
messages=messages,
|
||||
optional_params=optional_params,
|
||||
litellm_params=litellm_params,
|
||||
api_key=self.get_api_key(api_key=api_key),
|
||||
api_base=api_base,
|
||||
)
|
||||
|
||||
def get_complete_url(
|
||||
self,
|
||||
api_base: str | None,
|
||||
api_key: str | None,
|
||||
model: str,
|
||||
optional_params: Mapping[str, object],
|
||||
litellm_params: Mapping[str, object],
|
||||
stream: bool | None = None,
|
||||
) -> str:
|
||||
raw_base_url: Final = self.get_api_base(api_base=api_base).rstrip("/")
|
||||
root_url: Final = raw_base_url.removesuffix("/v1/messages").removesuffix("/v1").removesuffix("/beta")
|
||||
|
||||
if root_url.endswith("/anthropic") or "/anthropic/" in root_url:
|
||||
return f"{root_url}/v1/messages"
|
||||
return f"{root_url}/anthropic/v1/messages"
|
||||
1
litellm/llms/zai/responses/__init__.py
Normal file
1
litellm/llms/zai/responses/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
|
||||
64
litellm/llms/zai/responses/transformation.py
Normal file
64
litellm/llms/zai/responses/transformation.py
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
"""
|
||||
Z.AI OpenAI-compatible Responses API transformation config.
|
||||
"""
|
||||
|
||||
from collections.abc import Mapping
|
||||
from typing import Final
|
||||
|
||||
import litellm
|
||||
from litellm.llms.openai.responses.transformation import OpenAIResponsesAPIConfig
|
||||
from litellm.secret_managers.main import get_secret_str
|
||||
from litellm.types.router import GenericLiteLLMParams
|
||||
from litellm.types.utils import LlmProviders
|
||||
|
||||
|
||||
class ZAIResponsesAPIConfig(OpenAIResponsesAPIConfig):
|
||||
"""
|
||||
Z.AI exposes an OpenAI-compatible Responses API at
|
||||
https://api.z.ai/api/v1 (see https://docs.z.ai/guides/llm/glm-5.3).
|
||||
|
||||
The endpoint authenticates with the Z.AI API key sent as an
|
||||
``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
|
||||
|
||||
@staticmethod
|
||||
def get_api_key(api_key: str | None = None) -> str | None:
|
||||
return api_key or get_secret_str("ZAI_API_KEY") or litellm.api_key
|
||||
|
||||
def validate_environment(
|
||||
self,
|
||||
headers: Mapping[str, str],
|
||||
model: str,
|
||||
litellm_params: GenericLiteLLMParams | None,
|
||||
) -> dict[str, str]: # mutable-ok: the responses handler owns and mutates the returned headers dict
|
||||
request_api_key: Final = litellm_params.api_key if litellm_params is not None else None
|
||||
resolved_params: Final = GenericLiteLLMParams(api_key=self.get_api_key(api_key=request_api_key))
|
||||
|
||||
return super().validate_environment(
|
||||
headers=headers,
|
||||
model=model,
|
||||
litellm_params=resolved_params,
|
||||
)
|
||||
|
||||
def get_complete_url(
|
||||
self,
|
||||
api_base: str | None,
|
||||
litellm_params: Mapping[str, object],
|
||||
) -> str:
|
||||
# ``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: Final = (api_base or "").rstrip("/")
|
||||
chat_base_passed_in: Final = normalized_api_base.endswith(self._ZAI_CHAT_API_BASE_SUFFIXES)
|
||||
base_url: Final = normalized_api_base if api_base and not chat_base_passed_in else "https://api.z.ai/api/v1"
|
||||
|
||||
if base_url.endswith("/responses"):
|
||||
return base_url
|
||||
return f"{base_url}/responses"
|
||||
|
|
@ -8851,6 +8851,12 @@ class ProviderConfigManager:
|
|||
)
|
||||
|
||||
return TencentAnthropicMessagesConfig()
|
||||
elif litellm.LlmProviders.ZAI == provider:
|
||||
from litellm.llms.zai.messages.transformation import (
|
||||
ZAIAnthropicMessagesConfig,
|
||||
)
|
||||
|
||||
return ZAIAnthropicMessagesConfig()
|
||||
elif litellm.LlmProviders.GITHUB_COPILOT == provider:
|
||||
if "claude" in model_lower:
|
||||
from litellm.llms.github_copilot.messages.transformation import (
|
||||
|
|
@ -9094,6 +9100,12 @@ class ProviderConfigManager:
|
|||
return litellm.BedrockMantleResponsesAPIConfig(
|
||||
use_openai_path=mantle_base_segment(model, litellm.model_cost) == "openai/v1"
|
||||
)
|
||||
elif litellm.LlmProviders.ZAI == provider:
|
||||
from litellm.llms.zai.responses.transformation import (
|
||||
ZAIResponsesAPIConfig,
|
||||
)
|
||||
|
||||
return ZAIResponsesAPIConfig()
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
|
|
|
|||
|
|
@ -0,0 +1,96 @@
|
|||
import litellm
|
||||
from litellm.llms.anthropic.experimental_pass_through.messages.transformation import (
|
||||
AnthropicMessagesConfig,
|
||||
)
|
||||
from litellm.llms.zai.messages.transformation import ZAIAnthropicMessagesConfig
|
||||
from litellm.utils import ProviderConfigManager
|
||||
|
||||
|
||||
def test_zai_provider_uses_anthropic_messages_config():
|
||||
config = ProviderConfigManager.get_provider_anthropic_messages_config(
|
||||
model="glm-5.3",
|
||||
provider=litellm.LlmProviders.ZAI,
|
||||
)
|
||||
|
||||
assert isinstance(config, ZAIAnthropicMessagesConfig)
|
||||
assert config.custom_llm_provider == "zai"
|
||||
|
||||
|
||||
def test_anthropic_provider_keeps_default_config_for_zai_named_model():
|
||||
config = ProviderConfigManager.get_provider_anthropic_messages_config(
|
||||
model="glm-5.3",
|
||||
provider=litellm.LlmProviders.ANTHROPIC,
|
||||
)
|
||||
|
||||
assert isinstance(config, AnthropicMessagesConfig)
|
||||
assert not isinstance(config, ZAIAnthropicMessagesConfig)
|
||||
|
||||
|
||||
def test_zai_anthropic_messages_config_defaults():
|
||||
config = ZAIAnthropicMessagesConfig()
|
||||
|
||||
assert config.custom_llm_provider == "zai"
|
||||
assert config.get_api_base() == "https://api.z.ai/api/anthropic"
|
||||
|
||||
|
||||
def test_zai_anthropic_messages_url_defaults_to_anthropic_endpoint():
|
||||
config = ZAIAnthropicMessagesConfig()
|
||||
|
||||
url_cases = {
|
||||
None: "https://api.z.ai/api/anthropic/v1/messages",
|
||||
"https://api.z.ai/api/anthropic": "https://api.z.ai/api/anthropic/v1/messages",
|
||||
"https://api.z.ai/api/anthropic/v1": "https://api.z.ai/api/anthropic/v1/messages",
|
||||
"https://api.z.ai/api/anthropic/v1/messages": "https://api.z.ai/api/anthropic/v1/messages",
|
||||
"https://api.z.ai/api": "https://api.z.ai/api/anthropic/v1/messages",
|
||||
}
|
||||
|
||||
for api_base, expected_url in url_cases.items():
|
||||
assert (
|
||||
config.get_complete_url(
|
||||
api_base=api_base,
|
||||
api_key=None,
|
||||
model="glm-5.3",
|
||||
optional_params={},
|
||||
litellm_params={},
|
||||
)
|
||||
== expected_url
|
||||
)
|
||||
|
||||
|
||||
def test_zai_anthropic_messages_headers_use_zai_key():
|
||||
config = ZAIAnthropicMessagesConfig()
|
||||
|
||||
headers, api_base = config.validate_anthropic_messages_environment(
|
||||
headers={},
|
||||
model="glm-5.3",
|
||||
messages=[],
|
||||
optional_params={},
|
||||
litellm_params={},
|
||||
api_key="sk-zai",
|
||||
api_base="https://example.test/anthropic",
|
||||
)
|
||||
|
||||
assert api_base == "https://example.test/anthropic"
|
||||
assert headers["x-api-key"] == "sk-zai"
|
||||
assert headers["anthropic-version"] == "2023-06-01"
|
||||
assert headers["content-type"] == "application/json"
|
||||
|
||||
|
||||
def test_zai_anthropic_messages_respects_existing_case_insensitive_auth_headers():
|
||||
config = ZAIAnthropicMessagesConfig()
|
||||
|
||||
headers, _ = config.validate_anthropic_messages_environment(
|
||||
headers={"Authorization": "Bearer caller-token"},
|
||||
model="glm-5.3",
|
||||
messages=[],
|
||||
optional_params={},
|
||||
litellm_params={},
|
||||
api_key="sk-zai",
|
||||
api_base="https://api.z.ai/api/anthropic",
|
||||
)
|
||||
|
||||
assert headers == {
|
||||
"Authorization": "Bearer caller-token",
|
||||
"anthropic-version": "2023-06-01",
|
||||
"content-type": "application/json",
|
||||
}
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
import litellm
|
||||
from litellm.llms.zai.responses.transformation import ZAIResponsesAPIConfig
|
||||
from litellm.types.router import GenericLiteLLMParams
|
||||
from litellm.types.utils import LlmProviders
|
||||
from litellm.utils import ProviderConfigManager
|
||||
|
||||
|
||||
def test_zai_provider_uses_responses_api_config():
|
||||
config = ProviderConfigManager.get_provider_responses_api_config(
|
||||
model="glm-5.3",
|
||||
provider=litellm.LlmProviders.ZAI,
|
||||
)
|
||||
|
||||
assert isinstance(config, ZAIResponsesAPIConfig)
|
||||
assert config.custom_llm_provider == LlmProviders.ZAI
|
||||
|
||||
|
||||
def test_zai_responses_url_defaults_to_responses_endpoint():
|
||||
config = ZAIResponsesAPIConfig()
|
||||
|
||||
url_cases = {
|
||||
None: "https://api.z.ai/api/v1/responses",
|
||||
"https://api.z.ai/api/v1": "https://api.z.ai/api/v1/responses",
|
||||
"https://api.z.ai/api/v1/": "https://api.z.ai/api/v1/responses",
|
||||
"https://api.z.ai/api/v1/responses": "https://api.z.ai/api/v1/responses",
|
||||
}
|
||||
|
||||
for api_base, expected_url in url_cases.items():
|
||||
assert config.get_complete_url(api_base=api_base, litellm_params={}) == expected_url
|
||||
|
||||
|
||||
def test_zai_responses_url_ignores_chat_completions_api_base():
|
||||
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():
|
||||
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_headers_use_bearer_token():
|
||||
config = ZAIResponsesAPIConfig()
|
||||
litellm_params = GenericLiteLLMParams(api_key="sk-zai")
|
||||
|
||||
headers = config.validate_environment(
|
||||
headers={},
|
||||
model="glm-5.3",
|
||||
litellm_params=litellm_params,
|
||||
)
|
||||
|
||||
assert headers["Authorization"] == "Bearer sk-zai"
|
||||
assert headers["Content-Type"] == "application/json"
|
||||
|
||||
|
||||
def test_zai_responses_headers_fall_back_to_environment_key(monkeypatch):
|
||||
monkeypatch.setenv("ZAI_API_KEY", "sk-zai-env")
|
||||
config = ZAIResponsesAPIConfig()
|
||||
|
||||
headers = config.validate_environment(
|
||||
headers={},
|
||||
model="glm-5.3",
|
||||
litellm_params=GenericLiteLLMParams(),
|
||||
)
|
||||
|
||||
assert headers["Authorization"] == "Bearer sk-zai-env"
|
||||
|
||||
|
||||
def test_zai_responses_headers_prefer_zai_key_over_global_key(monkeypatch):
|
||||
monkeypatch.setenv("ZAI_API_KEY", "sk-zai-env")
|
||||
monkeypatch.setattr(litellm, "api_key", "sk-global-other-provider", raising=False)
|
||||
|
||||
config = ZAIResponsesAPIConfig()
|
||||
|
||||
headers = config.validate_environment(
|
||||
headers={},
|
||||
model="glm-5.3",
|
||||
litellm_params=GenericLiteLLMParams(),
|
||||
)
|
||||
|
||||
assert headers["Authorization"] == "Bearer sk-zai-env"
|
||||
Loading…
Add table
Reference in a new issue