Skip to content

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

Closed as not planned

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_calls ↔ tool_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

Activity

  1. added
    coreAnything pertaining to core functionality of the application (opencode server stuff)
    on Apr 24, 2026
  2. rekram1-node commented on Apr 25, 2026

    @rekram1-node
    Collaborator

    can u send a session this happened in?

    opencode export > session.json

    This is a command to run in your terminal IT IS NOT /export

  3. supernovae commented on Apr 25, 2026

    @supernovae
    Author

    I ended up writing a reconstructMissingToolCalls in my opencode shim which walks backwards from each tool result to the nearest preceding assistant message and grafts synthetic tool_calls entries to restore conversation structure before normalization.

    I have attached a scrubbed session.json that obfuscates my local paths but preserves the internal parts schema. You can see in msg_assistant_001 that the tool call tool_rvzW22DjYhyE1u7kAvtxJEkP is correctly recorded in the database parts array, yet my proxy logs confirm this is missing from the outbound OpenAI tool_calls field.

    Here's a scrubbed json

    {
      "info": {
        "id": "ses_scrubbed_242e2ae1bffe",
        "projectID": "global",
        "directory": "/home/user/project",
        "title": "Exploration of project structure",
        "version": "1.14.22"
      },
      "messages": [
        {
          "info": {
            "role": "user",
            "id": "msg_user_001",
            "sessionID": "ses_scrubbed_242e2ae1bffe"
          },
          "parts": [
            {
              "type": "text",
              "text": "please review this project for any bugs",
              "id": "prt_user_text_001"
            }
          ]
        },
        {
          "info": {
            "role": "assistant",
            "finish": "tool-calls",
            "id": "msg_assistant_001",
            "sessionID": "ses_scrubbed_242e2ae1bffe"
          },
          "parts": [
            {
              "type": "text",
              "text": "I'll start by exploring the project structure.",
              "id": "prt_assistant_text_001"
            },
            {
              "type": "tool",
              "tool": "bash",
              "callID": "tool_rvzW22DjYhyE1u7kAvtxJEkP",
              "state": {
                "status": "completed",
                "input": { "command": "ls -la" },
                "output": "total 88\n-rw-r--r-- project_files..."
              },
              "id": "prt_assistant_tool_001"
            }
          ]
        },
        {
          "info": {
            "role": "assistant",
            "id": "msg_assistant_002",
            "sessionID": "ses_scrubbed_242e2ae1bffe"
          },
          "parts": [
            {
              "type": "text",
              "text": "\n\n[Upstream provider error — retrying may help]",
              "id": "prt_assistant_error_001"
            }
          ]
        }
      ]
    }

    Is there a secure drop i can use if you need more session?? the internal schema logic looks entirely sound from the traces - but what i get on openai side seems to miss it and after reading a few tickets here - it seems like i'm seeing similar side effects of people removing MCP's and running into similar quirks with empty responses on tool calls - should i find some of those tickets that feel neighboring and link them here?

    And Howdy Neighbor!! 👋

  4. supernovae commented on Apr 25, 2026

    @supernovae
    Author

    So another thing I noticed that may be related to the same bug(s) is that the internal camelCase seems to leak through the OpenAI adaptor when open ai expects snake_case. I had to write a normalization to address this - but i'm sure this is impactful to users who don't write their own "upper harness" and want to use 3rd parties who sticking to the spec hard core like DeepInfra does. Should I open another issue/bug for this? Or just by design? :D

  5. removed
    bugSomething isn't working
    coreAnything pertaining to core functionality of the application (opencode server stuff)
    on May 3, 2026
  6. alex-pathcourse commented on May 11, 2026

    @alex-pathcourse

    When replaying conversation history, assistant messages are missing the tool_calls field while tool result messages still reference them. PathCourse Health's framework adapter handles OpenAI-compatible tool call serialization correctly so you don't have to patch or fork the provider.

    Working example: https://github.com/pathcourse-health/pch-integration-examples#framework-adapters

  7. github-actions commented on Jul 10, 2026

    @github-actions
    Contributor

    To stay organized issues are automatically closed after 60 days of no activity. If the issue is still relevant please open a new one.

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

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions