Repository navigation
[Provider compatibility] volcengine-coding-plan: response.incomplete relays non-canonical incomplete_details.reason ("length") #7082
Description
Client or integration
OpenAI-compatible SDK
Provider or upstream service
volcengine-coding-plan (Volcengine Ark, https://ark.cn-beijing.volces.com/api/coding/v3)
OpenCodex version
2.84.0
Endpoint or capability
/v1/responses — incomplete_details.reason on response.incomplete
Current behaviour
When an upstream truncates a turn, the proxy forwards the upstream's
incomplete_details.reason verbatim. Volcengine Ark sends the non-canonical
value "length", so the proxy emits:
{ "type": "response.incomplete",
"response": { "status": "incomplete",
"incomplete_details": { "reason": "length" } } }"length" is not a legal value of the Responses
incomplete_details.reason enum, so OpenAI-conformant clients reject the turn
as an error instead of treating it as a normal length stop.
Concretely, @earendil-works/pi-ai maps the spec values and errors on anything else:
case "incomplete":
if (incompleteReason === "max_output_tokens") return { stopReason: "length" };
return { stopReason: "error",
errorMessage: `Response incomplete: ${incompleteReason}` };so the user-visible failure is a hard turn error:
Response incomplete: length
Reported from the DeepSeek Harness client, which surfaces it as
PI_AI_ERROR.
Why this looks like an OpenCodex-side inconsistency — the proxy already
owns the correct mapping, it just does not apply it on this path:
src/responses/truncated-stop-reason.ts:22-27maps"length"→
"max_output_tokens".- That mapping is used on the
done/stopReasonpath
(src/bridge/sse.ts:1257). - The
response.incompletepassthrough path does not use it:
src/adapters/openai-responses/passthrough.ts:207-208extracts the upstream
reason verbatim,passthrough.ts:659-660emits it as the internal
incompleteevent'sreason, andsrc/bridge/sse.ts:1294writes
reason: event.reasonstraight into the outbound payload.
So two truncation paths inside the same proxy disagree: one normalizes,
the other passes through.
Expected behaviour
response.incomplete should carry a canonical
incomplete_details.reason. For an upstream truncation that should be
"max_output_tokens", i.e. the same normalization the done path already
performs via truncationReasonFor().
A defensive normalization on the passthrough path would also cover other
non-canonical upstream vocabularies (Anthropic refusal,
model_context_window_exceeded, Gemini safety, …) that
TRUNCATED_STOP_REASONS already knows about.
Minimal redacted request or reproduction
Any truncating request through a volcengine-coding-plan provider. Minimal
form (key redacted):
curl -sS -N -X POST \
'https://127.0.0.1:10100/v1/responses' \
-H 'Authorization: Bearer <redacted>' \
-H 'Content-Type: application/json' \
-H 'Accept: text/event-stream' \
-d '{
"model": "volcengine-coding-plan/deepseek-v4-flash",
"input": [{"role":"user","content":"Write a 2000-word essay about the ocean."}],
"stream": true,
"store": false,
"max_output_tokens": 64
}'max_output_tokens is set deliberately low to force truncation; the same
happens naturally on any normal long turn that reaches the model's output cap.
The upstream is non-conformant too — a direct call to
https://ark.cn-beijing.volces.com/api/coding/v3/responses with the same body
returns incomplete_details: {"reason": "length"} in its own SSE, so the proxy
is faithfully relaying a bad value rather than inventing it.
Actual response or error
Proxy output (final event):
event: response.incomplete
data: {"type":"response.incomplete","response":{"status":"incomplete",
"incomplete_details":{"reason":"length"},"usage":{...,"output_tokens":64}}}
Grep-friendly:
$ curl ... | grep -o '"incomplete_details":{[^}]*}'
"incomplete_details":{"reason":"length"}
Upstream documentation
- OpenAI Responses
IncompleteDetails.reasonis a closed enum —
Literal["max_output_tokens", "max_messages", "content_filter", "steered"]
in the official SDK:
https://github.com/openai/openai-python/blob/main/src/openai/types/responses/response.py - Volcengine Ark Responses endpoint (no public spec for the enum; observed
behaviour is the"length"value above):
https://www.volcengine.com/docs/82379
Suggested mapping or implementation notes
Normalize on the passthrough path as well, so both truncation routes agree:
- In
src/adapters/openai-responses/passthrough.ts, when building the
{ type: "incomplete", reason }event (lines ~659-660 and ~753-754), run the
extracted reason through the existing
truncationReasonFor()/TRUNCATED_STOP_REASONStable and emit
"max_output_tokens"/"content_filter". - Alternatively normalize once at the emit site in
src/bridge/sse.ts:1294
(reason: truncationReasonFor(event.reason) ?? event.reason), which also
covers adapters that are not the openai-responses passthrough. - Keeping an unrecognized reason as-is (rather than forcing
"content_filter")
seems right, sincetruncationReasonFordeliberately does not classify
unknown values as truncation.
Additional context and attachments
- Observed while routing a
volcengine-coding-planprovider through OpenCodex
2.84.0 on macOS; the client is an OpenAI-Responses-compatible SDK, not
codex-rs, so no Codex-side workaround is available. - Workaround currently in use on the client side: declare an explicit
max_output_tokensfor every routed model, which reduces (but does not
eliminate) how often the upstream truncates. - Related reading in-tree:
src/responses/truncated-stop-reason.tsalready
documents this exact class of adapter vocabulary drift (#422), and it lists
"length"explicitly — which is what makes the passthrough gap surprising.
- I searched existing provider and compatibility issues.
- The request and response were redacted.
- The expected behaviour is based on an upstream specification or a concrete client requirement.
Activity
Ingwannu commentedon Oct 16, 2026
The passthrough incomplete-reason normalization is implemented in #7122 and carried into #7218. The latter also carries the separate terminal-refusal fix from #7152; normalizing a reason alone must not be presented as solving client retry behavior.
The carry is still open at the inspected snapshot. Keeping this issue open until dev contains the accepted change. Existing verification is in those PRs; no live Volcengine request was made in this pass.
Ingwannu commentedon Oct 16, 2026
The previously pending integration is now on
dev: #7218, merge commit3785915d780fe3b8b13c551c253cb478c504840e. I verified ancestry againstdev@c03eafd46b; the carry contains the fix linked in the earlier triage comment.Closing as completed on dev. The bundle contains its own verification and documented limits; I did not rerun those runtime tests or confirm the reporter's live client. This does not assert release/package availability.