Skip to content

Assistant messages in history replay are missing tool_calls field, breaking OpenAI-compatible providers #24090

@supernovae

Description

@supernovae

When opencode replays conversation history to an OpenAI-compatible API endpoint, assistant messages that originally contained tool calls are sent with only {role: "assistant", content: "..."} — the tool_calls field is completely absent. The corresponding role: "tool" result messages are present with valid tool_call_id values, creating orphaned tool results with no matching tool call.

This breaks any OpenAI-compatible proxy, gateway, or provider that relies on the tool_callstool_call_id pairing to reconstruct the conversation structure.

Expected behavior

When the model generates a tool call and the result is returned, the next request to the API should replay the full conversation including:

{
  "role": "assistant",
  "content": "I'll explore the project structure.",
  "tool_calls": [
    {
      "id": "tool_rvzW22DjYhyE1u7kAvtxJEkP",
      "type": "function",
      "function": {
        "name": "bash",
        "arguments": "{\"cmd\": \"ls -la\"}"
      }
    }
  ]
}

followed by:

{
  "role": "tool",
  "tool_call_id": "tool_rvzW22DjYhyE1u7kAvtxJEkP",
  "content": "total 88\ndrwxr-xr-x@ 10 bymiller staff 320 Apr 21 19:20 .\n..."
}

Actual behavior

The assistant message is sent without tool_calls:

{
  "role": "assistant",
  "content": "I'll start by exploring the project structure and examining the key files."
}

The tool result message IS correctly sent:

{
  "role": "tool",
  "tool_call_id": "tool_rvzW22DjYhyE1u7kAvtxJEkP",
  "content": "/Users/bymiller/src/task\ntotal 88\ndrwxr-xr-x@ 10 bymiller staff 320 Apr 21 19:20 .\n..."
}

Diagnostic evidence

I added logging at the entry point of our OpenAI-compatible proxy to inspect the raw messages from opencode. Here is exactly what we see:

Assistant messages (sampled first 3):

[
  {
    "keys": ["role", "content"],
    "hasToolCalls": false,
    "hasFunctionCall": false,
    "contentType": "string",
    "contentIsArray": false,
    "contentSnippet": "I'll start by exploring the project structure and examining the key files."
  },
  {
    "keys": ["role", "content"],
    "hasToolCalls": false,
    "hasFunctionCall": false,
    "contentType": "string",
    "contentIsArray": false,
    "contentSnippet": ""
  },
  {
    "keys": ["role", "content"],
    "hasToolCalls": false,
    "hasFunctionCall": false,
    "contentType": "string",
    "contentIsArray": false,
    "contentSnippet": "\n\n[Upstream provider error — retrying may help]"
  }
]

Tool result messages (sampled first 2):

[
  {
    "keys": ["role", "content", "tool_call_id"],
    "tool_call_id": "tool_rvzW22DjYhyE1u7kAvtxJEkP",
    "contentSnippet": "/Users/bymiller/src/task\ntotal 88\ndrwxr-xr-x@ 10 bymiller  staff    320 Apr 21 19:20 ."
  },
  {
    "keys": ["role", "content", "tool_call_id"],
    "tool_call_id": "tool_iGCywJow7YLARoc0Z7BzJDxL",
    "contentSnippet": "The glob tool was called with invalid arguments: [..."
  }
]

Aggregate counts across the full request (192 messages):

role counts:        system=1, user=6, assistant=66, tool=119
assistant with tool_calls: 0    ← should be ~40-50
tool messages:      119         ← all orphaned, no matching call
tool_call IDs found: 0
tool_result IDs found: 119
orphaned results:   119         (all of them)

Every single tool result is orphaned because zero assistant messages carry the tool_calls field.

Impact

Any OpenAI-compatible backend that validates tool-call/result pair integrity will either:

  1. Strip all tool results as orphaned (what our proxy does to stay compliant with the Vercel AI SDK's AI_MissingToolResultsError validation), leaving the model with zero tool history — it then loops endlessly calling tools but never seeing results
  2. Reject the request outright as malformed

Environment

❯ opencode --version
1.14.22

  • Provider: OpenAI-compatible custom endpoint
  • Model: Any (tested with MiniMax M2.7 and Kimi)

Possible location in source

Based on the web search, opencode uses the Vercel AI SDK internally. The conversion from internal ModelMessage format (where tool calls are content: [{type: "tool-call", toolCallId, toolName, input}]) to OpenAI wire format (where tool calls should be in the tool_calls field) may be incomplete. The history replay path likely serializes assistant messages without extracting the tool_calls from the AI SDK's content array format.

PR #14456 (repairAssistantMessages) and PR #16531 (OpenAI-compatible provider layer) are related but don't appear to address this specific issue of tool_calls being absent in the outbound history replay.

Plugins

No response

OpenCode version

1.14.22

Steps to reproduce

How to reproduce

  1. Configure opencode to use a custom OpenAI-compatible endpoint (e.g. a local proxy that logs raw requests)
  2. Start a session that triggers tool use (e.g. "list the files in this directory")
  3. Let the model call a tool and receive a result
  4. On the next request (turn 2+), inspect the raw JSON body sent to the endpoint
  5. Observe that assistant messages have only role and content keys — tool_calls is missing

Screenshot and/or share link

No response

Operating System

mac os 26.0.1

Terminal

iterm2

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions