Repository navigation
Closed
Labels
Description
Summary
When interacting with strict OpenAI-compatible inference providers and proxies (such as OpenRouter with ox-alpha routing or strict upstream inference hosters), OpenCode (specifically OpenCode2 / V2 session execution) exhibits two related issues:
-
Non-Standard
role: "tool"Payload Formatting:- Standard OpenAI Chat Completion spec (and compliant frameworks like Pydantic AI 2.0) requires
role: "tool"messages to providecontentstrictly as a single string (e.g.{"role": "tool", "tool_call_id": "...", "content": "..."}). - OpenCode currently sends
contentas an array of content parts (e.g.[{"type": "text", "text": "..."}]) along with extraneous internal metadata keys (id,name,providerState) on the tool message object. - Strict upstream inference endpoints reject or abort this with HTTP/2 stream resets or schema validation errors.
- Standard OpenAI Chat Completion spec (and compliant frameworks like Pydantic AI 2.0) requires
-
Silent Subagent Failure on
rawFinish: "network_error":- When upstream connections drop or reset during subagent tool loop executions, the runner receives
rawFinish: "network_error". - The session state normalization maps
rawFinish: "network_error"directly tofinish: "stop". - Consequently, the subagent silently terminates its loop with empty output/state instead of raising an error or triggering a resilient retry mechanism, causing parent workflows to proceed unaware that the subagent aborted prematurely.
- When upstream connections drop or reset during subagent tool loop executions, the runner receives
Comparative Analysis: Pydantic AI 2.0 vs. OpenCode2
Pydantic AI 2.0 (Spec-Compliant OpenAI Tool Response Format)
In Pydantic AI 2.0 / OpenAI standard schema:
{
"role": "tool",
"tool_call_id": "call_123456",
"content": "{"result": "success", "data": 42}"
}contentis a serialized JSON / plain text string.- No extraneous client-internal fields (
id,name,providerState) are passed upstream.
OpenCode2 Current Behavior
In OpenCode2 request payloads sent upstream:
{
"role": "tool",
"tool_call_id": "call_123456",
"name": "read_file",
"id": "msg_01J...",
"providerState": { },
"content": [
{
"type": "text",
"text": "{"result": "success", "data": 42}"
}
]
}- Upstream inference proxies (e.g., OpenRouter / ox-alpha strict proxies) reject this payload or terminate the HTTP/2 stream when an array is encountered where a string is expected for
role: "tool".
Session State Normalization Gap (rawFinish: "network_error")
When the stream aborts or encounters a connection reset:
- Low-level provider client emits
rawFinish: "network_error". - Session message normalization defaults unhandled
rawFinishstatuses tofinish: "stop". - The subagent coordinator treats
finish: "stop"as a clean, normal model completion. - The subagent exits cleanly with 0 text chunks emitted, leaving the calling session with an empty response.
Proposed Fixes / Code Snippets
1. Tool Message Payload Normalization (Before Upstream Dispatch)
Ensure tool messages normalize content into a string and omit internal client metadata:
function normalizeToolMessage(message: Message): OpenAIMessage {
if (message.role === "tool") {
let contentStr = "";
if (typeof message.content === "string") {
contentStr = message.content;
} else if (Array.isArray(message.content)) {
contentStr = message.content
.map((part) => (typeof part === "string" ? part : part.text ?? JSON.stringify(part)))
.join("
");
} else if (message.content != null) {
contentStr = JSON.stringify(message.content);
}
return {
role: "tool",
tool_call_id: message.tool_call_id,
content: contentStr,
};
}
return message;
}2. Network Error / Abort Handling in Subagent Loop
Do not map network_error to finish: "stop". Instead, propagate the failure or trigger exponential backoff retry:
if (response.rawFinish === "network_error") {
// Option A: Explicitly fail fast so orchestrator / subagent loop can handle retry
throw new NetworkError(
`Subagent stream terminated unexpectedly with network_error. Tool call: ${toolCallId}`
);
// Option B: Set session status to error rather than clean stop
// session.status = "error";
// session.error = { type: "network_error", message: "Upstream stream reset" };
}Environment / Context
- OS: Linux (WSL2 / Ubuntu)
- OpenCode Version: OpenCode2 (v2)
- Upstream Providers: OpenRouter / strict OpenAI-compatible inference endpoints
Activity
github-actions commentedon Aug 24, 2026
This issue might be a duplicate of existing issues. Please check:
role: "tool"message content sent as an array of content parts instead of a plain string, causing strict OpenAI-compatible providers to return 400 errors.rawFinish: "network_error"/ unknown finish reason handling gap in V2 — that issue specifically requests surfacing the raw finish reason in the thrown error rather than silently mapping it tostop.yapily77 commentedon Aug 24, 2026
Cross-referencing related issues:
│ - #32613 & #30381: Document the same
role: "tool"array-of-parts 400 schema rejection on│ Xiaomi MiMo and Cloudflare Workers AI.
│ - #44092: Documents the
finish_reasonhandling gap onox-alpha.│
│ This issue (#44596) unifies the root cause across the OpenAI-compatible adapter and
│ documents the downstream subagent silent-exit behavior when
rawFinish: "network_error"│ is normalized to
"stop".