mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-11 08:38:24 +00:00
* fix(responses): replay the original Responses item order, phase, and boundaries on the next chat turn * fix(responses): keep tool-call-only stream content empty and drop unreachable replay guards * fix(responses): reject an assistant phase OpenAI does not define unless drop_params is set * fix(responses): keep other providers' reasoning and stream output unchanged and fall back on records that do not fit * fix(responses): label a structured output turn with its returned message's phase and keep lone surrogates from failing the checksum * fix(responses): leave a turn without a phase when its content message cannot be read * test(responses): add black box tests for chat turn history replay * test(responses): expect the replay record on bridged chat replies * test(responses): expect the replay record in bridged chat translation cases * fix(streaming): keep the served service tier on the finish chunk the stream adds * test(responses): read the expected chat message without JSON validation so the replay matcher stays --------- Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
214 lines
7.5 KiB
Python
214 lines
7.5 KiB
Python
import json
|
|
import uuid
|
|
from collections.abc import Callable, Sequence
|
|
from itertools import chain
|
|
from typing import Final
|
|
|
|
from integration._support.client import list_value, object_value
|
|
from integration._support.wire import Reply, Request, Wire
|
|
from pydantic import JsonValue
|
|
|
|
MODEL: Final = "gpt-6.1-sol"
|
|
PLAN: Final = "I am starting by choosing which city to check."
|
|
PICK: Final = "My pick is Tokyo, so I will check its weather."
|
|
PATCH_NOTE: Final = "I will create hello.txt containing hi."
|
|
ANSWER: Final = "It is raining in Tokyo."
|
|
|
|
WEATHER_TOOL: Final[dict[str, JsonValue]] = {
|
|
"type": "function",
|
|
"function": {
|
|
"name": "get_weather",
|
|
"description": "Get the weather for a city.",
|
|
"parameters": {"type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"]},
|
|
},
|
|
}
|
|
PATCH_TOOL: Final[dict[str, JsonValue]] = {
|
|
"type": "custom",
|
|
"custom": {"name": "apply_patch", "description": "Apply a V4A patch to the workspace"},
|
|
}
|
|
|
|
|
|
def _message(item_id: str, text: str, phase: str) -> dict[str, JsonValue]:
|
|
return {
|
|
"type": "message",
|
|
"id": item_id,
|
|
"status": "completed",
|
|
"role": "assistant",
|
|
"phase": phase,
|
|
"content": [{"type": "output_text", "text": text, "annotations": []}],
|
|
}
|
|
|
|
|
|
def _reasoning(item_id: str, summary: str) -> dict[str, JsonValue]:
|
|
return {
|
|
"type": "reasoning",
|
|
"id": item_id,
|
|
"summary": [{"type": "summary_text", "text": summary}],
|
|
"encrypted_content": f"enc_{item_id}",
|
|
}
|
|
|
|
|
|
_WEATHER_CALL: Final[dict[str, JsonValue]] = {
|
|
"type": "function_call",
|
|
"id": "fc_weather",
|
|
"call_id": "call_weather",
|
|
"name": "get_weather",
|
|
"arguments": '{"city":"Tokyo"}',
|
|
"status": "completed",
|
|
}
|
|
WEATHER_TURN: Final[tuple[dict[str, JsonValue], ...]] = (
|
|
_message("msg_plan", PLAN, "commentary"),
|
|
_reasoning("rs_pick", "Tokyo is often rainy this time of year."),
|
|
_message("msg_pick", PICK, "commentary"),
|
|
_WEATHER_CALL,
|
|
)
|
|
TWO_THOUGHTS_TURN: Final[tuple[dict[str, JsonValue], ...]] = (
|
|
_reasoning("rs_scan", "Paris and Tokyo are the two candidates."),
|
|
_reasoning("rs_pick", "Tokyo is often rainy this time of year."),
|
|
_message("msg_pick", PICK, "commentary"),
|
|
_WEATHER_CALL,
|
|
)
|
|
CALL_ONLY_TURN: Final[tuple[dict[str, JsonValue], ...]] = (_WEATHER_CALL,)
|
|
PATCH_TURN: Final[tuple[dict[str, JsonValue], ...]] = (
|
|
_message("msg_patch", PATCH_NOTE, "commentary"),
|
|
{
|
|
"type": "custom_tool_call",
|
|
"id": "ctc_patch",
|
|
"call_id": "call_patch",
|
|
"name": "apply_patch",
|
|
"input": "*** Begin Patch\n*** Add File: hello.txt\n+hi\n*** End Patch",
|
|
"status": "completed",
|
|
},
|
|
)
|
|
WEATHER_REPLAY: Final = (
|
|
("message", "assistant", "commentary", PLAN),
|
|
("reasoning", "rs_pick"),
|
|
("message", "assistant", "commentary", PICK),
|
|
("function_call", "call_weather", "get_weather"),
|
|
("function_call_output", "call_weather"),
|
|
)
|
|
TWO_THOUGHTS_REPLAY: Final = (
|
|
("reasoning", "rs_scan"),
|
|
("reasoning", "rs_pick"),
|
|
("message", "assistant", "commentary", PICK),
|
|
("function_call", "call_weather", "get_weather"),
|
|
("function_call_output", "call_weather"),
|
|
)
|
|
|
|
|
|
def _response(identity: str, output: Sequence[dict[str, JsonValue]]) -> dict[str, JsonValue]:
|
|
return {
|
|
"id": f"resp_{identity}",
|
|
"object": "response",
|
|
"created_at": 1789788253,
|
|
"status": "completed",
|
|
"model": MODEL,
|
|
"output": list(output),
|
|
"usage": {"input_tokens": 10, "output_tokens": 5, "total_tokens": 15},
|
|
}
|
|
|
|
|
|
def _event(payload: dict[str, JsonValue]) -> bytes:
|
|
return f"event: {payload['type']}\ndata: {json.dumps(payload)}\n\n".encode()
|
|
|
|
|
|
def _opened(item: dict[str, JsonValue]) -> dict[str, JsonValue]:
|
|
match item["type"]:
|
|
case "message":
|
|
return {**item, "status": "in_progress", "content": []}
|
|
case "function_call":
|
|
return {**item, "status": "in_progress", "arguments": ""}
|
|
case _:
|
|
return item
|
|
|
|
|
|
def _deltas(index: int, item: dict[str, JsonValue]) -> tuple[dict[str, JsonValue], ...]:
|
|
match item["type"]:
|
|
case "message":
|
|
text: Final = object_value(list_value(item["content"])[0])["text"]
|
|
return (
|
|
{
|
|
"type": "response.output_text.delta",
|
|
"item_id": item["id"],
|
|
"output_index": index,
|
|
"content_index": 0,
|
|
"delta": text,
|
|
},
|
|
)
|
|
case "function_call":
|
|
return (
|
|
{
|
|
"type": "response.function_call_arguments.delta",
|
|
"item_id": item["id"],
|
|
"output_index": index,
|
|
"delta": item["arguments"],
|
|
},
|
|
)
|
|
case _:
|
|
return ()
|
|
|
|
|
|
def _item_events(index: int, item: dict[str, JsonValue]) -> tuple[bytes, ...]:
|
|
return (
|
|
_event({"type": "response.output_item.added", "output_index": index, "item": _opened(item)}),
|
|
*(_event(delta) for delta in _deltas(index, item)),
|
|
_event({"type": "response.output_item.done", "output_index": index, "item": item}),
|
|
)
|
|
|
|
|
|
def _stream(identity: str, output: Sequence[dict[str, JsonValue]]) -> tuple[bytes, ...]:
|
|
response: Final = _response(identity, output)
|
|
return (
|
|
_event({"type": "response.created", "response": {**response, "status": "in_progress", "output": []}}),
|
|
*chain.from_iterable(_item_events(index, item) for index, item in enumerate(output)),
|
|
_event({"type": "response.completed", "response": response}),
|
|
)
|
|
|
|
|
|
def _answered(body: dict[str, JsonValue]) -> bool:
|
|
return any(
|
|
object_value(item).get("type") in {"function_call_output", "custom_tool_call_output"}
|
|
for item in list_value(body["input"])
|
|
)
|
|
|
|
|
|
def responses_upstream(turn_one: Sequence[dict[str, JsonValue]]) -> Callable[[Request], Reply]:
|
|
def respond(request: Request) -> Reply:
|
|
if request.method == "GET":
|
|
return Reply(body=b'{"object":"list","data":[]}')
|
|
assert request.target == "/responses", request.target
|
|
body: Final = object_value(json.loads(request.body))
|
|
identity: Final = uuid.uuid4().hex
|
|
output: Final = (_message("msg_answer", ANSWER, "final_answer"),) if _answered(body) else turn_one
|
|
if body.get("stream"):
|
|
return Reply(content_type="text/event-stream", chunks=_stream(identity, output))
|
|
return Reply(body=json.dumps(_response(identity, output)).encode())
|
|
|
|
return respond
|
|
|
|
|
|
def _text(content: JsonValue) -> JsonValue:
|
|
if isinstance(content, str):
|
|
return content
|
|
return "".join(str(object_value(part).get("text")) for part in list_value(content))
|
|
|
|
|
|
def _shape(item: JsonValue) -> tuple[JsonValue, ...]:
|
|
fields: Final = object_value(item)
|
|
match fields.get("type"):
|
|
case "message":
|
|
return ("message", fields.get("role"), fields.get("phase"), _text(fields.get("content")))
|
|
case "reasoning":
|
|
return ("reasoning", fields.get("id"))
|
|
case "function_call" | "custom_tool_call" as kind:
|
|
return (kind, fields.get("call_id"), fields.get("name"))
|
|
case kind:
|
|
return (kind, fields.get("call_id"))
|
|
|
|
|
|
def posted_inputs(wire: Wire) -> tuple[tuple[tuple[JsonValue, ...], ...], ...]:
|
|
return tuple(
|
|
tuple(_shape(item) for item in list_value(object_value(json.loads(request.body))["input"]))
|
|
for request in wire.drain()
|
|
if request.method == "POST"
|
|
)
|