Skip to content

session: upstream rejects with missing 'reasoning_content' on older assistant message after model/provider switch #55096

Open

Description

@CTKiet2006

Summary

A long-running session started failing with [missing_required_parameter] Missing required parameter: 'messages[100].reasoning_content' after a mid-session provider.updated/model.updated event. The upstream provider now requires reasoning_content for every earlier assistant message, but one message in history (index 100) has none, so every subsequent request in the session is rejected.

Environment

  • opencode version: 2.0.28
  • OS: Windows_NT 10.0.26200 (win32 x64)
  • Terminal: TERM=xterm-256color (TERM_PROGRAM unset)
  • Shell: C:\WINDOWS\system32\cmd.exe
  • Install/channel: release (latest)
  • Active plugins: none found in config (only an MCP server blender-mcp configured)

Reproduction

  1. Start a build session on a reasoning model from the opencode provider (session was at ~100 messages).
  2. Let a tool call get interrupted mid-turn (a Start-Sleep foreground shell command hit the 120s tool timeout and was interrupted).
  3. Shortly after, an automatic provider.updated + model.updated event fires (polling refresh, every ~5 min in this log).
  4. The next continuation request fails with the error below; retries fail the same way.

Expected Behavior

Session continues normally; either OpenCode preserves/replays reasoning_content for prior assistant messages, or it strips/normalizes history to match the active model's requirements.

Actual Behavior

Two consecutive errors in the log, then the session was only usable again after a later provider/model refresh:

timestamp=2026-10-13T09:21:43.610Z level=ERROR run=23ed6170 message="Failed to drain Session" cause="AI.Error: Upstream request failed: [missing_required_parameter] Missing required parameter: 'messages[100].reasoning_content'. This model needs the reasoning of every earlier assistant message: send back the 'reasoning_content' returned with that message."
timestamp=2026-10-13T09:21:54.968Z level=ERROR run=23ed6170 message="Failed to drain Session" (same error)

Related events right before the failure:

09:19:07 event.type=provider.updated
09:19:07 event.type=model.updated

Note: an interrupted tool call occurred at 09:19 (Tool execution interrupted for a foreground shell command), possibly leaving a truncated assistant message without reasoning content at history index 100.

Additional Context

  • Frequency: twice in one session (09:21:43 and 09:21:54), triggered consistently until the session state moved past the bad message.
  • Workaround: starting a new session avoids the issue.
  • The session had ~100 messages, so older messages were likely written by a different model/provider revision before the refresh.
  • Hypothesis: message history persisted without reasoning_content + a provider that strictly requires it for all earlier assistant messages = unrecoverable session. OpenCode could either persist reasoning content for reasoning models or sanitize history when the active model requires it.

