Skip to content

fix(streaming): coerce Responses API output field to list (#55)#57

Merged
CaddyGlow merged 1 commit intomainfrom
fix/issue-55-responses-output-list
Apr 13, 2026
Merged

fix(streaming): coerce Responses API output field to list (#55)#57
CaddyGlow merged 1 commit intomainfrom
fix/issue-55-responses-output-list

Conversation

@CaddyGlow
Copy link
Copy Markdown
Owner

Summary

  • Fixes Codex plugin returns HTTP 502 on every /codex/v1/chat/completions: ResponseObject.output missing after SSE reassembly (affects all 0.2.x) #55. Every /codex/v1/chat/completions request was returning HTTP 502 with ResponseObject.output Field required because _parse_collected_stream set the fallback output to {} (a dict) when the upstream SSE stream shape drifted and ResponsesAccumulator silently dropped response.completed / response.created events.
  • The buffer now coerces any non-list output to [] both before and after the accumulator rebuild, so the payload handed to the format chain always validates against ResponseObject.
  • Silent contextlib.suppress(Exception) + debug logs around accumulator.accumulate and rebuild_response_object are upgraded to warning, so future upstream SSE drift is visible instead of hidden.
  • Does not attempt fix (2) from the issue (refreshing the Codex SSE event schema) — that requires an up-to-date capture of the current chatgpt.com/backend-api/codex/responses stream. The defensive coercion is enough to stop the 502s in the meantime, and the new warning logs will surface which events are being dropped.

Test plan

  • New regression tests in tests/unit/streaming/test_buffer_parse_responses.py covering:
    • unrecognized response.completed shape (the production bug)
    • upstream sending output as a bare dict
    • happy path where the accumulator successfully rebuilds message outputs
  • uv run pytest tests/unit/streaming/ tests/unit/llms/streaming/ (10 passed)
  • make pre-commit

When the Codex upstream SSE stream drifts and the ResponsesAccumulator
silently drops response.completed / response.created events, the buffer
was leaving response_obj["output"] as an empty dict (or missing), which
caused every /codex/v1/chat/completions request to fail downstream with
"ResponseObject.output Field required".

Fixes:
- _parse_collected_stream now always coerces non-list output values to []
  both before and after accumulator rebuild, so the payload handed to the
  format chain always satisfies ResponseObject validation.
- Silent contextlib.suppress and debug-only except blocks around
  accumulator.accumulate and rebuild_response_object are upgraded to
  warning-level logs so upstream SSE drift is visible in production.

Adds regression tests covering:
- unrecognized response.completed event shape (the production bug)
- upstream sending output as a bare dict
- happy path where accumulator successfully rebuilds message outputs
Copilot AI review requested due to automatic review settings April 13, 2026 10:00
@CaddyGlow CaddyGlow merged commit 6e30225 into main Apr 13, 2026
9 checks passed
@CaddyGlow CaddyGlow deleted the fix/issue-55-responses-output-list branch April 13, 2026 10:01
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes Codex streaming failures (#55) by ensuring the reconstructed OpenAI Responses API payload always conforms to ResponseObject validation—specifically, that output is present and always a list—so /codex/v1/chat/completions no longer returns 502s when upstream SSE event shapes drift.

Changes:

  • Coerce any non-list output to [] during Responses payload reconstruction (both pre- and post-accumulator rebuild).
  • Promote previously-suppressed/hidden accumulator + rebuild failures to warning logs for better visibility into upstream SSE drift.
  • Add regression tests covering the production failure mode, non-list output, and the successful rebuild path.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 5 comments.

File Description
ccproxy/streaming/buffer.py Makes _parse_collected_stream resilient to upstream SSE drift by coercing output to a list and surfacing accumulator/rebuild failures via warnings.
tests/unit/streaming/test_buffer_parse_responses.py Adds unit regression coverage to ensure parsed Responses payloads always validate as ResponseObject, including drift/fallback scenarios.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +32 to +35
@pytest.fixture
def buffer() -> StreamingBufferService:
return StreamingBufferService(http_client=httpx.AsyncClient())

Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test fixture creates an httpx.AsyncClient but never closes it, which can leak connections and trigger unclosed-client warnings/flaky tests. Convert this into an async fixture that yields the StreamingBufferService and ensures the underlying AsyncClient is closed in a teardown (e.g., via async with or aclose() in finally).

Copilot uses AI. Check for mistakes.
Comment on lines +69 to +73
parsed = await buffer._parse_collected_stream(
chunks=chunks,
handler_config=None, # type: ignore[arg-type]
request_context=_Ctx(), # type: ignore[arg-type]
)
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These tests pass handler_config=None (with type ignores) to a method that expects a HandlerConfig and may access handler_config.sse_parser on other code paths. This makes the tests fragile against refactors; prefer passing a minimal stub/real HandlerConfig instance with sse_parser=None so the signature is respected without type: ignore.

Copilot uses AI. Check for mistakes.
logger.warning(
"streaming_buffer_accumulate_failed",
event_type=event_type,
error=str(exc),
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new warning log on accumulator failures drops the traceback (exc_info), which makes diagnosing upstream event/schema drift harder in production. Consider logging exc_info=exc (or equivalent) alongside error=str(exc) so the stack/context is retained.

Suggested change
error=str(exc),
error=str(exc),
exc_info=exc,

Copilot uses AI. Check for mistakes.
Comment on lines +619 to +623
logger.warning(
"streaming_buffer_rebuild_accumulate_failed",
event_type=event_type,
error=str(exc),
request_id=getattr(request_context, "request_id", None),
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above: this rebuild-accumulate warning omits exc_info, which limits observability when events fail validation/coercion. Including exc_info=exc would make these warnings actionable without having to reproduce locally.

Copilot uses AI. Check for mistakes.
"response_rebuild_failed",
error=str(exc),
request_id=getattr(request_context, "request_id", None),
category="streaming",
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

response_rebuild_failed was upgraded to warning, but it still logs only error=str(exc) without exc_info. Adding exc_info=exc would preserve the traceback and help pinpoint the specific field/event causing rebuild failures.

Suggested change
category="streaming",
category="streaming",
exc_info=exc,

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Codex plugin returns HTTP 502 on every /codex/v1/chat/completions: ResponseObject.output missing after SSE reassembly (affects all 0.2.x)

2 participants