Skip to content

a tool result with a file type other than image or PDF makes every later request in the session fail #54798

Closed

Description

@maggu2810

Description

Version: OpenCode v2.0.24 (checked against the sources at tag v2.0.24, commit e7a34f09bf)

Summary

If a tool result contains a file whose media type is not an image, a PDF, or (on the Anthropic route only) text/plain, the request builder of the active provider route rejects the whole request. The result stays in the session history, so every following prompt rebuilds the same request and fails with the same error. The session cannot continue unless the user reverts past that tool call.

Observed error (Anthropic Messages route):

provider.invalid-request: Anthropic Messages does not support media type text/x-patch

The same check exists in the OpenAI Chat builder (OpenAI Chat does not support media type …), so this is not specific to one provider.

Reproduction (from a real session)

  1. Use any model on the Anthropic Messages route.
  2. Call an MCP tool that returns attachments. In my case the tool downloaded the attachments of an issue and returned three parts: a text part, a text/x-patch file and an image/png file.
  3. The step that produced the tool result succeeds. The next model call, which sends the tool result back, fails with the error above.
  4. Every later prompt in the same session fails with the same error, because the history still contains the tool result.

The file part reaches the history as data:text/x-patch;base64,… with mime: "text/x-patch" (MCP server declares the type; core/src/tool/mcp.ts copies it unchanged into a file content item).

