fix(openrouter): preserve anthropic reasoning signature in streaming

Andrey Nering created

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 <crush@charm.land>

Change summary

agent.go                                     | 4 +++-
providers/openrouter/language_model_hooks.go | 6 ++++++
2 files changed, 9 insertions(+), 1 deletion(-)

Detailed changes

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 {

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