This commit is contained in:
AaronHowell 2026-10-28 22:43:06 +00:00 • committed by GitHub
commit 8c90ce5b8f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
31 changed files with 2170 additions and 26 deletions

View file

@ -236,6 +236,7 @@ jobs:
tests/unit/llms/azure_ai
tests/unit/llms/base_llm
tests/unit/llms/baseten
tests/unit/llms/bigmodel
tests/unit/llms/black_forest_labs
tests/unit/llms/brave
tests/unit/llms/bytez

63
examples/bigmodel.md Normal file
View file

@ -0,0 +1,63 @@
# BigModel (Zhipu AI)
Use `bigmodel/` for the domestic BigModel platform and `zai/` for international Z.AI. Each platform uses its own credentials and default URL
```yaml
model_list:
- model_name: glm
litellm_params:
model: bigmodel/glm-5.3
api_key: os.environ/BIGMODEL_API_KEY
api_base: https://open.bigmodel.cn
```
One model configuration supports all three native APIs. The provider selects the upstream endpoint from the requested API
| Proxy endpoint | BigModel endpoint |
| --- | --- |
| `/v1/chat/completions` | `/api/paas/v4/chat/completions` |
| `/v1/responses` | `/api/v1/responses` |
| `/v1/messages` | `/api/anthropic/v1/messages` |
`api_base` is optional and defaults to `https://open.bigmodel.cn`. Set `BIGMODEL_API_BASE` to override the default, or pass an explicit `api_base` to override the environment value. Existing API-specific base URLs and complete endpoint URLs are normalized, preserving a custom proxy's path prefix
A GLM Coding Plan key uses `https://open.bigmodel.cn/api/coding/paas/v4` as its `api_base`, since Coding Plan and standard keys do not work on each other's endpoints. Chat Completions then calls the Coding Plan endpoint, and Responses and Messages keep the endpoints in the table above
The Admin UI lists BigModel (Zhipu AI), displays its official logo, and provides a required password API Key field and an optional API Base field with the default URL. Authentication uses an explicit `api_key` or `BIGMODEL_API_KEY`
```python
import litellm
chat = litellm.completion(
model="bigmodel/glm-5.3",
messages=[{"role": "user", "content": "Hello"}],
thinking={"type": "enabled"},
reasoning_effort="low",
)
response = litellm.responses(
model="bigmodel/glm-5.3",
input="Hello",
reasoning={"effort": "low"},
store=True,
)
async def messages():
return await litellm.anthropic.messages.acreate(
model="bigmodel/glm-5.3",
max_tokens=1024,
messages=[{"role": "user", "content": "Hello"}],
thinking={"type": "adaptive"},
output_config={"effort": "low"},
)
```
Each API retains its native response format and streaming events. The native Messages SDK entry point is asynchronous, as is the proxy's `/v1/messages` route
GLM-5.3, GLM-5.3-Flash, and GLM-5.3-FlashX use always-on thinking. GLM-5.3 supports `low`, `high`, and `max` reasoning effort. Messages preserves native `thinking`, `output_config`, and `reasoning_effort` parameters
Set `store=True` when creating a Responses request if you need continuation, retrieval, or deletion. Use the response ID returned by LiteLLM. BigModel documents a seven-day retention period and does not support cancel, compact, native WebSocket, or file_search
The model catalog and packaged backup include GLM-5.3, GLM-5.3-Flash, and GLM-5.3-FlashX. Prices are expressed in USD/token using a static 7.0 CNY/USD conversion, with the source and verification date in each entry's metadata. Use existing `litellm_params` cost fields for contract-specific deployment prices
API references: [Chat Completions](https://docs.bigmodel.cn/cn/guide/develop/openai/introduction), [Responses](https://docs.bigmodel.cn/cn/guide/develop/responses/introduction), [Messages](https://docs.bigmodel.cn/cn/guide/develop/claude/introduction), and [pricing](https://docs.bigmodel.cn/cn/guide/start/pricing), checked on 2026-10-27

View file

@ -635,6 +635,7 @@ watsonx_models: Set = set()
gemini_models: Set = set()
xai_models: Set = set()
zai_models: Set = set()
bigmodel_models: Final[set[str]] = set() # mutable-ok: provider registry is populated and extended at runtime
deepseek_models: Set = set()
tencent_models: Set = set()
runwayml_models: Set = set()
@ -876,6 +877,8 @@ def _populate_provider_model_sets(model_cost_map: Mapping[str, Mapping[str, obje
xai_models.add(key)
elif value.get("litellm_provider") == "zai":
zai_models.add(key)
elif value.get("litellm_provider") == "bigmodel":
bigmodel_models.add(key)
elif value.get("litellm_provider") == "fal_ai":
fal_ai_models.add(key)
elif value.get("litellm_provider") == "deepseek":
@ -1104,6 +1107,7 @@ model_list = list(
| text_completion_inception_models
| xai_models
| zai_models
| bigmodel_models
| fal_ai_models
| deepseek_models
| modelscope_models
@ -1217,6 +1221,7 @@ def _build_models_by_provider() -> dict:
"text-completion-inception": text_completion_inception_models,
"xai": xai_models,
"zai": zai_models,
"bigmodel": bigmodel_models,
"fal_ai": fal_ai_models,
"deepseek": deepseek_models,
"tencent": tencent_models,

View file

@ -18,6 +18,7 @@ from litellm.litellm_core_utils.bug_report import (
)
from litellm.litellm_core_utils.core_helpers import as_str_mapping, attribute_or_none
from litellm.litellm_core_utils.secret_redaction import redact_string as redact_secret_string
from litellm.llms.bigmodel.common_utils import raise_bigmodel_business_error
from litellm.types.utils import LlmProviders
from ..exceptions import (
@ -2762,6 +2763,8 @@ def exception_type(
exception_provider=exception_provider,
extra_information=extra_information,
)
if custom_llm_provider == "bigmodel":
raise_bigmodel_business_error(model, f"{exception_provider} - {error_str}", original_exception) # pyright: ignore[reportUnknownArgumentType] # exception_type takes untyped parameters
if custom_llm_provider == "openrouter":
_map_openrouter_exception(
model=model,

View file

@ -795,6 +795,13 @@ def _get_openai_compatible_provider_info(
api_base,
dynamic_api_key,
) = litellm.XAIChatConfig().get_openai_compatible_provider_info(api_base, api_key)
elif custom_llm_provider == "bigmodel":
from litellm.llms.bigmodel.common_utils import get_bigmodel_api_key, get_bigmodel_url
(
api_base, # rebind-ok: existing provider-resolution pipeline resolves the API base in place
dynamic_api_key, # rebind-ok: existing provider-resolution pipeline resolves the API key in place
) = get_bigmodel_url(api_base, "chat"), get_bigmodel_api_key(api_key)
elif custom_llm_provider == "zai":
(
api_base,

View file

@ -268,6 +268,9 @@ class BaseResponsesAPIConfig(ABC):
"""Returns True if litellm should fake a stream for the given model and stream value"""
return False
def may_answer_stream_with_json(self) -> bool:
return False
def supports_native_websocket(self) -> bool:
"""
Returns True if the provider has a native WebSocket endpoint for Responses API.

View file

View file

View file

@ -0,0 +1,59 @@
from collections.abc import Mapping
from typing import Final
from litellm.llms.zai.chat.transformation import ZAIChatConfig
from ..common_utils import get_bigmodel_api_key, get_bigmodel_url, is_unsupported_disabled_thinking
_SUPPORTED_PARAMS: Final = (
"max_tokens",
"max_completion_tokens",
"stream",
"stream_options",
"temperature",
"top_p",
"stop",
"tools",
"tool_choice",
"response_format",
"thinking",
"reasoning_effort",
)
class BigModelChatConfig(ZAIChatConfig):
@property
def custom_llm_provider(self) -> str:
return "bigmodel"
def get_openai_compatible_provider_info(self, api_base: str | None, api_key: str | None) -> tuple[str, str | None]:
return get_bigmodel_url(api_base, "chat"), get_bigmodel_api_key(api_key)
def get_supported_openai_params(self, model: str) -> list[str]: # mutable-ok: inherited interface requires a list
return list(_SUPPORTED_PARAMS)
def map_openai_params(
self,
non_default_params: Mapping[str, object],
optional_params: Mapping[str, object],
model: str,
drop_params: bool,
) -> dict[str, object]: # mutable-ok: request pipeline adds parameters to the returned body
mapped: Final = {
**optional_params,
**{
("max_tokens" if key == "max_completion_tokens" else key): value
for key, value in non_default_params.items()
if key in _SUPPORTED_PARAMS and key != "thinking"
},
}
if "thinking" not in non_default_params:
return mapped
thinking: Final = non_default_params["thinking"]
if is_unsupported_disabled_thinking(model, thinking):
return {
**mapped,
"reasoning_effort": mapped.get("reasoning_effort", "low"),
"extra_body": {"thinking": {"type": "enabled"}},
}
return {**mapped, "extra_body": {"thinking": thinking}}

View file

@ -0,0 +1,92 @@
import re
from collections.abc import Mapping
from types import MappingProxyType
from typing import Final, Literal
from urllib.parse import urlsplit, urlunsplit
import httpx
from pydantic import JsonValue, TypeAdapter, ValidationError
import litellm
from litellm.exceptions import ContentPolicyViolationError, ContextWindowExceededError
from litellm.llms.base_llm.chat.transformation import BaseLLMException
from litellm.secret_managers.main import get_secret_str
BIGMODEL_API_BASE: Final = "https://open.bigmodel.cn"
_JSON_OBJECT: Final = TypeAdapter(dict[str, JsonValue])
_BUSINESS_CODE_IN_ERROR: Final = re.compile(r"""['"]code['"]\s*:\s*['"]?(\d{4})\b""")
_CODING_CHAT_PATH: Final = "/api/coding/paas/v4"
_API_PATHS: Final = (
"/api/coding/paas/v4/chat/completions",
_CODING_CHAT_PATH,
"/api/paas/v4/chat/completions",
"/api/anthropic/v1/messages",
"/api/anthropic/v1",
"/api/v1/responses",
"/api/paas/v4",
"/api/anthropic",
"/api/v1",
"/api",
)
_ENDPOINT_PATHS: Final = MappingProxyType(
{
"chat": "/api/paas/v4",
"responses": "/api/v1/responses",
"messages": "/api/anthropic/v1/messages",
}
)
def get_bigmodel_api_key(api_key: str | None) -> str | None:
return api_key or get_secret_str("BIGMODEL_API_KEY")
def get_bigmodel_url(api_base: str | None, endpoint: Literal["chat", "responses", "messages"]) -> str:
resolved_base: Final = api_base or get_secret_str("BIGMODEL_API_BASE") or BIGMODEL_API_BASE
parsed: Final = urlsplit(resolved_base)
path: Final = parsed.path.rstrip("/")
suffix: Final = next((candidate for candidate in _API_PATHS if path.endswith(candidate)), "")
endpoint_path: Final = (
_CODING_CHAT_PATH if endpoint == "chat" and suffix.startswith(_CODING_CHAT_PATH) else _ENDPOINT_PATHS[endpoint]
)
return urlunsplit(
(parsed.scheme, parsed.netloc, f"{path.removesuffix(suffix)}{endpoint_path}", parsed.query, parsed.fragment)
)
def bigmodel_json_body(response: httpx.Response) -> Mapping[str, JsonValue]:
try:
return _JSON_OBJECT.validate_json(response.content)
except ValidationError:
raise BaseLLMException(message=response.text, status_code=response.status_code, headers=response.headers)
def is_unsupported_disabled_thinking(model: str, thinking: object) -> bool:
return thinking == {"type": "disabled"} and litellm.utils.supports_factory(
model=model, custom_llm_provider="bigmodel", key="thinking_always_on"
)
def bigmodel_business_error(
code: JsonValue, message: str, model: str, response: httpx.Response | None
) -> ContextWindowExceededError | ContentPolicyViolationError | None:
match code:
case 1261 | "1261":
return ContextWindowExceededError(message=message, model=model, llm_provider="bigmodel", response=response)
case 1301 | "1301":
return ContentPolicyViolationError(message=message, model=model, llm_provider="bigmodel", response=response)
case _:
return None
def raise_bigmodel_business_error(model: object, error_str: str, original_exception: object) -> None:
found: Final = _BUSINESS_CODE_IN_ERROR.search(error_str)
response: Final = getattr(original_exception, "response", None)
error: Final = bigmodel_business_error(
found.group(1) if found else None,
error_str,
str(model),
response if isinstance(response, httpx.Response) else None,
)
if error is not None:
raise error

View file

@ -0,0 +1,206 @@
import json
from collections.abc import AsyncIterator, Mapping, Sequence
from typing import Final
import httpx
from openai._streaming import ServerSentEvent, SSEDecoder
from pydantic import JsonValue, TypeAdapter, ValidationError
from litellm.exceptions import AuthenticationError
from litellm.litellm_core_utils.litellm_logging import Logging
from litellm.llms.openai_like.messages.transformation import (
DEFAULT_ANTHROPIC_API_VERSION,
OpenAILikeAnthropicMessagesConfig,
)
from litellm.types.llms.anthropic_messages.anthropic_response import AnthropicMessagesResponse
from litellm.types.router import GenericLiteLLMParams
from ..common_utils import (
bigmodel_json_body,
get_bigmodel_api_key,
get_bigmodel_url,
is_unsupported_disabled_thinking,
)
_JSON_OBJECT: Final = TypeAdapter(dict[str, JsonValue])
_OUTPUT_CONFIG: Final = TypeAdapter(dict[str, object])
def _with_lowest_effort_thinking(
params: Mapping[str, object],
) -> dict[str, object]: # mutable-ok: the parent transform takes a request dict
requested_output_config: Final = params.get("output_config")
output_config: Final = {
**({} if "reasoning_effort" in params else {"effort": "low"}),
**(
_OUTPUT_CONFIG.validate_python(requested_output_config) if isinstance(requested_output_config, dict) else {}
),
}
return {**params, "thinking": {"type": "adaptive"}, **({"output_config": output_config} if output_config else {})}
def _normalize_message(body: Mapping[str, JsonValue]) -> dict[str, JsonValue]: # mutable-ok: native JSON response
usage: Final = body.get("usage")
if not isinstance(usage, dict):
return dict(body)
server_tools: Final = usage.get("server_tool_use")
if not isinstance(server_tools, dict) or "web_fetch_requests" in server_tools:
return dict(body)
normalized: Final[JsonValue] = {
**body,
"usage": {**usage, "server_tool_use": {"web_fetch_requests": 0, **server_tools}},
}
return normalized
class _BigModelMessagesStream(httpx.AsyncByteStream):
def __init__(self, response: httpx.Response) -> None:
self._response = response
async def __aiter__(self) -> AsyncIterator[bytes]:
async for event in SSEDecoder().aiter_bytes(self._response.aiter_bytes()):
yield _normalize_event(event)
async def aclose(self) -> None:
await self._response.aclose()
def _json_object(data: str) -> Mapping[str, JsonValue] | None:
try:
return _JSON_OBJECT.validate_json(data)
except ValidationError:
return None
def _normalized_data(data: str) -> str:
body: Final = _json_object(data)
if body is None:
return data
message: Final = body.get("message")
normalized: Final[JsonValue] = (
{**body, "message": _normalize_message(message)}
if body.get("type") == "message_start" and isinstance(message, dict)
else _normalize_message(body)
)
return data if normalized == body else json.dumps(normalized, ensure_ascii=False)
def _normalize_event(event: ServerSentEvent) -> bytes:
metadata: Final = "".join(
f"{name}: {value}\n"
for name, value in (("event", event.event), ("id", event.id), ("retry", event.retry))
if value is not None
)
data_lines: Final = "".join(f"data: {line}\n" for line in _normalized_data(event.data).split("\n"))
return f"{metadata}{data_lines}\n".encode()
class BigModelMessagesConfig(OpenAILikeAnthropicMessagesConfig):
@property
def custom_llm_provider(self) -> str:
return "bigmodel"
def is_native_messages_route(self) -> bool:
return True
def should_strip_billing_metadata(self) -> bool:
return True
def transform_anthropic_messages_request(
self,
model: str,
messages: list[dict[str, object]], # mutable-ok: matches dict-typed base signature
anthropic_messages_optional_request_params: dict[str, object], # mutable-ok: matches dict-typed base signature
litellm_params: GenericLiteLLMParams,
headers: dict[str, str], # mutable-ok: matches dict-typed base signature
) -> dict[str, object]: # mutable-ok: matches dict-typed base signature
request_params: Final = (
_with_lowest_effort_thinking(anthropic_messages_optional_request_params)
if is_unsupported_disabled_thinking(model, anthropic_messages_optional_request_params.get("thinking"))
else anthropic_messages_optional_request_params
)
return super().transform_anthropic_messages_request( # pyright: ignore[reportUnknownMemberType, reportUnknownVariableType] # the parent transform returns a bare dict
model, messages, request_params, litellm_params, headers
)
def transform_anthropic_messages_response(
self, model: str, raw_response: httpx.Response, logging_obj: Logging
) -> AnthropicMessagesResponse:
normalized: Final = httpx.Response(
raw_response.status_code,
json=_normalize_message(bigmodel_json_body(raw_response)),
request=raw_response.request,
)
return super().transform_anthropic_messages_response(model, normalized, logging_obj)
def get_async_streaming_response_iterator(
self,
model: str,
httpx_response: httpx.Response,
request_body: dict[str, object], # mutable-ok: inherited native stream API takes a request dict
litellm_logging_obj: Logging,
) -> AsyncIterator[bytes]:
normalized: Final = httpx.Response(
httpx_response.status_code,
headers={
key: value
for key, value in httpx_response.headers.items()
if key.lower() not in ("content-encoding", "content-length")
},
stream=_BigModelMessagesStream(httpx_response),
request=httpx_response.request,
)
return super().get_async_streaming_response_iterator( # pyright: ignore[reportUnknownMemberType, reportUnknownVariableType] # native parent yields bytes but its iterator annotation omits the element type
model, normalized, request_body, litellm_logging_obj
)
@staticmethod
def _translate_reasoning_effort_to_anthropic(
model: str, optional_params: Mapping[str, object], max_tokens: int | None, custom_llm_provider: str
) -> None:
return None
@staticmethod
def _translate_adaptive_effort_for_non_adaptive_model(
model: str, optional_params: Mapping[str, object], max_tokens: int | None, custom_llm_provider: str
) -> None:
return None
@staticmethod
def _drop_incompatible_temperature_for_thinking(
model: str, optional_params: Mapping[str, object], custom_llm_provider: str
) -> None:
return None
def validate_anthropic_messages_environment(
self,
headers: Mapping[str, str],
model: str,
messages: Sequence[object],
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: HTTP handler merges forwarded headers
resolved_key: Final = get_bigmodel_api_key(api_key)
if resolved_key is None:
raise AuthenticationError(
message="Set BIGMODEL_API_KEY or pass api_key", llm_provider="bigmodel", model=model
)
return {
**{key: value for key, value in headers.items() if key.lower() not in ("x-api-key", "authorization")},
"x-api-key": resolved_key,
"anthropic-version": DEFAULT_ANTHROPIC_API_VERSION,
"content-type": "application/json",
}, 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:
return get_bigmodel_url(api_base, "messages")

View file

@ -0,0 +1,263 @@
from collections.abc import Mapping
from types import MappingProxyType
from typing import Final
import httpx
from pydantic import BaseModel, ConfigDict, JsonValue, TypeAdapter, ValidationError
from typing_extensions import Never
from litellm.exceptions import AuthenticationError, UnsupportedParamsError
from litellm.litellm_core_utils.litellm_logging import Logging
from litellm.llms.base_llm.chat.transformation import BaseLLMException
from litellm.llms.openai.responses.transformation import OpenAIResponsesAPIConfig
from litellm.types.llms.openai import (
ResponseInputParam,
ResponsesAPIOptionalRequestParams,
ResponsesAPIResponse,
ResponsesAPIStreamingResponse,
)
from litellm.types.router import GenericLiteLLMParams
from litellm.types.utils import LlmProviders
from ..common_utils import bigmodel_business_error, bigmodel_json_body, get_bigmodel_api_key, get_bigmodel_url
_JSON_OBJECT: Final = TypeAdapter(dict[str, JsonValue])
_SUPPORTED_PARAMS: Final = (
"input",
"model",
"instructions",
"stream",
"temperature",
"top_p",
"max_output_tokens",
"tools",
"tool_choice",
"parallel_tool_calls",
"include",
"reasoning",
"text",
"prompt_cache_key",
"previous_response_id",
"store",
"extra_headers",
"extra_query",
"extra_body",
"timeout",
)
_STATUS_BY_BUSINESS_CODE: Final = MappingProxyType(
{
**dict.fromkeys((1000, 1001, 1003, 1005), 401),
**dict.fromkeys((1210, 1211, 1212, 1213, 1214, 1215, 1221, 1222, 1261, 1301), 400),
1220: 403,
**dict.fromkeys(
(1113, 1302, 1305, 1308, 1309, 1310, 1311, 1313, 1314, 1315, 1316, 1317, 1318, 1319, 1320, 1321),
429,
),
}
)
class _RequestedTools(BaseModel):
model_config = ConfigDict(frozen=True, extra="ignore")
tools: JsonValue = None
tool_choice: JsonValue = None
parallel_tool_calls: JsonValue = None
def _requested_tools(logging_obj: Logging) -> _RequestedTools:
try:
return _RequestedTools.model_validate(logging_obj.model_call_details.get("optional_params") or {})
except ValidationError:
return _RequestedTools()
def _envelope_status_code(code: JsonValue) -> int:
if not isinstance(code, int) or isinstance(code, bool):
return 500
if 400 <= code <= 599:
return code
return _STATUS_BY_BUSINESS_CODE.get(code, 500)
def _normalize_output_item(item: JsonValue) -> JsonValue:
if not isinstance(item, dict) or item.get("type") not in ("message", "reasoning"):
return item
content: Final = item.get("content")
normalized: Final[JsonValue] = {"summary": [], **item} if item["type"] == "reasoning" else item
if not isinstance(content, dict):
return normalized
return {**normalized, "content": [content]}
def _normalize_response(
body: Mapping[str, JsonValue],
requested: _RequestedTools,
) -> dict[str, JsonValue]: # mutable-ok: JSON serialization boundary
usage: Final = body.get("usage")
output: Final = body.get("output")
tools: Final = body.get("tools")
tool_choice: Final = body.get("tool_choice")
parallel_tool_calls: Final = body.get("parallel_tool_calls")
input_tokens: Final = usage.get("input_tokens") if isinstance(usage, dict) else None
output_tokens: Final = usage.get("output_tokens") if isinstance(usage, dict) else None
normalized_usage: Final = (
{
"total_tokens": input_tokens + output_tokens,
**usage,
}
if isinstance(usage, dict) and isinstance(input_tokens, int) and isinstance(output_tokens, int)
else usage
)
normalized: Final[JsonValue] = {
**body,
"parallel_tool_calls": (
requested.parallel_tool_calls is not False if parallel_tool_calls is None else parallel_tool_calls
),
"tools": (requested.tools or []) if tools is None else tools,
"tool_choice": (requested.tool_choice or "auto") if tool_choice is None else tool_choice,
**({"usage": normalized_usage} if "usage" in body else {}),
**({"output": [_normalize_output_item(item) for item in output]} if isinstance(output, list) else {}),
}
return normalized
def _normalize_stream_part(part: JsonValue) -> JsonValue:
if not isinstance(part, dict):
return part
if part.get("type") == "output_text":
return {
"annotations": [],
"logprobs": None,
**part,
}
if part.get("type") == "reasoning_text":
return {"reasoning": part.get("text", ""), **part}
return part
class BigModelResponsesAPIConfig(OpenAIResponsesAPIConfig):
@property
def custom_llm_provider(self) -> LlmProviders:
return LlmProviders.BIGMODEL
def supports_native_file_search(self) -> bool:
return False
def supports_native_websocket(self) -> bool:
return False
def should_fake_stream(
self, model: str | None, stream: bool | None, custom_llm_provider: str | None = None
) -> bool:
return False
def may_answer_stream_with_json(self) -> bool:
return True
def get_supported_openai_params(self, model: str) -> list[str]: # mutable-ok: inherited API requires a list
return list(_SUPPORTED_PARAMS)
def map_openai_params(
self,
response_api_optional_params: ResponsesAPIOptionalRequestParams,
model: str,
drop_params: bool,
) -> dict[str, object]: # mutable-ok: request pipeline adds parameters to the returned body
return {key: value for key, value in response_api_optional_params.items() if key in _SUPPORTED_PARAMS}
def validate_environment(
self, headers: Mapping[str, str], model: str, litellm_params: GenericLiteLLMParams | None
) -> dict[str, str]: # mutable-ok: HTTP handler merges forwarded headers
resolved_key: Final = get_bigmodel_api_key(litellm_params.api_key if litellm_params else None)
if resolved_key is None:
raise AuthenticationError(
message="Set BIGMODEL_API_KEY or pass api_key", llm_provider="bigmodel", model=model
)
return {
**headers,
"Authorization": f"Bearer {resolved_key}",
"Content-Type": "application/json",
}
def get_complete_url(self, api_base: str | None, litellm_params: Mapping[str, object]) -> str:
return get_bigmodel_url(api_base, "responses")
def transform_response_api_response(
self, model: str, raw_response: httpx.Response, logging_obj: Logging
) -> ResponsesAPIResponse:
body: Final = bigmodel_json_body(raw_response)
if body.get("success") is False:
message: Final = str(body.get("msg") or raw_response.text)
raise bigmodel_business_error(body.get("code"), message, model, None) or BaseLLMException(
message=message,
status_code=_envelope_status_code(body.get("code")),
headers=raw_response.headers,
)
normalized: Final = _normalize_response(body, _requested_tools(logging_obj))
normalized_response: Final = httpx.Response(
status_code=raw_response.status_code,
headers={
key: value
for key, value in raw_response.headers.items()
if key.lower() not in ("content-encoding", "content-length")
},
json=normalized,
request=raw_response.request,
)
return super().transform_response_api_response(model, normalized_response, logging_obj)
def transform_get_response_api_response(
self, raw_response: httpx.Response, logging_obj: Logging
) -> ResponsesAPIResponse:
return self.transform_response_api_response("", raw_response, logging_obj)
def transform_streaming_response(
self, model: str, parsed_chunk: Mapping[str, object], logging_obj: Logging
) -> ResponsesAPIStreamingResponse:
chunk: Final = _JSON_OBJECT.validate_python(parsed_chunk)
response: Final = chunk.get("response")
item: Final = chunk.get("item")
normalized: Final[JsonValue] = {
**chunk,
**(
{"response": _normalize_response(response, _requested_tools(logging_obj))}
if isinstance(response, dict)
else {}
),
**({"item": _normalize_output_item(item)} if "item" in chunk else {}),
}
event_model: Final = self.get_event_model_class(str(normalized.get("type")))
match chunk.get("type"):
case "response.output_text.done":
return event_model.model_validate({"logprobs": [], **normalized})
case "response.content_part.done":
return event_model.model_validate({**normalized, "part": _normalize_stream_part(chunk.get("part"))})
case "error":
error: Final = chunk.get("error")
error_fields: Final = error if isinstance(error, dict) else chunk
normalized_error: Final[JsonValue] = {
**normalized,
"error": {"type": "api_error", **error_fields, "code": error_fields.get("code") or "unknown_error"},
}
return event_model.model_validate(normalized_error)
case _:
return event_model.model_validate(normalized)
def transform_cancel_response_api_request(
self, response_id: str, api_base: str, litellm_params: GenericLiteLLMParams, headers: Mapping[str, str]
) -> Never:
raise UnsupportedParamsError(message="BigModel does not support cancelling responses", status_code=400)
def transform_compact_response_api_request(
self,
model: str,
input: str | ResponseInputParam,
response_api_optional_request_params: Mapping[str, object],
api_base: str,
litellm_params: GenericLiteLLMParams,
headers: Mapping[str, str],
) -> Never:
raise UnsupportedParamsError(message="BigModel does not support compacting responses", status_code=400)

View file

@ -366,6 +366,19 @@ class _SignedBodyKwargs(TypedDict, total=False):
json: ReadOnly[dict[str, object]]
def _is_sized_json_body(response: httpx.Response) -> bool:
headers: Final = httpx.Headers(response.headers)
media_types: Final = (value.split(";")[0].strip().lower() for value in headers.get_list("content-type"))
return "content-length" in headers and "application/json" in media_types
def _is_json_document(content: bytes) -> bool:
try:
return isinstance(json.loads(content), dict)
except ValueError:
return False
def _signed_body_kwargs(*, signed_body: bytes | None, data: dict[str, object]) -> _SignedBodyKwargs:
if signed_body is not None:
return {"data": signed_body}
@ -3024,6 +3037,17 @@ class BaseLLMHTTPHandler:
},
)
replay_as_stream: Final = partial(
MockResponsesAPIStreamingIterator,
model=model,
logging_obj=logging_obj,
responses_api_provider_config=responses_api_provider_config,
litellm_metadata=litellm_metadata,
custom_llm_provider=custom_llm_provider,
request_data=request_context,
call_type=CallTypes.responses.value,
)
try:
if is_stream_request:
response = sync_httpx_client.post(
@ -3035,7 +3059,13 @@ class BaseLLMHTTPHandler:
)
if caller_requested_stream:
if fake_stream is True:
return MockResponsesAPIStreamingIterator(
return replay_as_stream(response=response)
if not (
responses_api_provider_config.may_answer_stream_with_json()
and _is_sized_json_body(response)
and _is_json_document(response.read())
):
return SyncResponsesAPIStreamingIterator(
response=response,
model=model,
logging_obj=logging_obj,
@ -3045,17 +3075,6 @@ class BaseLLMHTTPHandler:
request_data=request_context,
call_type=CallTypes.responses.value,
)
return SyncResponsesAPIStreamingIterator(
response=response,
model=model,
logging_obj=logging_obj,
responses_api_provider_config=responses_api_provider_config,
litellm_metadata=litellm_metadata,
custom_llm_provider=custom_llm_provider,
request_data=request_context,
call_type=CallTypes.responses.value,
)
response.read()
else:
response = sync_httpx_client.post(
@ -3070,6 +3089,9 @@ class BaseLLMHTTPHandler:
provider_config=responses_api_provider_config,
)
if is_stream_request and caller_requested_stream:
return replay_as_stream(response=response)
initial_response: Final = responses_api_provider_config.transform_response_api_response(
model=model,
raw_response=response,
@ -3225,6 +3247,17 @@ class BaseLLMHTTPHandler:
},
)
replay_as_stream: Final = partial(
MockResponsesAPIStreamingIterator,
model=model,
logging_obj=logging_obj,
responses_api_provider_config=responses_api_provider_config,
litellm_metadata=litellm_metadata,
custom_llm_provider=custom_llm_provider,
request_data=request_context,
call_type=CallTypes.responses.value,
)
try:
if is_stream_request:
response = await async_httpx_client.post(
@ -3238,7 +3271,13 @@ class BaseLLMHTTPHandler:
if caller_requested_stream:
if fake_stream is True:
return MockResponsesAPIStreamingIterator(
return replay_as_stream(response=response)
if not (
responses_api_provider_config.may_answer_stream_with_json()
and _is_sized_json_body(response)
and _is_json_document(await response.aread())
):
return ResponsesAPIStreamingIterator(
response=response,
model=model,
logging_obj=logging_obj,
@ -3248,17 +3287,6 @@ class BaseLLMHTTPHandler:
request_data=request_context,
call_type=CallTypes.responses.value,
)
return ResponsesAPIStreamingIterator(
response=response,
model=model,
logging_obj=logging_obj,
responses_api_provider_config=responses_api_provider_config,
litellm_metadata=litellm_metadata,
custom_llm_provider=custom_llm_provider,
request_data=request_context,
call_type=CallTypes.responses.value,
)
await response.aread()
else:
response = await async_httpx_client.post(
@ -3275,6 +3303,9 @@ class BaseLLMHTTPHandler:
provider_config=responses_api_provider_config,
)
if is_stream_request and caller_requested_stream:
return replay_as_stream(response=response)
initial_response: Final = responses_api_provider_config.transform_response_api_response(
model=model,
raw_response=response,

View file

@ -2672,10 +2672,13 @@ def _complete_custom_openai(
# for deepinfra/perplexity/anyscale/groq/friendliai we check in get_llm_provider and pass in the api base from there
api_base = _resolve_openai_api_base(api_base)
if custom_llm_provider == "bigmodel" and not api_key:
raise litellm.AuthenticationError(
message="Set BIGMODEL_API_KEY or pass api_key", llm_provider="bigmodel", model=model
)
organization = (
organization
or litellm.organization
or get_secret("OPENAI_ORGANIZATION")
or (None if custom_llm_provider == "bigmodel" else litellm.organization or get_secret("OPENAI_ORGANIZATION"))
or None # default - https://github.com/openai/openai-python/blob/284c1799070c723c6a553337134148a7ab088dd8/openai/util.py#L105
)
openai.organization = organization
@ -6299,6 +6302,7 @@ def completion(
or custom_llm_provider == "nebius"
or custom_llm_provider == "wandb"
or custom_llm_provider == "clarifai"
or custom_llm_provider == "bigmodel"
or custom_llm_provider in litellm.openai_compatible_providers
or JSONProviderRegistry.exists(custom_llm_provider) # JSON-configured providers
or "ft:gpt-3.5-turbo" in model # finetune gpt-3.5-turbo

View file

@ -78461,5 +78461,81 @@
"supports_response_schema": true,
"supports_tool_choice": true,
"supports_vision": true
},
"bigmodel/glm-5.3": {
"input_cost_per_token": 1.1428571428571428e-06,
"output_cost_per_token": 4e-06,
"cache_read_input_token_cost": 2.857142857142857e-07,
"litellm_provider": "bigmodel",
"max_input_tokens": 1000000,
"max_output_tokens": 128000,
"mode": "chat",
"source": "https://docs.bigmodel.cn/cn/guide/start/pricing",
"supports_function_calling": true,
"supports_prompt_caching": true,
"supports_reasoning": true,
"supports_tool_choice": true,
"thinking_always_on": true,
"metadata": {
"comment": "Official API list price checked 2026-10-27: input 8, output 28, cached input 2 CNY per million tokens; converted at 7.0 CNY/USD. Cache storage is temporarily free and is not encoded."
},
"supported_endpoints": [
"/v1/chat/completions",
"/v1/responses",
"/v1/messages"
],
"supports_native_streaming": true
},
"bigmodel/glm-5.3-flash": {
"input_cost_per_token": 1.142857142857143e-07,
"output_cost_per_token": 4e-07,
"cache_read_input_token_cost": 3.2857142857142855e-08,
"litellm_provider": "bigmodel",
"max_input_tokens": 1000000,
"max_output_tokens": 128000,
"mode": "chat",
"source": "https://docs.bigmodel.cn/cn/guide/start/pricing",
"supports_function_calling": true,
"supports_prompt_caching": true,
"supports_reasoning": true,
"supports_tool_choice": true,
"thinking_always_on": true,
"supports_vision": true,
"supports_video_input": true,
"metadata": {
"comment": "Official API list price checked 2026-10-27: input 0.8, output 2.8, cached input 0.23 CNY per million tokens; converted at 7.0 CNY/USD. Cache storage is temporarily free and is not encoded."
},
"supported_endpoints": [
"/v1/chat/completions",
"/v1/responses",
"/v1/messages"
],
"supports_native_streaming": true
},
"bigmodel/glm-5.3-flashx": {
"input_cost_per_token": 2.857142857142857e-07,
"output_cost_per_token": 1e-06,
"cache_read_input_token_cost": 8.142857142857142e-08,
"litellm_provider": "bigmodel",
"max_input_tokens": 1000000,
"max_output_tokens": 128000,
"mode": "chat",
"source": "https://docs.bigmodel.cn/cn/guide/start/pricing",
"supports_function_calling": true,
"supports_prompt_caching": true,
"supports_reasoning": true,
"supports_tool_choice": true,
"thinking_always_on": true,
"supports_vision": true,
"supports_video_input": true,
"metadata": {
"comment": "Official API list price checked 2026-10-27: input 2, output 7, cached input 0.57 CNY per million tokens; converted at 7.0 CNY/USD. Cache storage is temporarily free and is not encoded."
},
"supported_endpoints": [
"/v1/chat/completions",
"/v1/responses",
"/v1/messages"
],
"supports_native_streaming": true
}
}

View file

@ -5300,6 +5300,32 @@
"a2a": false,
"generateContent": false
}
},
"bigmodel": {
"display_name": "BigModel (Zhipu AI) (`bigmodel`)",
"url": "https://docs.bigmodel.cn/cn/guide/develop/responses/introduction",
"endpoints": {
"chat_completions": true,
"responses": true,
"text_completion": true,
"embeddings": false,
"image_generations": false,
"image_edits": false,
"audio_transcriptions": false,
"audio_speech": false,
"rerank": false,
"ocr": false,
"search": false,
"moderations": false,
"realtime": false,
"video_generations": false,
"batches": false,
"messages": true,
"interactions": true,
"systemone": false,
"a2a": true,
"generateContent": true
}
}
},
"endpoints": {

View file

@ -4200,5 +4200,33 @@
}
],
"default_model_placeholder": "cursor/claude-4-sonnet"
},
{
"provider": "BIGMODEL",
"provider_display_name": "BigModel (Zhipu AI)",
"litellm_provider": "bigmodel",
"credential_fields": [
{
"key": "api_key",
"label": "BigModel (Zhipu AI) API Key",
"placeholder": null,
"tooltip": null,
"required": true,
"field_type": "password",
"options": null,
"default_value": null
},
{
"key": "api_base",
"label": "API Base",
"placeholder": "https://open.bigmodel.cn",
"tooltip": null,
"required": false,
"field_type": "text",
"options": null,
"default_value": "https://open.bigmodel.cn"
}
],
"default_model_placeholder": "bigmodel/glm-5.3"
}
]

View file

@ -4424,6 +4424,7 @@ class LlmProviders(str, Enum):
JINA_AI = "jina_ai"
XAI = "xai"
ZAI = "zai"
BIGMODEL = "bigmodel"
CUSTOM_OPENAI = "custom_openai"
TEXT_COMPLETION_OPENAI = "text-completion-openai"
COHERE = "cohere"

View file

@ -5641,6 +5641,7 @@ def add_provider_specific_params_to_optional_params(
"openai",
"azure",
"text-completion-openai",
"bigmodel",
*litellm.openai_compatible_providers,
] or JSONProviderRegistry.exists(custom_llm_provider)
if calls_through_openai_sdk:
@ -9135,6 +9136,7 @@ class ProviderConfigManager:
LlmProviders.DATABRICKS: (lambda: litellm.DatabricksConfig(), False),
LlmProviders.XAI: (lambda: litellm.XAIChatConfig(), False),
LlmProviders.ZAI: (lambda: litellm.ZAIChatConfig(), False),
LlmProviders.BIGMODEL: (ProviderConfigManager._get_bigmodel_chat_config, False),
LlmProviders.OPENCODE_GO: (ProviderConfigManager.get_opencode_go_config, True),
LlmProviders.LAMBDA_AI: (lambda: litellm.LambdaAIChatConfig(), False),
LlmProviders.INCEPTION: (lambda: litellm.InceptionChatConfig(), False),
@ -9327,6 +9329,12 @@ class ProviderConfigManager:
return get_bedrock_chat_config(model=model)
@staticmethod
def _get_bigmodel_chat_config() -> BaseConfig:
from litellm.llms.bigmodel.chat.transformation import BigModelChatConfig
return BigModelChatConfig()
@staticmethod
def get_opencode_go_config(model: str) -> BaseConfig:
from litellm.llms.opencode.chat.transformation import go_chat_config
@ -9648,6 +9656,10 @@ class ProviderConfigManager:
model: str,
provider: LlmProviders,
) -> BaseAnthropicMessagesConfig | None:
if provider == LlmProviders.BIGMODEL:
from litellm.llms.bigmodel.messages.transformation import BigModelMessagesConfig
return BigModelMessagesConfig()
if provider == LlmProviders.OPENCODE_GO:
from litellm.llms.opencode.common_utils import go_endpoint
from litellm.llms.opencode.messages.transformation import OpenCodeGoMessagesConfig
@ -9879,6 +9891,10 @@ class ProviderConfigManager:
if provider is None:
return None
if provider == LlmProviders.BIGMODEL:
from litellm.llms.bigmodel.responses.transformation import BigModelResponsesAPIConfig
return BigModelResponsesAPIConfig()
if provider == LlmProviders.OPENCODE_GO:
from litellm.llms.opencode.common_utils import go_endpoint
from litellm.llms.opencode.responses.transformation import OpenCodeGoResponsesConfig

View file

@ -78461,5 +78461,81 @@
"supports_response_schema": true,
"supports_tool_choice": true,
"supports_vision": true
},
"bigmodel/glm-5.3": {
"input_cost_per_token": 1.1428571428571428e-06,
"output_cost_per_token": 4e-06,
"cache_read_input_token_cost": 2.857142857142857e-07,
"litellm_provider": "bigmodel",
"max_input_tokens": 1000000,
"max_output_tokens": 128000,
"mode": "chat",
"source": "https://docs.bigmodel.cn/cn/guide/start/pricing",
"supports_function_calling": true,
"supports_prompt_caching": true,
"supports_reasoning": true,
"supports_tool_choice": true,
"thinking_always_on": true,
"metadata": {
"comment": "Official API list price checked 2026-10-27: input 8, output 28, cached input 2 CNY per million tokens; converted at 7.0 CNY/USD. Cache storage is temporarily free and is not encoded."
},
"supported_endpoints": [
"/v1/chat/completions",
"/v1/responses",
"/v1/messages"
],
"supports_native_streaming": true
},
"bigmodel/glm-5.3-flash": {
"input_cost_per_token": 1.142857142857143e-07,
"output_cost_per_token": 4e-07,
"cache_read_input_token_cost": 3.2857142857142855e-08,
"litellm_provider": "bigmodel",
"max_input_tokens": 1000000,
"max_output_tokens": 128000,
"mode": "chat",
"source": "https://docs.bigmodel.cn/cn/guide/start/pricing",
"supports_function_calling": true,
"supports_prompt_caching": true,
"supports_reasoning": true,
"supports_tool_choice": true,
"thinking_always_on": true,
"supports_vision": true,
"supports_video_input": true,
"metadata": {
"comment": "Official API list price checked 2026-10-27: input 0.8, output 2.8, cached input 0.23 CNY per million tokens; converted at 7.0 CNY/USD. Cache storage is temporarily free and is not encoded."
},
"supported_endpoints": [
"/v1/chat/completions",
"/v1/responses",
"/v1/messages"
],
"supports_native_streaming": true
},
"bigmodel/glm-5.3-flashx": {
"input_cost_per_token": 2.857142857142857e-07,
"output_cost_per_token": 1e-06,
"cache_read_input_token_cost": 8.142857142857142e-08,
"litellm_provider": "bigmodel",
"max_input_tokens": 1000000,
"max_output_tokens": 128000,
"mode": "chat",
"source": "https://docs.bigmodel.cn/cn/guide/start/pricing",
"supports_function_calling": true,
"supports_prompt_caching": true,
"supports_reasoning": true,
"supports_tool_choice": true,
"thinking_always_on": true,
"supports_vision": true,
"supports_video_input": true,
"metadata": {
"comment": "Official API list price checked 2026-10-27: input 2, output 7, cached input 0.57 CNY per million tokens; converted at 7.0 CNY/USD. Cache storage is temporarily free and is not encoded."
},
"supported_endpoints": [
"/v1/chat/completions",
"/v1/responses",
"/v1/messages"
],
"supports_native_streaming": true
}
}

View file

@ -5300,6 +5300,32 @@
"a2a": false,
"generateContent": false
}
},
"bigmodel": {
"display_name": "BigModel (Zhipu AI) (`bigmodel`)",
"url": "https://docs.bigmodel.cn/cn/guide/develop/responses/introduction",
"endpoints": {
"chat_completions": true,
"responses": true,
"text_completion": true,
"embeddings": false,
"image_generations": false,
"image_edits": false,
"audio_transcriptions": false,
"audio_speech": false,
"rerank": false,
"ocr": false,
"search": false,
"moderations": false,
"realtime": false,
"video_generations": false,
"batches": false,
"messages": true,
"interactions": true,
"systemone": false,
"a2a": true,
"generateContent": true
}
}
},
"endpoints": {

View file

File diff suppressed because it is too large Load diff

View file

@ -5784,3 +5784,16 @@ async def test_non_chatgpt_responses_preserves_caller_headers_after_validation(
await client.close()
else:
client.client.close()
@pytest.mark.asyncio
async def test_a_stream_answered_with_one_json_body_is_not_replayed_for_a_provider_that_does_not_opt_in(
httpx_transport: None,
) -> None:
with respx.mock(assert_all_called=True) as router:
router.post("https://api.openai.com/v1/responses").respond(json={"error": {"message": "quota exceeded"}})
stream: Final = await litellm.aresponses(
model="openai/gpt-5.5", input="Hello", api_key="fake-key", stream=True
)
with pytest.raises(litellm.exceptions.MidStreamFallbackError, match="before any terminal event"):
_ = tuple([event async for event in stream])

View file

@ -25,6 +25,26 @@ from litellm.types.proxy.public_endpoints.public_endpoints import ProviderCreate
from litellm.types.utils import LlmProviders
def test_bigmodel_fields_require_private_key_and_share_default_base_with_adapter() -> None:
from litellm.llms.bigmodel.common_utils import BIGMODEL_API_BASE
app: Final = FastAPI()
app.include_router(router)
response: Final = TestClient(app).get("/public/providers/fields")
assert response.status_code == 200
providers: Final = TypeAdapter(list[ProviderCreateInfo]).validate_python(response.json())
provider: Final = next(item for item in providers if item.litellm_provider == "bigmodel")
fields: Final = {field.key: field for field in provider.credential_fields}
assert provider.provider_display_name == "BigModel (Zhipu AI)"
assert fields["api_key"].required is True
assert fields["api_key"].field_type == "password"
assert fields["api_base"].required is False
assert fields["api_base"].default_value == BIGMODEL_API_BASE
assert fields["api_base"].placeholder == BIGMODEL_API_BASE
assert provider.default_model_placeholder is not None
assert provider.default_model_placeholder.startswith("bigmodel/")
def test_fuse_presets_route_serves_the_shared_catalog_without_authentication() -> None:
app: Final = FastAPI()
app.include_router(router)

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View file

@ -9,9 +9,18 @@ import CredentialModal from "./CredentialModal";
vi.mock("../networking", async () => {
const actual = await vi.importActual("../networking");
const { readFileSync } = await import("node:fs");
const { resolve } = await import("node:path");
const metadata = JSON.parse(
readFileSync(
resolve(__dirname, "../../../../../litellm/proxy/public_endpoints/provider_create_fields.json"),
"utf8",
),
) as { provider: string }[];
return {
...actual,
getProviderCreateMetadata: vi.fn().mockResolvedValue([
...metadata.filter((provider) => provider.provider === "BIGMODEL"),
{
provider: "OpenAI",
provider_display_name: Providers.OpenAI,
@ -157,6 +166,39 @@ const chooseProvider = async (user: ReturnType<typeof userEvent.setup>, provider
await user.click(options.find((option) => option.textContent === provider) ?? options[0]);
};
describe("CredentialModal with BigModel metadata", () => {
it("requires a masked API key and shows the default base", async () => {
const user = userEvent.setup();
const onSubmit = renderModal();
await chooseProvider(user, Providers.BIGMODEL);
expect(await screen.findByLabelText("API Base")).toHaveValue("https://open.bigmodel.cn");
expect(screen.getByLabelText("BigModel (Zhipu AI) API Key")).toHaveAttribute("type", "password");
fill("Credential Name:", "bigmodel-key");
await user.click(screen.getByRole("button", { name: "Add Credential" }));
expect(await screen.findByText("Required")).toBeInTheDocument();
expect(onSubmit).not.toHaveBeenCalled();
});
it("saves the key and an explicit base for BigModel", async () => {
const user = userEvent.setup();
const onSubmit = renderModal();
await chooseProvider(user, Providers.BIGMODEL);
await screen.findByLabelText("BigModel (Zhipu AI) API Key");
fill("Credential Name:", "bigmodel-key");
fill("BigModel (Zhipu AI) API Key", "synthetic-bigmodel-key");
fill("API Base", "https://bigmodel-proxy.example");
await user.click(screen.getByRole("button", { name: "Add Credential" }));
expect(onSubmit).toHaveBeenCalledWith(
{
credential_name: "bigmodel-key",
custom_llm_provider: "BIGMODEL",
credential_values: { api_key: "synthetic-bigmodel-key", api_base: "https://bigmodel-proxy.example" },
},
[],
);
});
});
describe("CredentialModal with Anthropic workload identity federation", () => {
it("deletes stored exchange fields when switching to a static token", async () => {
const user = userEvent.setup();

View file

@ -20,6 +20,18 @@ const BUNDLED_MODEL_MAP: Record<string, { litellm_provider?: string }> = JSON.pa
);
describe("provider_info_helpers", () => {
it("resolves BigModel metadata, name, logo and model placeholder", () => {
const metadata = PROVIDER_CREATE_FIELDS.find((provider) => provider.provider === "BIGMODEL");
expect(provider_map.BIGMODEL).toBe(metadata?.litellm_provider);
expect(getProviderLogoAndName("bigmodel")).toEqual({
displayName: "BigModel (Zhipu AI)",
logo: providerLogoMap[Providers.BIGMODEL],
});
expect(getProviderLogoAndName("BIGMODEL")).toEqual(getProviderLogoAndName("bigmodel"));
expect(providerLogoMap[Providers.BIGMODEL]).toContain("bigmodel");
expect(getPlaceholder(Providers.BIGMODEL)).toMatch(/^bigmodel\//);
});
it("maps Microsoft 365 Copilot to its chat model provider and placeholder", () => {
expect(provider_map.MICROSOFT_365_COPILOT).toBe("microsoft_365_copilot");
expect(getPlaceholder(Providers.MICROSOFT_365_COPILOT)).toBe("microsoft_365_copilot/chat");

View file

@ -6,6 +6,7 @@ import anthropicLogo from "../../public/assets/logos/anthropic.svg";
import assemblyaiSmallLogo from "../../public/assets/logos/assemblyai_small.png";
import basetenLogo from "../../public/assets/logos/baseten.svg";
import bedrockLogo from "../../public/assets/logos/bedrock.svg";
import bigmodelLogo from "../../public/assets/logos/bigmodel.png";
import cerebrasLogo from "../../public/assets/logos/cerebras.svg";
import cloudflareLogo from "../../public/assets/logos/cloudflare.svg";
import cohereLogo from "../../public/assets/logos/cohere.svg";
@ -197,6 +198,7 @@ export enum Providers {
xAI = "xAI",
XINFERENCE = "Xinference",
ZAI = "Z.AI (Zhipu AI)",
BIGMODEL = "BigModel (Zhipu AI)",
ZeroGPU = "ZeroGPU",
}
@ -326,6 +328,7 @@ export const provider_map: Record<string, string> = {
xAI: "xai",
XINFERENCE: "xinference",
ZAI: "zai",
BIGMODEL: "bigmodel",
ZeroGPU: "zerogpu",
};
@ -335,6 +338,7 @@ export const resolveLitellmProviderSlug = (provider: string): string =>
provider_map[provider] ?? provider.toLowerCase();
export const providerLogoMap: Partial<Record<Providers, string>> = {
[Providers.BIGMODEL]: bigmodelLogo.src,
[Providers.A2A_Agent]: a2aAgentLogo.src,
[Providers.AI21]: ai21Logo.src,
[Providers.AI21_CHAT]: ai21Logo.src,
@ -499,6 +503,7 @@ const providerPlaceholderMap: Partial<Record<Providers, string>> = {
[Providers.Voyage]: "voyage/",
[Providers.WATSONX]: "watsonx/ibm/granite-3-3-8b-instruct",
[Providers.ZAI]: "zai/glm-4.5",
[Providers.BIGMODEL]: "bigmodel/glm-5.3",
[Providers.ZeroGPU]: "zerogpu/gpt-oss-120b",
};