Where it happens

  • packages/core/src/mcp/client.ts (toCallToolResult) turns MCP image, audio and blob resource parts into media parts with the server's mimeType.
  • packages/core/src/tool/mcp.ts turns every non-text part into { type: "file", uri: "data:<mime>;base64,…", mime }.
  • packages/core/src/session/model-request.ts runs unsupportedParts(...) before building the request. Its unsupportedMedia only replaces media with an explanatory text when mimeToModality(mime) returns a modality (image/*, audio/*, video/*, application/pdf) that the model does not list in capabilities.input. For any other media type, mimeToModality returns undefined, so the part passes through unchanged.
  • packages/ai/src/protocols/anthropic-messages.ts, lowerMedia: accepts images, application/pdf and text/plain; every other type ends in invalid("Anthropic Messages does not support media type …") (line ~768). lowerToolResultContentItem routes tool-result files through this function.
  • packages/ai/src/protocols/openai-chat.ts, lowerMedia: accepts PDFs and images, otherwise invalidRequest("OpenAI Chat does not support media type …").

packages/ai/src/media.ts kindOf classifies every text/* type as document, which makes the builders' behavior look broader than it is.

Why this is a bug and not only an unsupported feature

  • A failing optional attachment aborts the whole request, including the user's own next prompt.
  • The failure is permanent for the session, because the history is the source of every later request.
  • Core already has a mechanism for this exact situation (unsupportedMedia replaces media the model cannot read with an ERROR: Cannot read … text), but it is keyed on modality and the model's capabilities.input, so it does not cover types that have no modality, and it does not know what the selected route can actually encode.
  • The tool author and the model provider are not under the user's control. An MCP server may legitimately return any media type.

Expected behavior

One of the following, in order of preference:

  1. Media that the selected route cannot lower is replaced by a short text note (as unsupportedMedia already does for known modalities), so the request succeeds and the model can tell the user the file could not be read.
  2. Textual types (text/*, application/json, and similar) are sent as text. The Anthropic builder already does this for text/plain documents.
  3. At least, the error should not poison the session: a failed lowering of a tool-result part should not fail later requests.

Suggested fix

In unsupportedMedia (or a route-aware step next to it), treat any media type that is not image/*, audio/*, video/* or application/pdf as unsupported unless the route declares support. For text/*, re-type to text/plain or inline the decoded text. A test can follow the existing one in packages/ai/test/provider/anthropic-messages.test.ts ("rejects tool-result media that cannot be lowered"), which currently pins the rejecting behavior for audio/mpeg; a core-level test should show that text/x-patch in a tool result no longer fails the request.

Workaround

A plugin can edit event.messages in the context session hook (the built-in plugins plan and warming do this) and re-type or replace the unsupported parts in tool results. It must run before the request is built. The history is not changed, so a session that already failed starts working again.

Environment

  • OpenCode v2.0.24, Linux
  • Route: @opencode/ai/providers/anthropic (Anthropic Messages) through a LiteLLM gateway
  • Tool: an MCP server tool that returns attachments (text, text/x-patch, image/png)

Plugins

mcp-atlassian

OpenCode version

v2.0.24

Steps to reproduce

  • You need mcp-atlassian
  • A Jira ticket containing a .patch patch file
  • A prompt like: Read ticket ..., especially the attached patch file and summarize it.

Screenshot and/or share link

No response

Operating System

Linux Fedora

Terminal

kitty

Activity

  1. opencode-agent commented on Oct 12, 2026

    @opencode-agent
    Contributor

    Thanks for the detailed analysis! I think this is a duplicate of #52448. In that issue, an MCP tool result with a media type the route can't encode (application/vnd.apache.parquet on OpenAI Chat) is saved in the history, and every later request in the session then fails. It's the same cause you traced: unsupportedMedia only covers types that map to a modality, so other types reach the route's lowerMedia and get rejected.

    Possible duplicates and closely related issues, ordered from most to least likely:

    The bot will close this issue as a duplicate in 1 day unless you reply explaining how your problem is different.

  2. maggu2810 commented on Oct 13, 2026

    @maggu2810
    Author

    This is not about binary but a general mime type regression (I think so).
    And easily to reproduce with the Atlassian MCP.
    Reading attachements high potentially result into non working session that has to be reverted to the previous message.

    Now I got:
    Anthropic Messages does not support media type text/markdown
    because the Jira ticket has a .md file attached.

  3. opencode-agent commented on Oct 13, 2026

    @opencode-agent
    Contributor

    Thanks for the follow-up. You're right that this goes beyond the binary-file case in #52448, so I'm keeping this issue open.

    I reproduced it on v2.0.24, and it still happens on the latest dev (4ffede4):

    1. A minimal MCP server has one tool that returns what an attachment download returns: a text part, a text/markdown resource blob and a PNG image.
    2. The model is on the Anthropic Messages route (@opencode/ai/providers/anthropic), pointed at a local mock endpoint. The mock only makes the model call the tool through Code Mode.
    3. The tool call succeeds. The next request fails with Anthropic Messages does not support media type text/markdown and never reaches the endpoint, because it fails while OpenCode builds the request.
    4. An unrelated follow-up in the same session fails with the same error.

    The script below reproduces it end to end, with no provider credentials needed.

    The rejecting check in the Anthropic request builder hasn't changed since at least v2.0.17. So rather than a recent regression, it looks like no text/* type other than text/plain has ever been handled on this route. Core only replaces unsupported media for image, audio, video and PDF types, so text/markdown and text/x-patch reach the builder unchanged. Related: #52448 (same problem on OpenAI Chat). Its open fix, #52517, only changes the OpenAI Chat adapter and does not cover the Anthropic route.

    Reproduction script (repro.sh)
    #!/usr/bin/env bash
    # Reproduction for anomalyco/opencode#54798.
    # An MCP tool returns a text/markdown resource blob. On the Anthropic Messages route,
    # the request that sends the tool result back fails locally, and so does every later turn.
    # Needs bun and opencode v2 on PATH (or OC="<command>"). No provider credentials:
    # a local mock Anthropic endpoint only scripts the model's tool calls.
    set -u
    OC=${OC:-opencode}
    W=$(mktemp -d /tmp/oc54798.XXXX); mkdir -p "$W/project"; cd "$W"
    
    cat > mcp-server.ts <<'TS'
    import { createInterface } from "node:readline"
    const send = (m: unknown) => process.stdout.write(JSON.stringify(m) + "\n")
    const png = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg=="
    const md = Buffer.from("# Notes\n\nREADME.md attached to the ticket.\n").toString("base64")
    createInterface({ input: process.stdin }).on("line", (line) => {
      const msg = JSON.parse(line)
      if (msg.id === undefined) return
      if (msg.method === "initialize")
        return send({ jsonrpc: "2.0", id: msg.id, result: { protocolVersion: msg.params.protocolVersion ?? "2025-06-18", capabilities: { tools: {} }, serverInfo: { name: "attach", version: "1.0.0" } } })
      if (msg.method === "tools/list")
        return send({ jsonrpc: "2.0", id: msg.id, result: { tools: [{ name: "get_attachments", description: "Download ticket attachments", inputSchema: { type: "object", properties: {} } }] } })
      if (msg.method === "tools/call")
        return send({ jsonrpc: "2.0", id: msg.id, result: { content: [
          { type: "text", text: "Downloaded 2 attachments" },
          { type: "resource", resource: { uri: "file:///README.md", mimeType: "text/markdown", blob: md } },
          { type: "image", data: png, mimeType: "image/png" },
        ] } })
      send({ jsonrpc: "2.0", id: msg.id, result: {} })
    })
    TS
    
    cat > mock-anthropic.ts <<'TS'
    // Mock Anthropic Messages endpoint. Asks the model to run Code Mode `execute` with the MCP tool
    // (retrying while the MCP catalog is still loading), then answers with plain text.
    let n = 0
    const sse = (events: any[]) => events.map((e) => `event: ${e.type}\ndata: ${JSON.stringify(e)}\n\n`).join("")
    Bun.serve({ port: 4555, async fetch(req) {
      const body: any = await req.json().catch(() => ({}))
      n++
      const raw = JSON.stringify(body.messages ?? [])
      const results = (raw.match(/tool_result/g) ?? []).length
      const unknown = (raw.match(/Unknown tool/g) ?? []).length
      const hasExecute = (body.tools ?? []).some((t: any) => t.name === "execute")
      console.log(`[mock] request #${n} received (tool results in history: ${results})`)
      const start = { type: "message_start", message: { id: "msg_" + n, type: "message", role: "assistant", model: "claude-test", content: [], stop_reason: null, usage: { input_tokens: 1, output_tokens: 0 } } }
      const callTool = hasExecute && results === unknown && results < 8 && !raw.includes("unrelated")
      const events = callTool
        ? [start,
           { type: "content_block_start", index: 0, content_block: { type: "tool_use", id: "toolu_" + n, name: "execute", input: {} } },
           { type: "content_block_delta", index: 0, delta: { type: "input_json_delta", partial_json: JSON.stringify({ code: "return await tools.attach.get_attachments({})" }) } },
           { type: "content_block_stop", index: 0 },
           { type: "message_delta", delta: { stop_reason: "tool_use" }, usage: { output_tokens: 1 } },
           { type: "message_stop" }]
        : [start,
           { type: "content_block_start", index: 0, content_block: { type: "text", text: "" } },
           { type: "content_block_delta", index: 0, delta: { type: "text_delta", text: "MOCK-REPLY-OK" } },
           { type: "content_block_stop", index: 0 },
           { type: "message_delta", delta: { stop_reason: "end_turn" }, usage: { output_tokens: 1 } },
           { type: "message_stop" }]
      return new Response(sse(events), { headers: { "content-type": "text/event-stream" } })
    } })
    TS
    
    cat > project/opencode.jsonc <<JSON
    {
      "model": "mockant/claude-test",
      "providers": {
        "mockant": {
          "package": "@opencode/ai/providers/anthropic",
          "settings": { "baseURL": "http://127.0.0.1:4555/v1", "apiKey": "test" },
          "models": { "claude-test": { "capabilities": { "input": ["text", "image", "pdf"] } } }
        }
      },
      "mcp": { "servers": { "attach": { "type": "local", "command": ["bun", "$W/mcp-server.ts"] } } }
    }
    JSON
    (cd project && git init -q)
    
    bun mock-anthropic.ts > mock.log 2>&1 & MOCK=$!
    trap 'kill $MOCK 2>/dev/null' EXIT
    sleep 1
    export XDG_DATA_HOME=$W/xdg/data XDG_CONFIG_HOME=$W/xdg/config XDG_STATE_HOME=$W/xdg/state XDG_CACHE_HOME=$W/xdg/cache
    cd project
    SID=ses_repro54798_$$
    echo "== $($OC --version)"
    echo "== turn 1: the model calls the MCP tool, which returns a text/markdown attachment"
    timeout 120 $OC run --standalone --auto -s $SID "Read the ticket attachments and summarize them" 2>&1 | tail -8
    echo "== turn 2: unrelated follow-up in the same session"
    timeout 90 $OC run --standalone --auto -s $SID "hello, unrelated question" 2>&1 | tail -4
    echo "== mock log (requests that failed locally never show up here)"
    cat ../mock.log

    Run it: bash repro.sh

  4. self-assigned this
    on Oct 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions