Version: v7.3.4 (8335eac); the files below are unchanged on dev @ 6724a95
Path: Devin OAuth (devin/swe-2) → /v1/chat/completions and /v1/responses, streaming
Clients: opencode (@ai-sdk/openai, chat completions) and ZCode (@ai-sdk/openai, Responses API); both are Vercel AI SDK v5 clients
OS: CPA in Docker on Ubuntu/arm64; clients on Windows 11
Summary
With devin/swe-2 behind CPA, any turn that ends in a tool call fails on both clients while plain-text turns work. The tool call is streamed to the client (tool-input-start + N × tool-input-delta) but the AI SDK never emits the final tool-call; the client renders the tool as failed/orphaned and the turn ends with finishReason: stop. In practice: small answers are fine, every real edit fails, and larger edits fail more reliably.
Tracing the two endpoints separately shows two different defects producing the same symptom, plus a spec violation on the same path:
/v1/responses: the function_call response.output_item.done frame carries no status. The AI SDK schema rejects it and silently downgrades it to an unknown chunk, so the tool call is never emitted. This hits every tool call, independent of size. (ZCode)
/v1/chat/completions: tool_calls[].index is the interactions step index, not the index into tool_calls. A tool call preceded by a thought or text step is emitted as tool_calls[1]/[2] with no [0].
- Both endpoints: Devin's per-frame
StopReason is decoded but never used, so an output-budget truncation mid tool call is reported as finish_reason: "stop" (or response.completed) with an unparsable arguments string. The AI SDK chat path only emits tool-call once arguments parses as JSON, so the call is never emitted. (opencode)
Defects 1 and 2 are in the shared interactions → OpenAI translators, so they likely affect every interactions-backed provider (Gemini Interactions API, Antigravity) on these endpoints, not only Devin; I have only verified them against Devin. Defect 3 is in the Devin executor.
Example A — ZCode, /v1/responses
Task: "generate an HTML file with an SVG pelican-on-a-bicycle animation" → the model answers with a short text and one write_file call (~260 lines).
CPA main.log: 200 | 4m20s | POST "/v1/responses", no warnings.
ZCode's AI SDK diagnostics for the turn:
finishReason=stop toolCallCount=0 emittedError=false errorChunkCount=0
chunkCounts: reasoning-start=1 reasoning-end=1 text-start=1 text-delta=42 text-end=1
tool-input-start=1 tool-input-delta=4705 tool-input-end=0 tool-call=0
Every other item closed normally (reasoning-end, text-end), only the function_call never got its tool-input-end/tool-call. Three earlier turns the same day show the identical pattern (3528 / 3621 / 4705 deltas, always tool-input-end=0, finishReason=stop, no error chunk). ZCode shows the tool as "执行失败" (execution failed) because the turn ended with a pending tool input.
Reproducing the same request with curl shows what CPA sends for the function_call item:
{"type":"response.output_item.added","output_index":1,
"item":{"id":"write_file_0#…","type":"function_call","call_id":"write_file_0#…","name":"write_file","arguments":""}}
…
{"type":"response.output_item.done","output_index":1,
"item":{"id":"write_file_0#…","type":"function_call","call_id":"write_file_0#…","name":"write_file","arguments":"{…}"}}
Neither item has a status. In the AI SDK bundled with ZCode (@ai-sdk/openai, Responses model), the chunk schema for response.output_item.done is a discriminated union whose function_call member is:
z.object({ type: z.literal('function_call'), id: z.string(), call_id: z.string(),
name: z.string(), arguments: z.string(), status: z.literal('completed'), … })
status is required. The outer chunk union ends with a catch-all
z.object({ type: z.string() }).loose().transform(e => ({ type: 'unknown_chunk', message: e.type }))
so a frame that fails the strict member is not an error — it becomes unknown_chunk and the isResponseOutputItemDoneChunk handler (which emits tool-input-end + tool-call and sets hasFunctionCall = true) never runs. response.completed then parses fine (it carries usage), hasFunctionCall is still false, and the finish reason is mapped to stop. That is exactly the diagnostics above: no error chunk, no tool-input-end, finishReason=stop.
The response.completed payload is also fragile for the same reason: it starts from "usage":{} and only fills fields that the interaction provides, while the schema requires usage.input_tokens and usage.output_tokens as numbers. With Devin the usage is present so this frame survives; an interactions source without usage would lose the terminal frame too.
Related in the request direction: ConvertOpenAIResponsesRequestToInteractions only maps max_output_tokens for Antigravity (agent_config.max_total_tokens); for other models max_output_tokens, temperature and top_p are dropped, so a Responses client cannot bound the output at all (a max_output_tokens: 400 request produced 2868 output tokens), whereas the chat-completions request translator does map them (copyOpenAIChatGenerationConfigToInteractions).
Where — internal/translator/openai/interactions/responses/interactions_openai_responses_response.go
- L376
interactionsStepStopToResponses: done-item template {"id":"","type":"function_call","call_id":"","name":"","arguments":""} — no status
- L262
interactionsStepStartToResponses: added-item template — no status (OpenAI sends in_progress)
- L518
responsesCompletedOutputItem: function_call output in response.completed — no status
- L395
responsesCompletedEvent: "usage":{} template; always response.completed / status: "completed"
interactions_openai_responses_request.go L60–L68: max_output_tokens mapping is Antigravity-only
Example B — opencode, /v1/chat/completions
Same kind of task (one large write/edit tool call). opencode's log for the failing turns ends with the tool never resolving:
… stream error
… orphaned interrupted tool
and the UI shows the edit as interrupted. Small edits on the same model succeed.
curl against /v1/chat/completions with one tool and max_tokens: 400 to force the budget:
"delta":{"tool_calls":[{"index":1,"id":"write_file_0#…","type":"function","function":{"name":"write_file","arguments":""}}]}
…
"delta":{"tool_calls":[{"index":1,"function":{"arguments":"…"}}]} ← ×N, JSON cut mid-string
…
"choices":[{"index":0,"delta":{},"finish_reason":"stop"}],"usage":{"prompt_tokens":584,"completion_tokens":400,"total_tokens":984}
Two things are wrong in this stream:
tool_calls[].index = 1 for the only tool call. The Devin executor numbers interactions steps with one running counter across thought → model_output → function_call, and the chat translator writes that step index straight into tool_calls[].index (and into the call_%d fallback id). OpenAI defines index as the position in the tool_calls array — 0-based and contiguous per response. A client that assembles the array from index ends up with [null, {…}]; anything that reads tool_calls[0] or validates contiguity breaks. This happens on every tool call that follows a thought or text step, i.e. on essentially every Devin tool call. I have not isolated a client failure caused by this alone, but it is a plain spec violation on the primary path.
finish_reason: "stop" with completion_tokens == max_tokens and truncated arguments. Devin's Connect-RPC response frames carry a StopReason varint in field 5. helps.ParseDevinFrame decodes it into DevinFrameResult.StopReason (devin_wire.go L112, L542 — the comment there already notes 2/4=stop, 10=tool_calls) but devin_executor.go never reads the field; interaction.completed is always emitted with status: "completed" (L823 streaming, L1086 non-stream). The interactions consumers therefore can only guess: chat completions emits stop/tool_calls (L215–L217), never length; Responses always emits response.completed; Claude emits end_turn/tool_use, never max_tokens.
The enum values (from the Windsurf extension's setEnumType("StopReason", …), as documented in rsvedant/opencode-windsurf-auth src/cloud-direct/chat.ts):
0 UNSPECIFIED 2 STOP_PATTERN 10 FUNCTION_CALL
1 INCOMPLETE 3 MAX_TOKENS 11 CONTENT_FILTER
4–9 internal 12 NON_INSERTION 13 ERROR (errors arrive via the Connect trailer)
1 and 3 mean the output was cut short. Without them the client receives a tool call whose arguments never becomes valid JSON and a finish_reason that says the model stopped on its own. @ai-sdk/openai (chat) emits tool-call only when the accumulated arguments parses, so for a truncated call it emits nothing at all; opencode sees a stream that ended with a half-built tool and reports orphaned interrupted tool. Because the budget is only exceeded by long outputs, this is why "large edits always fail while small ones work".
Where
internal/translator/openai/interactions/chat-completions/openai_interactions_response.go L161 (interactionsStepStartToOpenAIChat passes the step index), L245 / L255 (sjson.SetBytes(toolCall, "index", index)), L215–L217 (finish_reason derivation)
internal/runtime/executor/devin_executor.go L570 / L968 (ParseDevinFrame result consumed, StopReason ignored), L823 / L1086 (completion payload always completed)
internal/runtime/executor/helps/devin_wire.go L112, L542
Steps to reproduce
Chat completions (defects 2 and 3):
curl -N https://<cpa>/v1/chat/completions -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' -d '{
"model":"devin/swe-2","stream":true,"max_tokens":400,
"messages":[{"role":"user","content":"Use write_file to create notes.md with a 2000-word essay about the history of Go. Do not answer in text, call the tool."}],
"tools":[{"type":"function","function":{"name":"write_file","parameters":{"type":"object","properties":{"path":{"type":"string"},"content":{"type":"string"}},"required":["path","content"]}}}]}'
Observed: tool_calls[].index: 1, finish_reason: "stop", completion_tokens: 400, arguments not valid JSON.
Responses (defect 1; any tool call, no truncation needed):
curl -N https://<cpa>/v1/responses -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' -d '{
"model":"devin/swe-2","stream":true,
"input":[{"role":"user","content":"Use write_file to create poem.txt with a 30-line poem. Do not answer in text, call the tool."}],
"tools":[{"type":"function","name":"write_file","parameters":{"type":"object","properties":{"path":{"type":"string"},"content":{"type":"string"}},"required":["path","content"]}}]}'
Observed: response.output_item.added / .done items for the function_call have no status; fed through @ai-sdk/openai's Responses model the tool call is never emitted and the turn finishes with stop. Adding "max_output_tokens": 400 to the same request has no effect on output length.
Expected behavior
/v1/responses: function_call items carry status (in_progress on added, completed on done) so standard Responses clients accept them; a truncated response ends with response.incomplete and incomplete_details.reason: "max_output_tokens"; max_output_tokens is honored.
/v1/chat/completions: tool_calls[].index is the 0-based position within the response's tool calls; a truncated response ends with finish_reason: "length".
- Devin's
StopReason (1 INCOMPLETE, 3 MAX_TOKENS, 11 CONTENT_FILTER) is reflected in the completion payload so every downstream format (length / response.incomplete / max_tokens) can report truncation instead of a normal stop.
Version: v7.3.4 (
8335eac); the files below are unchanged ondev@6724a95Path: Devin OAuth (
devin/swe-2) →/v1/chat/completionsand/v1/responses, streamingClients: opencode (
@ai-sdk/openai, chat completions) and ZCode (@ai-sdk/openai, Responses API); both are Vercel AI SDK v5 clientsOS: CPA in Docker on Ubuntu/arm64; clients on Windows 11
Summary
With
devin/swe-2behind CPA, any turn that ends in a tool call fails on both clients while plain-text turns work. The tool call is streamed to the client (tool-input-start+ N ×tool-input-delta) but the AI SDK never emits the finaltool-call; the client renders the tool as failed/orphaned and the turn ends withfinishReason: stop. In practice: small answers are fine, every real edit fails, and larger edits fail more reliably.Tracing the two endpoints separately shows two different defects producing the same symptom, plus a spec violation on the same path:
/v1/responses: thefunction_callresponse.output_item.doneframe carries nostatus. The AI SDK schema rejects it and silently downgrades it to an unknown chunk, so the tool call is never emitted. This hits every tool call, independent of size. (ZCode)/v1/chat/completions:tool_calls[].indexis the interactions step index, not the index intotool_calls. A tool call preceded by a thought or text step is emitted astool_calls[1]/[2]with no[0].StopReasonis decoded but never used, so an output-budget truncation mid tool call is reported asfinish_reason: "stop"(orresponse.completed) with an unparsableargumentsstring. The AI SDK chat path only emitstool-callonceargumentsparses as JSON, so the call is never emitted. (opencode)Defects 1 and 2 are in the shared
interactions → OpenAItranslators, so they likely affect every interactions-backed provider (Gemini Interactions API, Antigravity) on these endpoints, not only Devin; I have only verified them against Devin. Defect 3 is in the Devin executor.Example A — ZCode,
/v1/responsesTask: "generate an HTML file with an SVG pelican-on-a-bicycle animation" → the model answers with a short text and one
write_filecall (~260 lines).CPA
main.log:200 | 4m20s | POST "/v1/responses", no warnings.ZCode's AI SDK diagnostics for the turn:
Every other item closed normally (
reasoning-end,text-end), only the function_call never got itstool-input-end/tool-call. Three earlier turns the same day show the identical pattern (3528 / 3621 / 4705 deltas, alwaystool-input-end=0,finishReason=stop, no error chunk). ZCode shows the tool as "执行失败" (execution failed) because the turn ended with a pending tool input.Reproducing the same request with curl shows what CPA sends for the function_call item:
{"type":"response.output_item.added","output_index":1, "item":{"id":"write_file_0#…","type":"function_call","call_id":"write_file_0#…","name":"write_file","arguments":""}} … {"type":"response.output_item.done","output_index":1, "item":{"id":"write_file_0#…","type":"function_call","call_id":"write_file_0#…","name":"write_file","arguments":"{…}"}}Neither item has a
status. In the AI SDK bundled with ZCode (@ai-sdk/openai, Responses model), the chunk schema forresponse.output_item.doneis a discriminated union whosefunction_callmember is:statusis required. The outer chunk union ends with a catch-allso a frame that fails the strict member is not an error — it becomes
unknown_chunkand theisResponseOutputItemDoneChunkhandler (which emitstool-input-end+tool-calland setshasFunctionCall = true) never runs.response.completedthen parses fine (it carries usage),hasFunctionCallis stillfalse, and the finish reason is mapped tostop. That is exactly the diagnostics above: no error chunk, notool-input-end,finishReason=stop.The
response.completedpayload is also fragile for the same reason: it starts from"usage":{}and only fills fields that the interaction provides, while the schema requiresusage.input_tokensandusage.output_tokensas numbers. With Devin the usage is present so this frame survives; an interactions source without usage would lose the terminal frame too.Related in the request direction:
ConvertOpenAIResponsesRequestToInteractionsonly mapsmax_output_tokensfor Antigravity (agent_config.max_total_tokens); for other modelsmax_output_tokens,temperatureandtop_pare dropped, so a Responses client cannot bound the output at all (amax_output_tokens: 400request produced 2868 output tokens), whereas the chat-completions request translator does map them (copyOpenAIChatGenerationConfigToInteractions).Where —
internal/translator/openai/interactions/responses/interactions_openai_responses_response.gointeractionsStepStopToResponses: done-item template{"id":"","type":"function_call","call_id":"","name":"","arguments":""}— nostatusinteractionsStepStartToResponses: added-item template — nostatus(OpenAI sendsin_progress)responsesCompletedOutputItem:function_calloutput inresponse.completed— nostatusresponsesCompletedEvent:"usage":{}template; alwaysresponse.completed/status: "completed"interactions_openai_responses_request.goL60–L68:max_output_tokensmapping is Antigravity-onlyExample B — opencode,
/v1/chat/completionsSame kind of task (one large
write/edittool call). opencode's log for the failing turns ends with the tool never resolving:and the UI shows the edit as interrupted. Small edits on the same model succeed.
curl against
/v1/chat/completionswith one tool andmax_tokens: 400to force the budget:Two things are wrong in this stream:
tool_calls[].index= 1 for the only tool call. The Devin executor numbers interactions steps with one running counter acrossthought→model_output→function_call, and the chat translator writes that step index straight intotool_calls[].index(and into thecall_%dfallback id). OpenAI definesindexas the position in thetool_callsarray — 0-based and contiguous per response. A client that assembles the array fromindexends up with[null, {…}]; anything that readstool_calls[0]or validates contiguity breaks. This happens on every tool call that follows a thought or text step, i.e. on essentially every Devin tool call. I have not isolated a client failure caused by this alone, but it is a plain spec violation on the primary path.finish_reason: "stop"withcompletion_tokens == max_tokensand truncatedarguments. Devin's Connect-RPC response frames carry aStopReasonvarint in field 5.helps.ParseDevinFramedecodes it intoDevinFrameResult.StopReason(devin_wire.goL112, L542 — the comment there already notes2/4=stop, 10=tool_calls) butdevin_executor.gonever reads the field;interaction.completedis always emitted withstatus: "completed"(L823 streaming, L1086 non-stream). The interactions consumers therefore can only guess: chat completions emitsstop/tool_calls(L215–L217), neverlength; Responses always emitsresponse.completed; Claude emitsend_turn/tool_use, nevermax_tokens.The enum values (from the Windsurf extension's
setEnumType("StopReason", …), as documented in rsvedant/opencode-windsurf-authsrc/cloud-direct/chat.ts):1and3mean the output was cut short. Without them the client receives a tool call whoseargumentsnever becomes valid JSON and afinish_reasonthat says the model stopped on its own.@ai-sdk/openai(chat) emitstool-callonly when the accumulatedargumentsparses, so for a truncated call it emits nothing at all; opencode sees a stream that ended with a half-built tool and reportsorphaned interrupted tool. Because the budget is only exceeded by long outputs, this is why "large edits always fail while small ones work".Where
internal/translator/openai/interactions/chat-completions/openai_interactions_response.goL161 (interactionsStepStartToOpenAIChatpasses the stepindex), L245 / L255 (sjson.SetBytes(toolCall, "index", index)), L215–L217 (finish_reasonderivation)internal/runtime/executor/devin_executor.goL570 / L968 (ParseDevinFrameresult consumed,StopReasonignored), L823 / L1086 (completion payload alwayscompleted)internal/runtime/executor/helps/devin_wire.goL112, L542Steps to reproduce
Chat completions (defects 2 and 3):
Observed:
tool_calls[].index: 1,finish_reason: "stop",completion_tokens: 400,argumentsnot valid JSON.Responses (defect 1; any tool call, no truncation needed):
Observed:
response.output_item.added/.doneitems for the function_call have nostatus; fed through@ai-sdk/openai's Responses model the tool call is never emitted and the turn finishes withstop. Adding"max_output_tokens": 400to the same request has no effect on output length.Expected behavior
/v1/responses: function_call items carrystatus(in_progresson added,completedon done) so standard Responses clients accept them; a truncated response ends withresponse.incompleteandincomplete_details.reason: "max_output_tokens";max_output_tokensis honored./v1/chat/completions:tool_calls[].indexis the 0-based position within the response's tool calls; a truncated response ends withfinish_reason: "length".StopReason(1 INCOMPLETE,3 MAX_TOKENS,11 CONTENT_FILTER) is reflected in the completion payload so every downstream format (length/response.incomplete/max_tokens) can report truncation instead of a normal stop.