Skip to content

[Bug]: Non-standard role: "tool" content array format and silent subagent termination on rawFinish: "network_error" #44596

Closed

Description

@yapily77

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:

  1. Non-Standard role: "tool" Payload Formatting:

    • Standard OpenAI Chat Completion spec (and compliant frameworks like Pydantic AI 2.0) requires role: "tool" messages to provide content strictly as a single string (e.g. {"role": "tool", "tool_call_id": "...", "content": "..."}).
    • OpenCode currently sends content as 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.
  2. 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 to finish: "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.

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}"
}
  • content is 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:

  1. Low-level provider client emits rawFinish: "network_error".
  2. Session message normalization defaults unhandled rawFinish statuses to finish: "stop".
  3. The subagent coordinator treats finish: "stop" as a clean, normal model completion.
  4. 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

  1. github-actions commented on Aug 24, 2026

    @github-actions
    Contributor

    This issue might be a duplicate of existing issues. Please check:

  2. yapily77 commented on Aug 24, 2026

    @yapily77
    Author

    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_reason handling gap on ox-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".

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions