From a3b8a690c623c630ed52e688e69272643fea3c4b Mon Sep 17 00:00:00 2001 From: Andrey Nering Date: Thu, 12 Mar 2026 11:42:29 -0300 Subject: [PATCH] fix(openrouter): preserve anthropic reasoning signature in streaming MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Anthropic sends the reasoning signature chunk after tool_calls have started. The streaming hook was ending reasoning prematurely on the first tool_call chunk, so the late-arriving signature was lost. Track the reasoning format and defer ending for anthropic streams until the signature arrives. Also fix the agent stream assembler to not overwrite provider metadata with nil on reasoning deltas. 💘 Generated with Crush Assisted-by: Claude Opus 4.6 via Crush --- agent.go | 4 +++- providers/openrouter/language_model_hooks.go | 6 ++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/agent.go b/agent.go index 6d2d62dfb3f363491fde42eab4959069a66bead9..e05e7b536902fd75788db6ff1304ec4410f52e60 100644 --- a/agent.go +++ b/agent.go @@ -1240,7 +1240,9 @@ func (a *agent) processStepStream(ctx context.Context, stream StreamResponse, op case StreamPartTypeReasoningDelta: if active, exists := activeReasoningContent[part.ID]; exists { active.content += part.Delta - active.options = part.ProviderMetadata + if part.ProviderMetadata != nil { + active.options = part.ProviderMetadata + } activeReasoningContent[part.ID] = active } if opts.OnReasoningDelta != nil { diff --git a/providers/openrouter/language_model_hooks.go b/providers/openrouter/language_model_hooks.go index 5ae579d3ddb3fb226c62717f225b840807210b42..3390a075e3701413932409d587370bcf324ceb2f 100644 --- a/providers/openrouter/language_model_hooks.go +++ b/providers/openrouter/language_model_hooks.go @@ -188,6 +188,7 @@ type currentReasoningState struct { metadata *openai.ResponsesReasoningMetadata googleMetadata *google.ReasoningMetadata googleText string + format string } func extractReasoningContext(ctx map[string]any) *currentReasoningState { @@ -291,6 +292,7 @@ func languageModelStreamExtra(chunk openaisdk.ChatCompletionChunk, yield func(fa } } + currentState.format = detail.Format ctx[reasoningStartedCtx] = currentState delta := detail.Summary if strings.HasPrefix(detail.Format, "google-gemini") { @@ -304,6 +306,10 @@ func languageModelStreamExtra(chunk openaisdk.ChatCompletionChunk, yield func(fa }) } if len(reasoningData.ReasoningDetails) == 0 { + // Anthropic sends the signature after tool_calls, so don't end reasoning early + if strings.HasPrefix(currentState.format, "anthropic-claude") { + return ctx, true + } // this means its a model different from openai/anthropic that ended reasoning if choice.Delta.Content != "" || len(choice.Delta.ToolCalls) > 0 { ctx[reasoningStartedCtx] = nil