diff --git a/examples/bigmodel.md b/examples/bigmodel.md index 1f0bb4cd2af..1ae64bcbfc4 100644 --- a/examples/bigmodel.md +++ b/examples/bigmodel.md @@ -56,8 +56,6 @@ GLM-5.3, GLM-5.3-Flash, and GLM-5.3-FlashX use always-on thinking. GLM-5.3 suppo 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 -Codex sends `include` and `parallel_tool_calls` on every Responses request, and LiteLLM does not forward either to BigModel. Set `drop_params: true` on the deployment so LiteLLM drops them instead of answering 400 - 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 diff --git a/litellm/llms/bigmodel/messages/transformation.py b/litellm/llms/bigmodel/messages/transformation.py index 041adf49374..7af6fc6f6c3 100644 --- a/litellm/llms/bigmodel/messages/transformation.py +++ b/litellm/llms/bigmodel/messages/transformation.py @@ -62,7 +62,7 @@ def _normalized_data(data: str) -> str: if body.get("type") == "message_start" and isinstance(message, dict) else _normalize_message(body) ) - return json.dumps(normalized) + return data if normalized == body else json.dumps(normalized, ensure_ascii=False) def _normalize_event(event: ServerSentEvent) -> bytes: diff --git a/litellm/llms/bigmodel/responses/transformation.py b/litellm/llms/bigmodel/responses/transformation.py index 0957f57832c..cb3ec1050ff 100644 --- a/litellm/llms/bigmodel/responses/transformation.py +++ b/litellm/llms/bigmodel/responses/transformation.py @@ -32,6 +32,8 @@ _SUPPORTED_PARAMS: Final = ( "max_output_tokens", "tools", "tool_choice", + "parallel_tool_calls", + "include", "reasoning", "text", "prompt_cache_key", @@ -62,6 +64,7 @@ class _RequestedTools(BaseModel): tools: JsonValue = None tool_choice: JsonValue = None + parallel_tool_calls: JsonValue = None def _requested_tools(logging_obj: Logging) -> _RequestedTools: @@ -97,6 +100,7 @@ def _normalize_response( 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 = ( @@ -108,8 +112,10 @@ def _normalize_response( else usage ) normalized: Final[JsonValue] = { - "parallel_tool_calls": False, **body, + "parallel_tool_calls": ( + requested.parallel_tool_calls is True 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 {}), diff --git a/litellm/main.py b/litellm/main.py index d5427e8062a..626313c5b58 100644 --- a/litellm/main.py +++ b/litellm/main.py @@ -2672,8 +2672,7 @@ def _complete_custom_openai( ) 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 diff --git a/tests/unit/llms/bigmodel/test_bigmodel_provider.py b/tests/unit/llms/bigmodel/test_bigmodel_provider.py index a9ecbe66118..f36604ca9a2 100644 --- a/tests/unit/llms/bigmodel/test_bigmodel_provider.py +++ b/tests/unit/llms/bigmodel/test_bigmodel_provider.py @@ -180,6 +180,23 @@ async def test_chat_uses_native_endpoint_and_maps_token_limit(is_async: bool) -> assert json.loads(route.calls.last.request.content)["thinking"] == {"type": "enabled"} +@respx.mock +def test_chat_does_not_send_the_openai_organization_to_bigmodel(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv("OPENAI_ORGANIZATION", "org-openai-only") + route: Final = respx.post(f"{BASE}/api/paas/v4/chat/completions").respond( + json={ + "id": "chat_test", + "object": "chat.completion", + "created": 1, + "model": MODEL, + "choices": [{"index": 0, "message": {"role": "assistant", "content": "Hello"}, "finish_reason": "stop"}], + "usage": {"prompt_tokens": 1, "completion_tokens": 1, "total_tokens": 2}, + } + ) + _ = litellm.completion(model=f"bigmodel/{MODEL}", messages=[{"role": "user", "content": "Hello"}]) + assert "openai-organization" not in route.calls.last.request.headers + + @pytest.mark.parametrize("is_async", (False, True)) @pytest.mark.asyncio @respx.mock @@ -451,23 +468,37 @@ def test_responses_failure_envelope_raises_its_status_instead_of_a_parse_error( @pytest.mark.parametrize("stream", (False, True)) @respx.mock -def test_responses_without_tools_in_the_answer_echo_the_requested_tools(stream: bool) -> None: +def test_responses_forward_codex_tool_params_and_echo_them_when_the_answer_omits_them(stream: bool) -> None: tools: Final = [{"type": "function", "name": "weather", "parameters": {"type": "object", "properties": {}}}] - completed: Final = {"type": "response.completed", "sequence_number": 0, "response": response_payload()} - respx.post(f"{BASE}/api/v1/responses").respond( + completed: Final = { + "type": "response.completed", + "sequence_number": 0, + "response": {**response_payload(), "parallel_tool_calls": None}, + } + route: Final = respx.post(f"{BASE}/api/v1/responses").respond( **( {"content": f"event: response.completed\ndata: {json.dumps(completed)}\n\n"} if stream - else {"json": response_payload()} + else {"json": {**response_payload(), "parallel_tool_calls": None}} ) ) answer: Final = litellm.responses( - model=f"bigmodel/{MODEL}", input="Hello", tools=tools, tool_choice="required", stream=stream + model=f"bigmodel/{MODEL}", + input="Hello", + tools=tools, + tool_choice="required", + parallel_tool_calls=True, + include=["reasoning.encrypted_content"], + stream=stream, ) response: Final = tuple(answer)[-1].response if stream else answer validated: Final = OpenAIResponse.model_validate(response.model_dump()) + sent: Final = json.loads(route.calls.last.request.content) + assert sent["parallel_tool_calls"] is True + assert sent["include"] == ["reasoning.encrypted_content"] assert validated.tools[0].name == "weather" assert validated.tool_choice == "required" + assert validated.parallel_tool_calls is True @pytest.mark.asyncio @@ -820,6 +851,9 @@ async def test_messages_stream_preserves_native_events() -> None: "web_search_requests": 0, "web_fetch_requests": 0, } + assert tuple(line for line in lines if line.startswith("data: {"))[1:] == tuple( + f"data: {json.dumps(event, ensure_ascii=False)}" for event in events[1:] + ) assert "event: ping" in lines assert "data: [DONE]" in lines assert b"id: bigmodel-event\n" in output[0]