litellm/tests/integration/streaming/test_faked_responses_stream_items.py
Mateo Wang 2c4ec9c6fa
fix(responses): open faked stream items empty so tool arguments and text arrive once (#46533)
* fix(responses): open faked stream items empty so tool arguments and text arrive once

* fix(responses): open faked stream items in progress with a fresh empty list, and open mid-stream guardrail blocks empty

* fix(responses): open faked refusals, reasoning summaries, custom tool calls and MCP calls empty so each value arrives once

* test(integration): cover the empty-open Responses stream items on every fake, replay and block path

Integration cells for the faked /v1/responses stream, the chat and messages bridges, cache replay, guardrail block events and the MCP gateway call. Each opened item starts empty and each value arrives once, checked with raw httpx, the OpenAI SDK and the Anthropic SDK, plus upstream errors, 5 KB values, a client drop, a dropped upstream connection and a worker SIGKILL
2026-10-15 09:35:12 -07:00

309 lines
15 KiB
Python

import json
import uuid
from typing import Final
import httpx
import openai
import pytest
from integration._support.client import JSON_OBJECT, Gateway, list_value, object_value, string_value
from integration._support.wire import Reply, Request, wire_server
from integration.streaming.faked_responses_case import (
ANSWER,
ARGUMENTS,
NO_CACHE,
RESPONSES_TOOL_PARAMS,
SECOND_ARGUMENTS,
SQL,
SUMMARIES,
answering,
assert_each_value_arrives_once,
assert_one_unstreamed_upstream_call,
completed_output,
custom_tool_call_item,
data_events,
every_item_kind,
faked_model,
function_call_item,
items_by_index,
joined,
message_item,
reasoning_item,
responses_request,
spend_statuses,
stream_events,
values,
)
from pydantic import JsonValue
_TARGET: Final = "/v1/responses"
_LARGE: Final = 5000
def _openai(gateway: Gateway) -> openai.OpenAI:
return openai.OpenAI(base_url=str(gateway.client.base_url) + "/v1", api_key=gateway.key, max_retries=0)
def _async_openai(gateway: Gateway) -> openai.AsyncOpenAI:
return openai.AsyncOpenAI(base_url=str(gateway.client.base_url) + "/v1", api_key=gateway.key, max_retries=0)
def test_raw_stream_opens_every_item_empty_and_each_value_arrives_once(gateway: Gateway) -> None:
with wire_server(answering(every_item_kind)) as wire, gateway.scenario() as scenario:
model: Final = faked_model(scenario, wire)
call_id, events = stream_events(gateway, _TARGET, responses_request(model, cache=NO_CACHE))
assert_one_unstreamed_upstream_call(wire, _TARGET)
added: Final = items_by_index(events, "response.output_item.added")
assert [added[index]["type"] for index in sorted(added)] == [
"reasoning",
"message",
"function_call",
"custom_tool_call",
"message",
], added
assert added[0]["summary"] == [], added[0]
assert (added[1]["content"], added[1]["status"]) == ([], "in_progress"), added[1]
assert (added[2]["arguments"], added[2]["status"]) == ("", "in_progress"), added[2]
assert added[3]["input"] == "", added[3]
assert (added[4]["content"], added[4]["status"]) == ([], "in_progress"), added[4]
summary_parts: Final = [
(event["summary_index"], object_value(event["part"])["text"])
for event in events
if event["type"] == "response.reasoning_summary_part.added"
]
assert summary_parts == [(0, ""), (1, "")], summary_parts
assert [
joined(events, "response.reasoning_summary_text.delta", 0, summary_index=index) for index in (0, 1)
] == [*SUMMARIES]
assert joined(events, "response.output_text.delta", 1) == ANSWER
assert joined(events, "response.function_call_arguments.delta", 2) == ARGUMENTS
assert joined(events, "response.custom_tool_call_input.delta", 3) == SQL
custom_done: Final = [event for event in events if event["type"] == "response.custom_tool_call_input.done"]
assert [(event["output_index"], event["input"]) for event in custom_done] == [(3, SQL)], custom_done
identity: Final = string_value(object_value(added[1])["id"]).removeprefix("msg_")
assert_each_value_arrives_once(events, every_item_kind(identity))
assert spend_statuses(call_id) == ["success"]
def test_openai_sdk_stream_snapshots_hold_each_value_once(gateway: Gateway) -> None:
with wire_server(answering(lambda identity: (message_item(identity), function_call_item(identity)))) as wire:
with gateway.scenario() as scenario:
model: Final = faked_model(scenario, wire)
with _openai(gateway).responses.stream(
model=model,
input=f"weather {uuid.uuid4().hex}",
tools=RESPONSES_TOOL_PARAMS,
extra_body={"cache": NO_CACHE},
) as stream:
events: Final = list(stream)
final: Final = stream.get_final_response()
text_snapshots: Final = [event.snapshot for event in events if event.type == "response.output_text.delta"]
argument_snapshots: Final = [
event.snapshot for event in events if event.type == "response.function_call_arguments.delta"
]
assert text_snapshots and text_snapshots[-1] == ANSWER, text_snapshots[-1:]
assert argument_snapshots and argument_snapshots[-1] == ARGUMENTS, argument_snapshots[-1:]
assert final.output_text == ANSWER
calls: Final = [item for item in final.output if item.type == "function_call"]
assert [call.arguments for call in calls] == [ARGUMENTS], final.output
assert_one_unstreamed_upstream_call(wire, _TARGET)
async def test_async_openai_sdk_stream_snapshots_keep_two_calls_and_a_message_apart(gateway: Gateway) -> None:
def output(identity: str) -> tuple[dict[str, JsonValue], ...]:
return (
function_call_item(identity),
function_call_item(identity, SECOND_ARGUMENTS, "get_forecast"),
message_item(identity),
)
with wire_server(answering(output)) as wire, gateway.scenario() as scenario:
model: Final = faked_model(scenario, wire)
async with _async_openai(gateway).responses.stream(
model=model,
input=f"weather {uuid.uuid4().hex}",
tools=RESPONSES_TOOL_PARAMS,
extra_body={"cache": NO_CACHE},
) as stream:
events: Final = [event async for event in stream]
final: Final = await stream.get_final_response()
last_arguments: Final = {
event.output_index: event.snapshot
for event in events
if event.type == "response.function_call_arguments.delta"
}
assert last_arguments == {0: ARGUMENTS, 1: SECOND_ARGUMENTS}, last_arguments
last_text: Final = [event.snapshot for event in events if event.type == "response.output_text.delta"]
assert last_text and last_text[-1] == ANSWER, last_text[-1:]
assert final.output_text == ANSWER
assert_one_unstreamed_upstream_call(wire, _TARGET)
def test_non_stream_answer_carries_the_upstream_output_unchanged(gateway: Gateway) -> None:
with wire_server(answering(every_item_kind)) as wire, gateway.scenario() as scenario:
model: Final = faked_model(scenario, wire)
response: Final = gateway.request("POST", _TARGET, responses_request(model, cache=NO_CACHE))
assert response.status_code == 200, response.text
output: Final = tuple(map(object_value, list_value(JSON_OBJECT.validate_json(response.content)["output"])))
identity: Final = string_value(output[1]["id"]).removeprefix("msg_")
assert [(item["type"], item["id"], values(item)) for item in output] == [
(item["type"], item["id"], values(item)) for item in every_item_kind(identity)
], output
assert_one_unstreamed_upstream_call(wire, _TARGET)
def test_an_item_type_the_fake_stream_does_not_split_opens_as_its_done_value(gateway: Gateway) -> None:
def output(identity: str) -> tuple[dict[str, JsonValue], ...]:
search: Final[dict[str, JsonValue]] = {
"type": "web_search_call",
"id": f"ws_{identity}",
"status": "completed",
"action": {"type": "search", "query": "paris weather"},
}
return (search,)
with wire_server(answering(output)) as wire, gateway.scenario() as scenario:
model: Final = faked_model(scenario, wire)
_, events = stream_events(gateway, _TARGET, responses_request(model, cache=NO_CACHE))
added: Final = items_by_index(events, "response.output_item.added")
done: Final = items_by_index(events, "response.output_item.done")
assert list(added) == list(done) == [0], (added, done)
assert added[0] == done[0], (added[0], done[0])
action: Final = object_value(done[0]["action"])
assert (action["type"], action["query"]) == ("search", "paris weather"), action
assert [item["id"] for item in completed_output(events)] == [done[0]["id"]]
@pytest.mark.parametrize("status", (400, 401, 404))
def test_upstream_error_reaches_the_caller_and_the_next_stream_is_served(gateway: Gateway, status: int) -> None:
message: Final = f"scripted upstream failure {uuid.uuid4().hex}"
failure: Final = Reply(
status=status,
body=json.dumps({"error": {"message": message, "type": "invalid_request_error", "code": None}}).encode(),
)
healthy: Final = answering(lambda identity: (message_item(identity),))
def respond(request: Request) -> Reply:
return failure if b"fail-now" in request.body else healthy(request)
with wire_server(respond) as wire, gateway.scenario() as scenario:
model: Final = faked_model(scenario, wire)
with gateway.client.stream(
"POST",
_TARGET,
json={**responses_request(model, cache=NO_CACHE), "input": "fail-now", "stream": True},
headers={"Authorization": f"Bearer {gateway.key}"},
) as failed:
failed.read()
assert failed.status_code == status, failed.text
assert message in failed.text, failed.text
_, events = stream_events(gateway, _TARGET, responses_request(model, cache=NO_CACHE))
identity: Final = string_value(items_by_index(events, "response.output_item.done")[0]["id"]).removeprefix(
"msg_"
)
assert_each_value_arrives_once(events, (message_item(identity),))
assert [request.target for request in wire.drain() if request.method == "POST"] == [_TARGET, _TARGET]
def test_empty_values_open_empty_and_close_empty(gateway: Gateway) -> None:
def output(identity: str) -> tuple[dict[str, JsonValue], ...]:
return (
reasoning_item(identity, ()),
message_item(identity, ""),
function_call_item(identity, ""),
custom_tool_call_item(identity, ""),
)
with wire_server(answering(output)) as wire, gateway.scenario() as scenario:
model: Final = faked_model(scenario, wire)
_, events = stream_events(gateway, _TARGET, responses_request(model, cache=NO_CACHE))
identity: Final = string_value(items_by_index(events, "response.output_item.done")[1]["id"]).removeprefix(
"msg_"
)
assert_each_value_arrives_once(events, output(identity))
assert joined(events, "response.function_call_arguments.delta", 2) == ""
assert_one_unstreamed_upstream_call(wire, _TARGET)
def test_five_kilobyte_arguments_and_text_arrive_once(gateway: Gateway) -> None:
arguments: Final = json.dumps({"notes": ("détail " * _LARGE)[: _LARGE - 13]}, ensure_ascii=False)
text: Final = ("Long answer, part " * _LARGE)[:_LARGE]
def output(identity: str) -> tuple[dict[str, JsonValue], ...]:
return (message_item(identity, text), function_call_item(identity, arguments))
with wire_server(answering(output)) as wire, gateway.scenario() as scenario:
model: Final = faked_model(scenario, wire)
call_id, events = stream_events(gateway, _TARGET, responses_request(model, cache=NO_CACHE))
assert len(arguments) == _LARGE and len(text) == _LARGE
assert joined(events, "response.output_text.delta", 0) == text
assert joined(events, "response.function_call_arguments.delta", 1) == arguments
identity: Final = string_value(items_by_index(events, "response.output_item.done")[0]["id"]).removeprefix(
"msg_"
)
assert_each_value_arrives_once(events, output(identity))
assert_one_unstreamed_upstream_call(wire, _TARGET)
assert spend_statuses(call_id) == ["success"]
def test_arguments_sent_as_an_object_never_break_the_stream_or_the_next_request(gateway: Gateway) -> None:
malformed: Final[dict[str, JsonValue]] = {"city": "Paris"}
def output(identity: str) -> tuple[dict[str, JsonValue], ...]:
return (function_call_item(identity, malformed),)
with wire_server(answering(output)) as wire, gateway.scenario() as scenario:
model: Final = faked_model(scenario, wire)
with gateway.client.stream(
"POST",
_TARGET,
json={**responses_request(model, cache=NO_CACHE), "stream": True},
headers={"Authorization": f"Bearer {gateway.key}"},
) as response:
body: Final = response.read().decode()
assert response.status_code in (200, 400, 500), body
if response.status_code == 200:
events: Final = data_events(iter(body.splitlines()))
assert items_by_index(events, "response.output_item.done")[0]["arguments"] == malformed, body
healthy: Final = gateway.request("POST", _TARGET, responses_request(model, cache=NO_CACHE))
assert healthy.status_code == 200, healthy.text
assert healthy.json()["output"][0]["arguments"] == malformed, healthy.text
assert len([request for request in wire.drain() if request.method == "POST"]) == 2
def test_identical_uncached_streams_each_reach_the_upstream_and_log_once(gateway: Gateway) -> None:
with wire_server(answering(lambda identity: (message_item(identity),))) as wire, gateway.scenario() as scenario:
model: Final = faked_model(scenario, wire)
body: Final = responses_request(model, cache=NO_CACHE)
first_call, first = stream_events(gateway, _TARGET, body)
second_call, second = stream_events(gateway, _TARGET, body)
assert first_call != second_call
first_id: Final = string_value(items_by_index(first, "response.output_item.done")[0]["id"])
second_id: Final = string_value(items_by_index(second, "response.output_item.done")[0]["id"])
assert first_id != second_id
assert_each_value_arrives_once(first, (message_item(first_id.removeprefix("msg_")),))
assert_each_value_arrives_once(second, (message_item(second_id.removeprefix("msg_")),))
assert [request.target for request in wire.drain() if request.method == "POST"] == [_TARGET, _TARGET]
assert spend_statuses(first_call) == ["success"]
assert spend_statuses(second_call) == ["success"]
def test_client_that_drops_the_stream_after_the_first_event_leaves_the_next_stream_whole(gateway: Gateway) -> None:
with wire_server(answering(every_item_kind)) as wire, gateway.scenario() as scenario:
model: Final = faked_model(scenario, wire)
with httpx.Client(base_url=str(gateway.client.base_url), timeout=60, trust_env=False) as client:
with client.stream(
"POST",
_TARGET,
json={**responses_request(model, cache=NO_CACHE), "stream": True},
headers={"Authorization": f"Bearer {gateway.key}"},
) as dropped:
assert dropped.status_code == 200
first: Final = next(line for line in dropped.iter_lines() if line.startswith("data: {"))
assert json.loads(first.removeprefix("data: "))["type"] == "response.created", first
_, events = stream_events(gateway, _TARGET, responses_request(model, cache=NO_CACHE))
identity: Final = string_value(items_by_index(events, "response.output_item.done")[1]["id"]).removeprefix(
"msg_"
)
assert_each_value_arrives_once(events, every_item_kind(identity))
assert [request.target for request in wire.drain() if request.method == "POST"] == [_TARGET, _TARGET]
assert gateway.client.get("/health/liveliness").status_code == 200