fix(responses): number bridged chat tool calls 0, 1 and pin the remaining bridge shapes (#46109)

* test(responses): pin bridged commentary plus final answer as one chat choice

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* test(responses): pin reasoning, two-message and two-tool-call bridge shapes

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix(responses): number bridged chat tool calls 0, 1 instead of all 0

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

---------

Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
devin-ai-integration[bot] 2026-10-12 17:35:30 -07:00 • committed by GitHub
parent aa52927216
commit b6df3d7fe5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 833 additions and 464 deletions

View file

@ -2402,7 +2402,7 @@ class LiteLLMCompletionResponsesConfig:
),
"function": function_dict,
"type": "function",
"index": 0,
"index": index,
}
if provider_specific_fields:

View file

@ -2,6 +2,7 @@ import json
import uuid
from typing import Final
import httpx
import pytest
from integration._support.client import Gateway, list_value, object_value
from integration._support.wire import Reply, Request, wire_server
@ -589,13 +590,14 @@ _WEATHER_TOOL: Final[dict[str, JsonValue]] = {
}
def _message_item(item_id: str, text: str) -> dict[str, JsonValue]:
def _message_item(item_id: str, text: str, phase: str | None = None) -> dict[str, JsonValue]:
return {
"type": "message",
"id": item_id,
"status": "completed",
"role": "assistant",
"content": [{"type": "output_text", "text": text, "annotations": []}],
**({} if phase is None else {"phase": phase}),
}
@ -675,6 +677,219 @@ def test_chat_over_responses_deployment_folds_two_messages_and_function_call_int
assert [(request.method, request.target) for request in wire.drain()] == [("POST", "/responses")]
_COMMENTARY_THEN_FINAL_ANSWER: Final = (
_message_item("msg_commentary", "Let me answer that.", phase="commentary"),
_message_item("msg_final", '{"city": "Paris", "summary": "Sunny, 21C"}', phase="final_answer"),
)
def test_chat_over_responses_deployment_folds_commentary_and_final_answer_into_one_choice(
gateway: Gateway,
) -> None:
identity: Final = "responses-bridge-commentary-final-" + uuid.uuid4().hex
def respond(request: Request) -> Reply:
if request.method == "GET" and request.target == "/v1/models":
return Reply(body=b'{"object":"list","data":[]}')
assert request.method == "POST" and request.target == "/responses", request.target
return Reply(
body=json.dumps(
{
"id": "resp_commentary_final",
"object": "response",
"created_at": 1789788253,
"status": "completed",
"model": "gpt-6-sol",
"output": list(_COMMENTARY_THEN_FINAL_ANSWER),
"usage": {"input_tokens": 10, "output_tokens": 5, "total_tokens": 15},
}
).encode()
)
with wire_server(respond) as wire, gateway.scenario() as scenario:
model: Final = scenario.model(
model="openai/responses/gpt-6-sol", api_base=wire.url, api_key="synthetic-openai-key"
)
response: Final = gateway.request(
"POST",
"/v1/chat/completions",
{
"model": model,
"messages": [{"role": "user", "content": f"Weather in Paris as JSON. {identity}"}],
"tools": [_WEATHER_TOOL],
"cache": {"no-cache": True},
},
)
assert response.status_code == 200, response.text
body: Final = response.json()
assert body["choices"] == [
{
"finish_reason": "stop",
"index": 0,
"message": {
"role": "assistant",
"content": 'Let me answer that.{"city": "Paris", "summary": "Sunny, 21C"}',
},
}
], response.text
assert [(request.method, request.target) for request in wire.drain()] == [("POST", "/responses")]
_REASONING_ITEM: Final[dict[str, JsonValue]] = {
"type": "reasoning",
"id": "rs_fold",
"summary": [{"type": "summary_text", "text": "Checking the forecast."}],
}
_TIME_TOOL: Final[dict[str, JsonValue]] = {
"type": "function",
"function": {
"name": "get_time",
"description": "Get the local time for a city.",
"parameters": {
"type": "object",
"properties": {"city": {"type": "string"}},
"required": ["city"],
},
},
}
_TIME_CALL: Final[dict[str, JsonValue]] = {
"type": "function_call",
"id": "fc_2",
"call_id": "call_2",
"name": "get_time",
"arguments": '{"city":"Paris","tz":"CET"}',
"status": "completed",
}
_REASONING_THEN_TWO_MESSAGES_THEN_CALL: Final = (
_REASONING_ITEM,
_message_item("msg_first", "First message."),
_message_item("msg_second", "Second message."),
_WEATHER_CALL,
)
_MESSAGE_THEN_TWO_CALLS: Final = (
_message_item("msg_only", "Checking both."),
_WEATHER_CALL,
_TIME_CALL,
)
_TWO_CALLS_CHAT_SHAPE: Final = [
{
"id": "fc_1",
"type": "function",
"function": {"name": "get_weather", "arguments": '{"city":"Paris"}'},
"index": 0,
},
{
"id": "fc_2",
"type": "function",
"function": {"name": "get_time", "arguments": '{"city":"Paris","tz":"CET"}'},
"index": 1,
},
]
def _responses_body(response_id: str, output: tuple[dict[str, JsonValue], ...]) -> bytes:
return json.dumps(
{
"id": response_id,
"object": "response",
"created_at": 1789788253,
"status": "completed",
"model": "gpt-6-sol",
"output": list(output),
"usage": {"input_tokens": 10, "output_tokens": 5, "total_tokens": 15},
}
).encode()
def test_chat_over_responses_deployment_folds_reasoning_two_messages_and_function_call_into_one_choice(
gateway: Gateway,
) -> None:
identity: Final = "responses-bridge-reasoning-two-messages-" + uuid.uuid4().hex
def respond(request: Request) -> Reply:
if request.method == "GET" and request.target == "/v1/models":
return Reply(body=b'{"object":"list","data":[]}')
assert request.method == "POST" and request.target == "/responses", request.target
return Reply(body=_responses_body("resp_reasoning_two_messages", _REASONING_THEN_TWO_MESSAGES_THEN_CALL))
with wire_server(respond) as wire, gateway.scenario() as scenario:
model: Final = scenario.model(
model="openai/responses/gpt-6-sol", api_base=wire.url, api_key="synthetic-openai-key"
)
response: Final = gateway.request(
"POST",
"/v1/chat/completions",
{
"model": model,
"messages": [{"role": "user", "content": f"What is the weather in Paris? {identity}"}],
"tools": [_WEATHER_TOOL],
"cache": {"no-cache": True},
},
)
assert response.status_code == 200, response.text
body: Final = response.json()
assert body["choices"] == [
{
"finish_reason": "tool_calls",
"index": 0,
"message": {
"role": "assistant",
"content": "First message.Second message.",
"reasoning_content": "Checking the forecast.",
"reasoning_items": [_REASONING_ITEM],
"tool_calls": _TWO_CALLS_CHAT_SHAPE[:1],
},
}
], response.text
assert [(request.method, request.target) for request in wire.drain()] == [("POST", "/responses")]
def test_chat_over_responses_deployment_folds_message_and_two_function_calls_into_one_choice(
gateway: Gateway,
) -> None:
identity: Final = "responses-bridge-two-calls-" + uuid.uuid4().hex
def respond(request: Request) -> Reply:
if request.method == "GET" and request.target == "/v1/models":
return Reply(body=b'{"object":"list","data":[]}')
assert request.method == "POST" and request.target == "/responses", request.target
return Reply(body=_responses_body("resp_two_calls", _MESSAGE_THEN_TWO_CALLS))
with wire_server(respond) as wire, gateway.scenario() as scenario:
model: Final = scenario.model(
model="openai/responses/gpt-6-sol", api_base=wire.url, api_key="synthetic-openai-key"
)
response: Final = gateway.request(
"POST",
"/v1/chat/completions",
{
"model": model,
"messages": [{"role": "user", "content": f"Weather and time in Paris? {identity}"}],
"tools": [_WEATHER_TOOL, _TIME_TOOL],
"cache": {"no-cache": True},
},
)
assert response.status_code == 200, response.text
body: Final = response.json()
assert body["choices"] == [
{
"finish_reason": "tool_calls",
"index": 0,
"message": {
"role": "assistant",
"content": "Checking both.",
"tool_calls": _TWO_CALLS_CHAT_SHAPE,
},
}
], response.text
assert [(request.method, request.target) for request in wire.drain()] == [("POST", "/responses")]
def _responses_frame(event: str, payload: dict[str, JsonValue]) -> bytes:
return f"event: {event}\ndata: {json.dumps(payload, separators=(',', ':'))}\n\n".encode()
@ -743,6 +958,34 @@ def _function_call_frames(output_index: int, item: dict[str, JsonValue]) -> tupl
)
def _reasoning_item_frames(output_index: int, item: dict[str, JsonValue]) -> tuple[bytes, ...]:
text: Final = object_value(list_value(item["summary"])[0])["text"]
return (
_responses_frame(
"response.output_item.added",
{
"type": "response.output_item.added",
"output_index": output_index,
"item": {**item, "summary": []},
},
),
_responses_frame(
"response.reasoning_summary_text.delta",
{
"type": "response.reasoning_summary_text.delta",
"item_id": item["id"],
"output_index": output_index,
"summary_index": 0,
"delta": text,
},
),
_responses_frame(
"response.output_item.done",
{"type": "response.output_item.done", "output_index": output_index, "item": item},
),
)
def _sse_payloads(lines: list[str]) -> list[dict[str, JsonValue]]:
return [json.loads(line[6:]) for line in lines if line.startswith("data: ") and line != "data: [DONE]"]
@ -826,3 +1069,231 @@ def test_chat_stream_over_responses_deployment_folds_two_messages_and_function_c
), chunks
assert [choice["finish_reason"] for choice in choices if choice.get("finish_reason")] == ["tool_calls"], chunks
assert [(request.method, request.target) for request in wire.drain()] == [("POST", "/responses")]
def test_chat_stream_over_responses_deployment_folds_commentary_and_final_answer_into_one_choice(
gateway: Gateway,
) -> None:
identity: Final = "responses-bridge-commentary-final-stream-" + uuid.uuid4().hex
response_payload: Final[dict[str, JsonValue]] = {
"id": "resp_commentary_final_stream",
"object": "response",
"created_at": 1789788253,
"model": "gpt-6-sol",
"status": "in_progress",
"output": [],
}
def respond(request: Request) -> Reply:
if request.method == "GET" and request.target == "/v1/models":
return Reply(body=b'{"object":"list","data":[]}')
assert request.method == "POST" and request.target == "/responses", request.target
assert json.loads(request.body)["stream"] is True, request.body
return Reply(
content_type="text/event-stream",
chunks=(
_responses_frame("response.created", {"type": "response.created", "response": response_payload}),
*_message_item_frames(0, _COMMENTARY_THEN_FINAL_ANSWER[0]),
*_message_item_frames(1, _COMMENTARY_THEN_FINAL_ANSWER[1]),
_responses_frame(
"response.completed",
{
"type": "response.completed",
"response": {
**response_payload,
"status": "completed",
"output": list(_COMMENTARY_THEN_FINAL_ANSWER),
"usage": {"input_tokens": 10, "output_tokens": 5, "total_tokens": 15},
},
},
),
),
)
with wire_server(respond) as wire, gateway.scenario() as scenario:
model: Final = scenario.model(
model="openai/responses/gpt-6-sol", api_base=wire.url, api_key="synthetic-openai-key"
)
response: Final = gateway.request(
"POST",
"/v1/chat/completions",
{
"model": model,
"messages": [{"role": "user", "content": f"Weather in Paris as JSON. {identity}"}],
"tools": [_WEATHER_TOOL],
"stream": True,
"cache": {"no-cache": True},
},
)
assert response.status_code == 200, response.text
chunks: Final = _sse_payloads(list(response.iter_lines()))
choices: Final = [object_value(choice) for chunk in chunks for choice in list_value(chunk["choices"])]
assert [choice["index"] for choice in choices] == [0] * len(choices), chunks
deltas: Final = [object_value(choice["delta"]) for choice in choices]
assert (
"".join(str(delta["content"]) for delta in deltas if delta.get("content"))
== 'Let me answer that.{"city": "Paris", "summary": "Sunny, 21C"}'
), chunks
assert not [delta for delta in deltas if delta.get("tool_calls")], chunks
assert [choice["finish_reason"] for choice in choices if choice.get("finish_reason")] == ["stop"], chunks
assert [(request.method, request.target) for request in wire.drain()] == [("POST", "/responses")]
def _stream_reply(response_id: str, output: tuple[dict[str, JsonValue], ...], frames: tuple[bytes, ...]) -> Reply:
response_payload: Final[dict[str, JsonValue]] = {
"id": response_id,
"object": "response",
"created_at": 1789788253,
"model": "gpt-6-sol",
"status": "in_progress",
"output": [],
}
return Reply(
content_type="text/event-stream",
chunks=(
_responses_frame("response.created", {"type": "response.created", "response": response_payload}),
*frames,
_responses_frame(
"response.completed",
{
"type": "response.completed",
"response": {
**response_payload,
"status": "completed",
"output": list(output),
"usage": {"input_tokens": 10, "output_tokens": 5, "total_tokens": 15},
},
},
),
),
)
def _stream_choices(response: httpx.Response) -> list[dict[str, JsonValue]]:
chunks: Final = _sse_payloads(list(response.iter_lines()))
return [object_value(choice) for chunk in chunks for choice in list_value(chunk["choices"])]
def _stream_tool_calls(deltas: list[dict[str, JsonValue]]) -> list[dict[str, JsonValue]]:
return [
object_value(call) for delta in deltas if delta.get("tool_calls") for call in list_value(delta["tool_calls"])
]
def test_chat_stream_over_responses_deployment_folds_reasoning_two_messages_and_function_call_into_one_choice(
gateway: Gateway,
) -> None:
identity: Final = "responses-bridge-reasoning-two-messages-stream-" + uuid.uuid4().hex
def respond(request: Request) -> Reply:
if request.method == "GET" and request.target == "/v1/models":
return Reply(body=b'{"object":"list","data":[]}')
assert request.method == "POST" and request.target == "/responses", request.target
assert json.loads(request.body)["stream"] is True, request.body
return _stream_reply(
"resp_reasoning_two_messages_stream",
_REASONING_THEN_TWO_MESSAGES_THEN_CALL,
(
*_reasoning_item_frames(0, _REASONING_THEN_TWO_MESSAGES_THEN_CALL[0]),
*_message_item_frames(1, _REASONING_THEN_TWO_MESSAGES_THEN_CALL[1]),
*_message_item_frames(2, _REASONING_THEN_TWO_MESSAGES_THEN_CALL[2]),
*_function_call_frames(3, _REASONING_THEN_TWO_MESSAGES_THEN_CALL[3]),
),
)
with wire_server(respond) as wire, gateway.scenario() as scenario:
model: Final = scenario.model(
model="openai/responses/gpt-6-sol", api_base=wire.url, api_key="synthetic-openai-key"
)
response: Final = gateway.request(
"POST",
"/v1/chat/completions",
{
"model": model,
"messages": [{"role": "user", "content": f"What is the weather in Paris? {identity}"}],
"tools": [_WEATHER_TOOL],
"stream": True,
"cache": {"no-cache": True},
},
)
assert response.status_code == 200, response.text
choices: Final = _stream_choices(response)
assert [choice["index"] for choice in choices] == [0] * len(choices), choices
deltas: Final = [object_value(choice["delta"]) for choice in choices]
assert (
"".join(str(delta["reasoning_content"]) for delta in deltas if delta.get("reasoning_content"))
== "Checking the forecast."
), choices
assert (
"".join(str(delta["content"]) for delta in deltas if delta.get("content"))
== "First message.Second message."
), choices
tool_calls: Final = _stream_tool_calls(deltas)
assert [
object_value(call["function"])["name"] for call in tool_calls if object_value(call["function"]).get("name")
] == ["get_weather"], choices
assert (
"".join(str(object_value(call["function"]).get("arguments") or "") for call in tool_calls)
== '{"city":"Paris"}'
), choices
assert [choice["finish_reason"] for choice in choices if choice.get("finish_reason")] == ["tool_calls"], choices
assert [(request.method, request.target) for request in wire.drain()] == [("POST", "/responses")]
def test_chat_stream_over_responses_deployment_folds_message_and_two_function_calls_into_one_choice(
gateway: Gateway,
) -> None:
identity: Final = "responses-bridge-two-calls-stream-" + uuid.uuid4().hex
def respond(request: Request) -> Reply:
if request.method == "GET" and request.target == "/v1/models":
return Reply(body=b'{"object":"list","data":[]}')
assert request.method == "POST" and request.target == "/responses", request.target
assert json.loads(request.body)["stream"] is True, request.body
return _stream_reply(
"resp_two_calls_stream",
_MESSAGE_THEN_TWO_CALLS,
(
*_message_item_frames(0, _MESSAGE_THEN_TWO_CALLS[0]),
*_function_call_frames(1, _MESSAGE_THEN_TWO_CALLS[1]),
*_function_call_frames(2, _MESSAGE_THEN_TWO_CALLS[2]),
),
)
with wire_server(respond) as wire, gateway.scenario() as scenario:
model: Final = scenario.model(
model="openai/responses/gpt-6-sol", api_base=wire.url, api_key="synthetic-openai-key"
)
response: Final = gateway.request(
"POST",
"/v1/chat/completions",
{
"model": model,
"messages": [{"role": "user", "content": f"Weather and time in Paris? {identity}"}],
"tools": [_WEATHER_TOOL, _TIME_TOOL],
"stream": True,
"cache": {"no-cache": True},
},
)
assert response.status_code == 200, response.text
choices: Final = _stream_choices(response)
assert [choice["index"] for choice in choices] == [0] * len(choices), choices
deltas: Final = [object_value(choice["delta"]) for choice in choices]
assert "".join(str(delta["content"]) for delta in deltas if delta.get("content")) == "Checking both.", choices
tool_calls: Final = _stream_tool_calls(deltas)
assert [
(call["index"], object_value(call["function"])["name"])
for call in tool_calls
if object_value(call["function"]).get("name")
] == [(0, "get_weather"), (1, "get_time")], choices
arguments_by_index: Final = {
index: "".join(
str(object_value(call["function"]).get("arguments") or "")
for call in tool_calls
if call["index"] == index
)
for index in (0, 1)
}
assert arguments_by_index == {0: '{"city":"Paris"}', 1: '{"city":"Paris","tz":"CET"}'}, choices
assert [choice["finish_reason"] for choice in choices if choice.get("finish_reason")] == ["tool_calls"], choices
assert [(request.method, request.target) for request in wire.drain()] == [("POST", "/responses")]

