Skip to content

Using reasoning models with OpenRouter provider yields "provider error" due rewritten reasoning blocks. #14716

@simonhj

Description

@simonhj

Description

Recently opencode become neigh-on unusable with OpenRouter for me. Most sessions end in a provider error.

It essentially breaks openrouter as provider for me, so its likely something related to my setup, but I can't figure out what.

From the logs the underlying error (using the repro below) is

  {
    "type": "error",
    "error": {
      "type": "invalid_request_error",
      "message": "messages.1.content.6: `thinking` or `redacted_thinking` blocks in the latest assistant message cannot be modified. These blocks must remain as they
  were in the original response."
    },
    "request_id": "req_011CYKKYMLyQjjKY8MRfvLHk"
  }

Me and Codex did some digging and it seems that tool Parts have duplicated reasoning attached to them in the session.

The following diff solves the issue for me, but it feels a bit like treating the symptom. If its a good approach, lemme know I can turn it into a PR.

diff --git a/packages/opencode/src/session/message-v2.ts b/packages/opencode/src/session/message-v2.ts
index 178751a22..b5d41b8dd 100644
--- a/packages/opencode/src/session/message-v2.ts
+++ b/packages/opencode/src/session/message-v2.ts
@@ -488,6 +488,24 @@ export namespace MessageV2 {
   })
   export type WithParts = z.infer<typeof WithParts>

+  function metadata(input: Record<string, any> | undefined): Record<string, any> | undefined {
+    if (!input) return input
+
+    const prune = ["reasoning", "reasoning_content", "reasoning_details"]
+    const result: Record<string, any> = Object.fromEntries(
+      Object.entries(input)
+        .map(([provider, value]) => {
+          if (typeof value !== "object" || value === null || Array.isArray(value)) return [provider, value]
+          const next = Object.fromEntries(Object.entries(value).filter(([key]) => !prune.includes(key)))
+          if (Object.keys(next).length === 0) return undefined
+          return [provider, next]
+        })
+        .filter((entry): entry is [string, unknown] => !!entry),
+    )
+    if (Object.keys(result).length === 0) return undefined
+    return result
+  }
+
   export function toModelMessages(input: WithParts[], model: Provider.Model): ModelMessage[] {
     const result: UIMessage[] = []
     const toolNames = new Set<string>()
@@ -608,7 +626,7 @@ export namespace MessageV2 {
             assistantMessage.parts.push({
               type: "text",
               text: part.text,
-              ...(differentModel ? {} : { providerMetadata: part.metadata }),
+              ...(differentModel ? {} : { providerMetadata: metadata(part.metadata) }),
             })
           if (part.type === "step-start")
             assistantMessage.parts.push({
@@ -645,7 +663,7 @@ export namespace MessageV2 {
                 toolCallId: part.callID,
                 input: part.state.input,
                 output,
-                ...(differentModel ? {} : { callProviderMetadata: part.metadata }),
+                ...(differentModel ? {} : { callProviderMetadata: metadata(part.metadata) }),
               })
             }
             if (part.state.status === "error")
@@ -655,7 +673,7 @@ export namespace MessageV2 {
                 toolCallId: part.callID,
                 input: part.state.input,
                 errorText: part.state.error,
-                ...(differentModel ? {} : { callProviderMetadata: part.metadata }),
+                ...(differentModel ? {} : { callProviderMetadata: metadata(part.metadata) }),
               })
             // Handle pending/running tool calls to prevent dangling tool_use blocks
             // Anthropic/Claude APIs require every tool_use to have a corresponding tool_result
@@ -666,7 +684,7 @@ export namespace MessageV2 {
                 toolCallId: part.callID,
                 input: part.state.input,
                 errorText: "[Tool execution was interrupted]",
-                ...(differentModel ? {} : { callProviderMetadata: part.metadata }),
+                ...(differentModel ? {} : { callProviderMetadata: metadata(part.metadata) }),
               })
           }

Plugins

None

OpenCode version

1.2.10

Steps to reproduce

In a git clone of the opencode repo the following repros the bug for me:

opencode run -m "openrouter/anthropic/claude-opus-4.6" "Read-only investigation. Use at least 10 tool calls (glob, grep, read) across packages
/opencode/src/session and packages/opencode/src/provider. Find every place where reasoning or provider metadata might be rewritten, dropped, trimmed, or remapped. Return a compact table with file, function, and risk. Do not edit files." --thinking true --variant high

Screenshot and/or share link

No response

Operating System

WSL2 on windows 11

Terminal

Windows Terminal

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingcoreAnything pertaining to core functionality of the application (opencode server stuff)windows

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