Bug Description
Every /codex/v1/chat/completions request with a streaming Codex upstream response returns HTTP 502 with a ResponseObject.output Field required [type=missing] Pydantic validation error, even though the upstream call itself succeeds (HTTP 200, full SSE stream, non-zero output_tokens).
Reproducible in 0.2.6 and 0.2.7. Code inspection of the relevant tags shows the SSE reassembly code has been unchanged since 5e41a2d (2025-09-28), so this affects every 0.2.x release.
First observed 2026-04-09 after months of stable operation, which suggests the ChatGPT Codex backend (https://chatgpt.com/backend-api/codex/responses) recently changed its SSE event shape in a way the current ResponsesAccumulator does not handle.
Error
{
"error": {
"type": "server_error",
"message": "Failed to convert provider response using format chain",
"details": "1 validation error for ResponseObject\noutput\n Field required [type=missing, input_value={'id': 'resp_...', 'object': 'response', ..., 'top_logprobs': 0}, input_type=dict]\n For further information visit https://errors.pydantic.dev/2.12/v/missing"
}
}
Structlog traceback in ccproxy:
codex_format_chain_response_failed
File "ccproxy/plugins/codex/adapter.py", line 202, in handle_request
response_payload = await self._apply_format_chain(...)
File "ccproxy/services/adapters/http_adapter.py", line 941, in _apply_format_chain
current = await adapter.convert_response(current)
File "ccproxy/services/adapters/simple_converters.py", line 348, in convert_openai_responses_to_openai_chat_response
response = openai_models.ResponseObject.model_validate(data)
ValidationError: 1 validation error for ResponseObject
output
Field required [type=missing, ...]
Upstream call is healthy
Structured log line from the same request (request_id 5d610c11):
sse_connection_ended closed_by=server duration_ms=38151 success=True status_code=200
url=https://chatgpt.com/backend-api/codex/responses
streaming_buffer_upstream_response status_code=200 total_bytes=315959 total_chunks=431
streaming_metrics_finalized usage_metrics={'input_tokens': 22800, 'output_tokens': 1954,
'cache_read_input_tokens': 2560, 'reasoning_tokens': 1572}
So Codex returned 431 SSE chunks totaling 315 KB with 1 954 output tokens — the model did produce output. The failure is purely in ccproxy's reassembly of that stream into a ResponseObject dict.
Reassembled body preview
streaming_buffer_response_ready logs the body ccproxy builds from the stream:
{
"id": "resp_070491e4e19538ee0169d8eee6e49481919773ebfb95e31586",
"object": "response",
"created_at": 1775824614,
"status": "completed",
"model": "gpt-5.3-codex",
"parallel_tool_calls": true,
"usage": {
"input_tokens": 22800,
"input_tokens_details": {"cached_tokens": 2560},
"output_tokens": 1954,
"output_tokens_details": {"reasoning_tokens": 1572},
"total_tokens": 24754
},
"instructions": "You are Codex, a coding agent based on GPT-5. ..."
}
No output field anywhere. The Pydantic input_value preview additionally shows 'top_logprobs': 0 at the tail of the dict — a key that's not in ccproxy/llms/models/openai.py::ResponseObject, suggesting extra fields from a response.created / response.in_progress payload leaked through while the actual output items (emitted separately via response.output_item.added / .done and text-delta events) were never collected into the final dict.
Root cause (code inspection)
Two independent defects in ccproxy/streaming/buffer.py::_parse_collected_stream (lines 502–655 in 0.2.6/0.2.7):
1. output fallback uses the wrong type
# ccproxy/streaming/buffer.py, ~line 587
response_obj.setdefault("output", response_obj.get("output") or {})
The fallback value is {} (empty dict) but ResponseObject.output is typed as list[MessageOutput | ReasoningOutput | FunctionCallOutput | dict[str, Any]]. Even when this fallback runs, the subsequent ResponseObject.model_validate() fails because {} is not a list.
Should be [].
2. ResponsesAccumulator.rebuild_response_object fallback path does not populate output
When _parse_collected_stream calls:
completed_payload = accumulator_for_rebuild.get_completed_response()
if completed_payload is not None:
response_obj = completed_payload
return response_obj
try:
response_obj = accumulator_for_rebuild.rebuild_response_object(response_obj)
...
except Exception as exc:
logger.debug("response_rebuild_failed", error=str(exc), ...)
get_completed_response() returns None (so self.completed_response was never set by a typed ResponseCompletedEvent), and rebuild_response_object() silently fails inside the try/except — the debug log is discarded at --log-level info so there is no visible trace. response_obj then retains its pre-rebuild state, which is missing output, and gets returned to the caller and handed to the format chain → validation error.
Most likely the upstream response.completed event shape has changed and no longer validates against openai_models.ResponseCompletedEvent / ResponseObject, causing _coerce_stream_event inside ResponsesAccumulator.accumulate to silently drop it (wrapped in contextlib.suppress(Exception) at buffer.py:563).
Affected versions
Confirmed by wheel comparison:
ccproxy/streaming/buffer.py and ccproxy/llms/streaming/accumulators.py are byte-for-byte identical across v0.2.0 → v0.2.7. Both frozen at commit 5e41a2d (2025-09-28).
- So every 0.2.x release has this bug once the upstream SSE shape triggers the fallback path.
Reproduction
# In a ccproxy 0.2.7 pod with Codex OAuth credentials:
curl -sS http://localhost:8000/codex/v1/chat/completions \
-H 'content-type: application/json' \
-d '{
"model": "gpt-5-codex",
"stream": false,
"messages": [
{"role": "user", "content": "<any prompt that produces a few thousand output tokens>"}
]
}'
Result: HTTP 502 with the error above.
The easiest way to see the raw upstream payload is to run ccproxy with --log-level debug and grep for streaming_buffer_collected_content and streaming_buffer_accumulator_rebuild_attempt around the failing request.
Suggested fixes
- Immediate: change
or {} to or [] in ccproxy/streaming/buffer.py:587. This is obviously wrong today regardless of the upstream shape.
- Real fix: capture an up-to-date sample of the
chatgpt.com/backend-api/codex/responses SSE stream (example request IDs from my logs: 520cbb5a, 5d610c11, 90b829eb) and update ResponsesAccumulator._coerce_stream_event / openai_models.*Event to validate against the current event schema. The contextlib.suppress(Exception) at buffer.py:563 and the try/except at buffer.py:635 are currently hiding the coercion failures — logging those at warning would make future drift visible.
- Defensive: if
rebuild_response_object fails, the buffer could fall back to regenerating a minimal ResponseObject from the aggregated text_content / usage it already tracks, instead of returning a dict that the next stage can't possibly validate.
Environment
ccproxy-api: 0.2.6 (originally hit on 0.2.7; downgrading to 0.2.6 did not change behaviour because the reassembly code is identical)
- Python: 3.14
- OS: Linux (python:3.14-slim container on Talos Kubernetes)
- Upstream:
https://chatgpt.com/backend-api/codex/responses via OAuth (@openai/codex 0.118.0 CLI for token management)
- Client: a GitLab-CI
mr-review job POSTing chat-completions-shaped requests with streaming disabled
- ccproxy command:
ccproxy serve --port 8000 --host 0.0.0.0 --log-level info --enable-plugin codex --enable-plugin access_log --disable-plugin claude_sdk --disable-plugin claude_api --disable-plugin copilot
Bug Description
Every
/codex/v1/chat/completionsrequest with a streaming Codex upstream response returns HTTP 502 with aResponseObject.output Field required [type=missing]Pydantic validation error, even though the upstream call itself succeeds (HTTP 200, full SSE stream, non-zerooutput_tokens).Reproducible in 0.2.6 and 0.2.7. Code inspection of the relevant tags shows the SSE reassembly code has been unchanged since
5e41a2d(2025-09-28), so this affects every0.2.xrelease.First observed 2026-04-09 after months of stable operation, which suggests the ChatGPT Codex backend (
https://chatgpt.com/backend-api/codex/responses) recently changed its SSE event shape in a way the currentResponsesAccumulatordoes not handle.Error
{ "error": { "type": "server_error", "message": "Failed to convert provider response using format chain", "details": "1 validation error for ResponseObject\noutput\n Field required [type=missing, input_value={'id': 'resp_...', 'object': 'response', ..., 'top_logprobs': 0}, input_type=dict]\n For further information visit https://errors.pydantic.dev/2.12/v/missing" } }Structlog traceback in ccproxy:
Upstream call is healthy
Structured log line from the same request (request_id
5d610c11):So Codex returned 431 SSE chunks totaling 315 KB with 1 954 output tokens — the model did produce output. The failure is purely in ccproxy's reassembly of that stream into a
ResponseObjectdict.Reassembled body preview
streaming_buffer_response_readylogs the body ccproxy builds from the stream:{ "id": "resp_070491e4e19538ee0169d8eee6e49481919773ebfb95e31586", "object": "response", "created_at": 1775824614, "status": "completed", "model": "gpt-5.3-codex", "parallel_tool_calls": true, "usage": { "input_tokens": 22800, "input_tokens_details": {"cached_tokens": 2560}, "output_tokens": 1954, "output_tokens_details": {"reasoning_tokens": 1572}, "total_tokens": 24754 }, "instructions": "You are Codex, a coding agent based on GPT-5. ..." }No
outputfield anywhere. The Pydanticinput_valuepreview additionally shows'top_logprobs': 0at the tail of the dict — a key that's not inccproxy/llms/models/openai.py::ResponseObject, suggesting extra fields from aresponse.created/response.in_progresspayload leaked through while the actualoutputitems (emitted separately viaresponse.output_item.added/.doneand text-delta events) were never collected into the final dict.Root cause (code inspection)
Two independent defects in
ccproxy/streaming/buffer.py::_parse_collected_stream(lines 502–655 in 0.2.6/0.2.7):1.
outputfallback uses the wrong typeThe fallback value is
{}(empty dict) butResponseObject.outputis typed aslist[MessageOutput | ReasoningOutput | FunctionCallOutput | dict[str, Any]]. Even when this fallback runs, the subsequentResponseObject.model_validate()fails because{}is not a list.Should be
[].2.
ResponsesAccumulator.rebuild_response_objectfallback path does not populateoutputWhen
_parse_collected_streamcalls:get_completed_response()returnsNone(soself.completed_responsewas never set by a typedResponseCompletedEvent), andrebuild_response_object()silently fails inside thetry/except— the debug log is discarded at--log-level infoso there is no visible trace.response_objthen retains its pre-rebuild state, which is missingoutput, and gets returned to the caller and handed to the format chain → validation error.Most likely the upstream
response.completedevent shape has changed and no longer validates againstopenai_models.ResponseCompletedEvent/ResponseObject, causing_coerce_stream_eventinsideResponsesAccumulator.accumulateto silently drop it (wrapped incontextlib.suppress(Exception)atbuffer.py:563).Affected versions
Confirmed by wheel comparison:
ccproxy/streaming/buffer.pyandccproxy/llms/streaming/accumulators.pyare byte-for-byte identical across v0.2.0 → v0.2.7. Both frozen at commit5e41a2d(2025-09-28).Reproduction
Result: HTTP 502 with the error above.
The easiest way to see the raw upstream payload is to run ccproxy with
--log-level debugand grep forstreaming_buffer_collected_contentandstreaming_buffer_accumulator_rebuild_attemptaround the failing request.Suggested fixes
or {}toor []inccproxy/streaming/buffer.py:587. This is obviously wrong today regardless of the upstream shape.chatgpt.com/backend-api/codex/responsesSSE stream (example request IDs from my logs:520cbb5a,5d610c11,90b829eb) and updateResponsesAccumulator._coerce_stream_event/openai_models.*Eventto validate against the current event schema. Thecontextlib.suppress(Exception)atbuffer.py:563and thetry/exceptatbuffer.py:635are currently hiding the coercion failures — logging those atwarningwould make future drift visible.rebuild_response_objectfails, the buffer could fall back to regenerating a minimalResponseObjectfrom the aggregatedtext_content/usageit already tracks, instead of returning a dict that the next stage can't possibly validate.Environment
ccproxy-api: 0.2.6 (originally hit on 0.2.7; downgrading to 0.2.6 did not change behaviour because the reassembly code is identical)https://chatgpt.com/backend-api/codex/responsesvia OAuth (@openai/codex0.118.0 CLI for token management)mr-reviewjob POSTing chat-completions-shaped requests with streaming disabledccproxy serve --port 8000 --host 0.0.0.0 --log-level info --enable-plugin codex --enable-plugin access_log --disable-plugin claude_sdk --disable-plugin claude_api --disable-plugin copilot