mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-06 07:48:06 +00:00
fix(bigmodel): forward Codex's include and parallel_tool_calls, keep the OpenAI org header off BigModel, and pass unchanged Messages stream events through
This commit is contained in:
parent
cf201798fc
commit
2a247465ff
5 changed files with 48 additions and 11 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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 {}),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue