Repository navigation
Description
Client or integration
Claude Code
Provider or upstream service
cubapi (https://xxxxx.cn/v1/responses), configured with adapter: openai-responses
OpenCodex version
2.7.42
Endpoint or capability
POST /v1/messages translating a streaming Responses API upstream
Current behaviour
The Claude Messages bridge reports a truncated upstream stream even though the upstream returned a complete Responses SSE stream.
Control tests against the same provider, model, key, and request content:
- Direct upstream
POST /v1/responseswithstream:true: HTTP 200, includesresponse.completedanddata: [DONE]. - OpenCodex
POST /v1/responses: HTTP 200, visible output andresponse.completedreceived. - OpenCodex
POST /v1/messageswith eitherstream:trueorstream:false: fails withupstream stream ended before a terminal frame (truncated response).
The relevant wire-format difference is that this upstream emits data-only SSE records:
data: {"type":"response.output_text.delta",...}
data: {"type":"response.completed",...}
data: [DONE]
It does not emit a separate event: line. An SSE event field is optional; absent fields use the default message event.
Expected behaviour
The Claude Messages bridge should accept Responses SSE frames whose event type is carried by the JSON payload's type field when the optional SSE event: field is absent. It should translate the complete stream and emit Anthropic message_stop, matching the already-successful /v1/responses passthrough path.
Minimal redacted request or reproduction
# Provider uses adapter=openai-responses and baseUrl=https://xxxx.cn/v1.
# API key is redacted.
curl http://127.0.0.1:10101/v1/messages \
-H 'content-type: application/json' \
-H 'x-api-key: REDACTED' \
-H 'anthropic-version: 2023-06-01' \
--data '{
"model": "claude-opus-4-6",
"max_tokens": 64,
"stream": true,
"thinking": {"type": "adaptive"},
"output_config": {"effort": "low"},
"messages": [{"role": "user", "content": "Reply exactly OK"}]
}'The translated upstream request was captured and redacted. It correctly used:
{
"stream": true,
"reasoning": {"effort": "low"}
}reasoning.summary was intentionally disabled with modelSupportsReasoningSummaries:false; this is unrelated to the SSE parsing failure.
Actual response or error
HTTP 200 for stream:true, followed by:
data: {"type":"message_start",...}
data: {"type":"ping"}
data: {"type":"error","error":{"type":"overloaded_error","message":"upstream stream ended before a terminal frame (truncated response)"}}
For stream:false:
HTTP 502
{"type":"error","error":{"type":"overloaded_error","message":"upstream stream ended before a terminal frame (truncated response)"}}Upstream documentation
- SSE specification: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation
- No public cubapi-specific wire-format documentation is available. Behaviour above was verified from a direct redacted HTTP response.
Suggested mapping or implementation notes
src/claude/outbound.ts currently discards frames without an explicit SSE event name before parsing the JSON payload:
if (!eventName || !dataLine) continue;At EOF it consequently never sees response.completed and emits the truncation error.
Suggested behaviour:
- Require
dataLine, but noteventName, before parsing JSON. - Parse the payload.
- If
eventNameis absent anddata.typeis a string, usedata.typeas the event name. - Preserve current strict terminal-frame validation.
This should be a narrow compatibility fix: it does not weaken truncation detection because response.completed still must be observed.
Additional context
A combo failover control test also succeeded independently: an intentionally unreachable first target failed with 502, OpenCodex selected the second target, and /v1/responses returned the expected output. This isolates the failure to Responses SSE-to-Anthropic translation rather than provider routing or failover.
Checks
- I searched existing provider and compatibility issues, including [Bug]: anthropic adapter returns empty content (adapter_eof / 0 tokens) for streaming requests against upstreams that emit reasoning blocks (Kimi k3) #312; it covers a different Anthropic-adapter EOF/framing bug.
- The request and response were redacted.
- The expected behaviour is based on the SSE specification and the concrete Claude Code streaming requirement.
Activity
github-actions commentedon Jul 29, 2026
Automated translation bookkeeping — detected language: English.
lidge-jun commentedon Jul 29, 2026
Fixed on
devby #711, which landed asd24c5233f.Your diagnosis was exactly right, and the wire capture is what made it actionable. The bridge required an SSE
event:line and discarded frames without one, so a data-only stream reached EOF with no frame the translator had accepted — henceupstream stream ended before a terminal frame (truncated response)even thoughresponse.completedhad arrived.The fix resolves the event name from the payload's
typewhen theevent:field is absent, insrc/claude/outbound.ts:This matches the spec position you cited. WHATWG's event-stream grammar allows a block carrying only
data, and OpenAI's own Python SDK routes Responses events off the decoded payload'styperather than requiringsse.event— so a data-only Responses frame is well-formed input, not a malformed stream.Verified on the merged
devtip, five regression cases intests/claude-outbound.test.ts:That covers your control matrix: streaming and non-streaming
/v1/messagesboth work, mixed framing works, and — importantly — truncation detection is preserved. A stream that really does end without a terminal frame still fails, so this loosens the parser without weakening the guarantee that prompted the original check.The change is on
devand will ship in the next release; it is not in 2.7.42. Closing as fixed. If your provider still reports truncation once it is released, please reopen with the same style of capture — it was unusually easy to work from.