View file

@ -5197,3 +5197,73 @@ def test_convert_response_output_folds_two_messages_and_function_call_into_one_c
"index": 0,
}
]
def test_convert_response_output_folds_commentary_and_final_answer_into_one_choice() -> None:
commentary: Final = ResponseOutputMessage.model_validate(
{
"id": "msg_commentary",
"type": "message",
"role": "assistant",
"status": "completed",
"phase": "commentary",
"content": [{"type": "output_text", "text": "Let me answer that.", "annotations": []}],
}
)
final_answer: Final = ResponseOutputMessage.model_validate(
{
"id": "msg_final",
"type": "message",
"role": "assistant",
"status": "completed",
"phase": "final_answer",
"content": [{"type": "output_text", "text": '{"city": "Paris"}', "annotations": []}],
}
)
choices: Final = LiteLLMResponsesTransformationHandler._convert_response_output_to_choices( # pyright: ignore[reportPrivateUsage] # exercises the converter directly
(commentary, final_answer)
)
assert len(choices) == 1
assert choices[0].index == 0
assert choices[0].finish_reason == "stop"
assert choices[0].message.content == 'Let me answer that.{"city": "Paris"}'
assert choices[0].message.tool_calls is None
def test_convert_response_output_numbers_two_function_calls_zero_and_one() -> None:
message: Final = ResponseOutputMessage.model_validate(
{
"id": "msg_only",
"type": "message",
"role": "assistant",
"status": "completed",
"content": [{"type": "output_text", "text": "Checking both.", "annotations": []}],
}
)
weather: Final = ResponseFunctionToolCall(
id="fc_1",
type="function_call",
status="completed",
arguments='{"city":"Paris"}',
call_id="call_1",
name="get_weather",
)
time: Final = ResponseFunctionToolCall(
id="fc_2",
type="function_call",
status="completed",
arguments='{"city":"Paris"}',
call_id="call_2",
name="get_time",
)
choices: Final = LiteLLMResponsesTransformationHandler._convert_response_output_to_choices( # pyright: ignore[reportPrivateUsage] # the fold is the unit under test
(message, weather, time)
)
assert len(choices) == 1
tool_calls: Final = choices[0].message.tool_calls
assert tool_calls is not None
assert [(call.index, call.function.name) for call in tool_calls] == [(0, "get_weather"), (1, "get_time")]