litellm/tests/integration/_support/agentic_probe.py
devin-ai-integration[bot] a983b2a5e7
fix(responses): honor caller stream flag when provider forces SSE (internal copy of #34095) (#41235)
* fix(responses): honor caller stream flag when provider forces SSE

The Responses handlers decided whether to hand back a streaming iterator
from the provider payload's `stream` field, which chatgpt sets
unconditionally because the Codex backend only serves SSE. A caller that
sent `stream: false` therefore received a raw SSE stream on /v1/responses,
and the chat-completions bridge failed with "Unknown items in responses
API response: []" once its recovery path lost the raw SSE it reads from

Transport streaming still follows the provider payload; only the caller's
own `stream` value now decides the response shape. When the provider
forces SSE for a non-streaming caller the body is read and aggregated
through the existing path

* test(chatgpt): inject the authenticator into the responses config so handler tests never log in

* fix(responses): treat an extra_body stream flag as the caller's own and drop a redundant comment

* test(integration): audit the chatgpt caller stream flag across responses, chat and messages

---------

Co-authored-by: SeongWoon Cho <coffee@soylatte.kr>
Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-10-06 03:30:47 +00:00

50 lines
1.7 KiB
Python

from __future__ import annotations
import json
import os
from collections.abc import Mapping, Sequence
from pathlib import Path
from typing import Final
from integration._support import responses_vendor as rv
from pydantic import JsonValue
from litellm.integrations.custom_logger import CustomLogger
OUT_ENVIRONMENT: Final = "AGENTIC_PROBE_OUT"
class AgenticProbe(CustomLogger):
"""Records every agentic-loop hook call the proxy makes, one JSON line per call, and never runs a loop."""
async def async_should_run_agentic_loop(
self,
response: object,
model: str,
messages: Sequence[Mapping[str, object]],
tools: Sequence[Mapping[str, object]] | None,
stream: bool,
custom_llm_provider: str,
kwargs: Mapping[str, object],
) -> tuple[bool, dict[str, object]]: # mutable-ok: the CustomLogger hook contract returns a dict
out: Final = os.environ.get(OUT_ENVIRONMENT)
if out:
line: Final[Mapping[str, JsonValue]] = {
"marker": rv.newest_marker(f"{messages!s} {response!s}"),
"surface": str(kwargs.get("_agentic_loop_api_surface")),
"response_type": type(response).__name__,
"stream": stream,
"model": model,
"provider": custom_llm_provider,
}
with open(out, "a", encoding="utf-8") as sink:
sink.write(json.dumps(line) + "\n")
return False, {}
probe: Final = AgenticProbe()
def lines(path: Path, marker: str) -> tuple[Mapping[str, JsonValue], ...]:
recorded: Final = tuple(rv.JSON_OBJECT.validate_json(line) for line in path.read_text().splitlines() if line)
return tuple(line for line in recorded if line["marker"] == marker)