Activity

  1. opencode-agent commented on Oct 13, 2026

    @opencode-agent
    Contributor

    Thanks for the detailed report. I was able to reproduce this on 2.0.28, and it still happens on the latest dev (4ffede4).

    What I did, using a simulated OpenAI-compatible thinking model that sends reasoning back in reasoning_content:

    1. One normal turn with reasoning and a reply.
    2. A turn where the model reasons and then starts a foreground shell command, which is interrupted ("Tool execution interrupted").
    3. A follow-up message, "continue".

    In the request for step 3, the completed turn is sent with its reasoning_content. The interrupted assistant message is sent with an empty reasoning_content, and its reasoning text is moved into the normal message content. A provider that needs the reasoning of every earlier assistant message rejects that, which matches your Missing required parameter: 'messages[N].reasoning_content' error. Because the same message is sent again on every request, the session stays broken. No provider or model switch was needed, so the provider.updated / model.updated events in your log were probably a coincidence. The interrupted tool call is what triggers it.

    The recording and screenshot below show the run, and the script below reproduces it.

    Related: #38620 is the same underlying behaviour hitting Anthropic thinking, and #54693 is an open PR in this area. Starting a new session remains the workaround for now.

    recording.mp4

    Screenshot

    Reproduction script (drive.ts)
    // Repro for #55096: an interrupted assistant turn (reasoning + shell tool call)
    // is replayed on the next request without its reasoning (reasoning_content),
    // which providers that require reasoning on every earlier assistant message reject.
    import { Effect, Stream } from "effect"
    import { appendFileSync, writeFileSync } from "node:fs"
    import { Llm, OpenCodeDriver } from "opencode-drive"
    
    const LOG = "/workspace/repro/requests.jsonl"
    writeFileSync(LOG, "")
    
    const describe = (body: any) => {
      const msgs: any[] = body?.messages ?? body?.input ?? []
      return msgs
        .map((m, i) => ({ i, m }))
        .filter(({ m }) => m.role === "assistant")
        .map(({ i, m }) => {
          const has = typeof m.reasoning_content === "string" && m.reasoning_content.length > 0
          return `messages[${i}] reasoning_content=${has ? "present" : "MISSING"}`
        })
    }
    
    export default OpenCodeDriver.use(
      {
        ...(process.env.OC_DEV ? { opencode: { dev: process.env.OC_DEV } } : {}),
        tui: { recording: true },
        tools: ["shell"],
        // Make the simulated OpenAI-compatible model behave like a DeepSeek/Kimi-style
        // thinking model on Zen: reasoning is carried in `reasoning_content`.
        config: {
          providers: {
            simulation: {
              models: { "gpt-sim-model": { compatibility: { reasoningField: "reasoning_content" } } },
            },
          },
        } as any,
      },
      ({ ui, llm, tools }) =>
        Effect.gen(function* () {
          const shells = yield* tools.control("shell")
          yield* llm.serve((request, index) => {
            appendFileSync(LOG, JSON.stringify({ index, url: request.url, body: request.body }) + "\n")
            // Turn 1: a normal completed reasoning turn (control).
            if (index === 0)
              return Stream.make(Llm.reasoning("The user says hi."), Llm.text("Hello!"))
            // Turn 2: reasoning + a foreground shell call that will be interrupted.
            if (index === 1)
              return Stream.make(
                Llm.reasoning("I should wait for the build with a sleep command."),
                Llm.toolCall({ index: 0, id: "call_sleep", name: "shell", input: { command: "Start-Sleep 600", description: "wait" } }),
                Llm.finish("tool-calls"),
              )
            const lines = describe(request.body)
            const missing = lines.some((l) => l.includes("MISSING"))
            return Stream.make(
              Llm.reasoning("Checking history."),
              Llm.text(
                (missing
                  ? "UPSTREAM WOULD REJECT: Missing required parameter reasoning_content. "
                  : "OK: every assistant message has reasoning_content. ") + lines.join("; "),
              ),
            )
          })
          yield* ui.submit("hi")
          yield* ui.waitFor("Hello!")
          yield* ui.submit("Run the build and wait for it")
          const shell = yield* shells.take("call_sleep")
          yield* shell.progress("Sleeping...\n")
          yield* Effect.sleep(1500)
          // Interrupt the turn while the foreground shell is running.
          yield* ui.press("escape")
          yield* Effect.sleep(300)
          yield* ui.press("escape")
          yield* Effect.sleep(2000)
          yield* ui.screenshot("interrupted")
          yield* ui.submit("continue")
          yield* ui.waitFor("reasoning_content", { timeout: 20000 } as any)
          yield* Effect.sleep(1000)
          yield* ui.screenshot("result")
        }),
    )

    Run it with opencode-drive: opencode-drive run ./drive.ts. It drives opencode from your PATH; set OC_DEV to the path of an OpenCode checkout to run that from source instead.

  2. JonnieDoe commented on Nov 3, 2026

    @JonnieDoe

    Same error on v2.2.5 (macOS 27.2.0 arm64, tmux/zsh, channel: latest), this time at messages[20]:

    Error: Missing required parameter: 'messages[20].reasoning_content'. This model needs the reasoning of every earlier assistant message: send back the 'reasoning_content' returned with that message. If 'reasoning_content' is not available, consider starting a new session or conversation history.
    

    Provider: custom OpenAI-compatible endpoint (reflection.ai, via @ai-sdk/openai-compatible), model Beam-1.0-501B-A23B — a thinking model served with DeepSeek-style reasoning_content semantics.
    Context:

    • No deliberate turn interruption and no mid-session model/provider switch were involved; the error appeared during ordinary chat/tool use, so the offending message may come from another path that drops reasoning_content (e.g. aborted/errored turns, compaction). Trigger unclear.
    • The session became unusable: every subsequent request failed with the same error. Starting a new session fixed it (matches the documented workaround).
    • Plugins active in the affected session: opencode-mem, @dietrichgebert/ponytail, superpowers, oh-my-opencode-slim, envsitter-guard (local), @tarquinen/opencode-dcp, @openchamber/opencode-claude.
  3. self-assigned this
    on Nov 13, 2026
  4. opencode-agent commented on Nov 13, 2026

    @opencode-agent
    Contributor

    @CTKiet2006 @JonnieDoe thanks both, and sorry this bricks the session.

    Workaround: tell OpenCode the model needs reasoning_content sent back on every earlier assistant message. Add a compatibility block to the model in opencode.jsonc:

    @CTKiet2006 (built-in opencode provider). Put in the model ID you were using:

    {
      "providers": {
        "opencode": {
          "models": {
            "<model-id>": {
              "compatibility": {
                "reasoningField": "reasoning_content",
                "requireReasoning": true
              }
            }
          }
        }
      }
    }

    @JonnieDoe (custom reflection.ai endpoint). Add it to the model you already have, or to the provider so it covers every model there:

    {
      "providers": {
        "reflection": {
          "package": "@ai-sdk/openai-compatible",
          // ...your existing settings
          "models": {
            "Beam-1.0-501B-A23B": {
              "compatibility": {
                "reasoningField": "reasoning_content",
                "requireReasoning": true
              }
            }
          }
        }
      }
    }

    With this set, OpenCode always sends reasoning_content on earlier assistant messages, even ones that have no reasoning stored.

    If it still fails after that, please send:

    • @CTKiet2006: the exact model ID, and an export (opencode export <sessionID>) or just the assistant message at the failing index, with anything sensitive removed. We mainly want to know whether that message has a reasoning part and whether it was the interrupted turn.
    • @JonnieDoe: whether it still happens with plugins disabled, especially @tarquinen/opencode-dcp, since it prunes context and could strip reasoning from old messages. Also the message at messages[20] if it happens again.
    • Both: whether the failing request had "reasoning_content": "" (an empty string) on that message or no field at all. That tells us whether the provider rejects an empty value, which decides the fix on our side.
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