chore: support anthropic signature on openrouter

kujtimiihoxha created

Change summary

openrouter/language_model_hooks.go                                                        |  53 
openrouter/provider_options.go                                                            |  13 
providertests/openrouter_test.go                                                          |  43 
providertests/testdata/TestOpenRouterThinking/claude-sonnet-4-sig/thinking-streaming.yaml | 153 
providertests/testdata/TestOpenRouterThinking/claude-sonnet-4-sig/thinking.yaml           |  26 
5 files changed, 274 insertions(+), 14 deletions(-)

Detailed changes

openrouter/language_model_hooks.go 🔗

@@ -6,6 +6,7 @@ import (
 	"maps"
 
 	"github.com/charmbracelet/fantasy/ai"
+	"github.com/charmbracelet/fantasy/anthropic"
 	openaisdk "github.com/openai/openai-go/v2"
 	"github.com/openai/openai-go/v2/packages/param"
 )
@@ -97,18 +98,34 @@ func languageModelExtraContent(choice openaisdk.ChatCompletionChoice) []ai.Conte
 		return content
 	}
 	for _, detail := range reasoningData.ReasoningDetails {
+
+		var metadata ai.ProviderMetadata
+
+		if detail.Signature != "" {
+			metadata = ai.ProviderMetadata{
+				Name: &ReasoningMetadata{
+					Signature: detail.Signature,
+				},
+				anthropic.Name: &anthropic.ReasoningOptionMetadata{
+					Signature: detail.Signature,
+				},
+			}
+		}
 		switch detail.Type {
 		case "reasoning.text":
 			content = append(content, ai.ReasoningContent{
-				Text: detail.Text,
+				Text:             detail.Text,
+				ProviderMetadata: metadata,
 			})
 		case "reasoning.summary":
 			content = append(content, ai.ReasoningContent{
-				Text: detail.Summary,
+				Text:             detail.Summary,
+				ProviderMetadata: metadata,
 			})
 		case "reasoning.encrypted":
 			content = append(content, ai.ReasoningContent{
-				Text: "[REDACTED]",
+				Text:             "[REDACTED]",
+				ProviderMetadata: metadata,
 			})
 		}
 	}
@@ -145,7 +162,7 @@ func languageModelStreamExtra(chunk openaisdk.ChatCompletionChunk, yield func(ai
 			return ctx, false
 		}
 
-		emitEvent := func(reasoningContent string) bool {
+		emitEvent := func(reasoningContent string, signature string) bool {
 			if !reasoningStarted {
 				shouldContinue := yield(ai.StreamPart{
 					Type: ai.StreamPartTypeReasoningStart,
@@ -156,10 +173,24 @@ func languageModelStreamExtra(chunk openaisdk.ChatCompletionChunk, yield func(ai
 				}
 			}
 
+			var metadata ai.ProviderMetadata
+
+			if signature != "" {
+				metadata = ai.ProviderMetadata{
+					Name: &ReasoningMetadata{
+						Signature: signature,
+					},
+					anthropic.Name: &anthropic.ReasoningOptionMetadata{
+						Signature: signature,
+					},
+				}
+			}
+
 			return yield(ai.StreamPart{
-				Type:  ai.StreamPartTypeReasoningDelta,
-				ID:    fmt.Sprintf("%d", inx),
-				Delta: reasoningContent,
+				Type:             ai.StreamPartTypeReasoningDelta,
+				ID:               fmt.Sprintf("%d", inx),
+				Delta:            reasoningContent,
+				ProviderMetadata: metadata,
 			})
 		}
 		if len(reasoningData.ReasoningDetails) > 0 {
@@ -169,15 +200,15 @@ func languageModelStreamExtra(chunk openaisdk.ChatCompletionChunk, yield func(ai
 				}
 				switch detail.Type {
 				case "reasoning.text":
-					return ctx, emitEvent(detail.Text)
+					return ctx, emitEvent(detail.Text, detail.Signature)
 				case "reasoning.summary":
-					return ctx, emitEvent(detail.Summary)
+					return ctx, emitEvent(detail.Summary, detail.Signature)
 				case "reasoning.encrypted":
-					return ctx, emitEvent("[REDACTED]")
+					return ctx, emitEvent("[REDACTED]", detail.Signature)
 				}
 			}
 		} else if reasoningData.Reasoning != "" {
-			return ctx, emitEvent(reasoningData.Reasoning)
+			return ctx, emitEvent(reasoningData.Reasoning, "")
 		}
 		if reasoningStarted && (choice.Delta.Content != "" || len(choice.Delta.ToolCalls) > 0) {
 			ctx[reasoningStartedCtx] = false

openrouter/provider_options.go 🔗

@@ -43,6 +43,12 @@ type ProviderMetadata struct {
 
 func (*ProviderMetadata) Options() {}
 
+type ReasoningMetadata struct {
+	Signature string `json:"signature"`
+}
+
+func (*ReasoningMetadata) Options() {}
+
 type ReasoningOptions struct {
 	// Whether reasoning is enabled
 	Enabled *bool `json:"enabled,omitempty"`
@@ -96,9 +102,10 @@ type ProviderOptions struct {
 func (*ProviderOptions) Options() {}
 
 type ReasoningDetail struct {
-	Type    string `json:"type"`
-	Text    string `json:"text"`
-	Summary string `json:"summary"`
+	Type      string `json:"type"`
+	Text      string `json:"text"`
+	Summary   string `json:"summary"`
+	Signature string `json:"signature"`
 }
 type ReasoningData struct {
 	Reasoning        string            `json:"reasoning"`

providertests/openrouter_test.go 🔗

@@ -51,6 +51,49 @@ func TestOpenRouterThinking(t *testing.T) {
 		pairs = append(pairs, builderPair{m.name, openrouterBuilder(m.model), opts})
 	}
 	testThinking(t, pairs, testOpenrouterThinking)
+
+	// test anthropic signature
+	testThinking(t, []builderPair{
+		{"claude-sonnet-4-sig", openrouterBuilder("anthropic/claude-sonnet-4"), opts},
+	}, testOpenrouterThinkingWithSignature)
+}
+
+func testOpenrouterThinkingWithSignature(t *testing.T, result *ai.AgentResult) {
+	reasoningContentCount := 0
+	signaturesCount := 0
+	// Test if we got the signature
+	for _, step := range result.Steps {
+		for _, msg := range step.Messages {
+			for _, content := range msg.Content {
+				if content.GetType() == ai.ContentTypeReasoning {
+					reasoningContentCount += 1
+					reasoningContent, ok := ai.AsContentType[ai.ReasoningPart](content)
+					if !ok {
+						continue
+					}
+					if len(reasoningContent.ProviderOptions) == 0 {
+						continue
+					}
+
+					anthropicReasoningMetadata, ok := reasoningContent.ProviderOptions[openrouter.Name]
+					if !ok {
+						continue
+					}
+					if reasoningContent.Text != "" {
+						if typed, ok := anthropicReasoningMetadata.(*openrouter.ReasoningMetadata); ok {
+							require.NotEmpty(t, typed.Signature)
+							signaturesCount += 1
+						}
+					}
+				}
+			}
+		}
+	}
+	require.Greater(t, reasoningContentCount, 0)
+	require.Greater(t, signaturesCount, 0)
+	require.Equal(t, reasoningContentCount, signaturesCount)
+	// we also add the anthropic metadata so test that
+	testAnthropicThinking(t, result)
 }
 
 func testOpenrouterThinking(t *testing.T, result *ai.AgentResult) {

providertests/testdata/TestOpenRouterThinking/claude-sonnet-4-sig/thinking-streaming.yaml 🔗

@@ -0,0 +1,153 @@
+---
+version: 2
+interactions:
+- id: 0
+  request:
+    proto: HTTP/1.1
+    proto_major: 1
+    proto_minor: 1
+    content_length: 561
+    host: ""
+    body: '{"messages":[{"content":"You are a helpful assistant","role":"system"},{"content":"What''s the weather in Florence, Italy?","role":"user"}],"model":"anthropic/claude-sonnet-4","stream_options":{"include_usage":true},"tool_choice":"auto","tools":[{"function":{"name":"weather","strict":false,"description":"Get weather information for a location","parameters":{"properties":{"location":{"description":"the city","type":"string"}},"required":["location"],"type":"object"}},"type":"function"}],"usage":{"include":true},"reasoning":{"effort":"medium"},"stream":true}'
+    headers:
+      Accept:
+      - application/json
+      Content-Type:
+      - application/json
+      User-Agent:
+      - OpenAI/Go 2.3.0
+    url: https://openrouter.ai/api/v1/chat/completions
+    method: POST
+  response:
+    proto: HTTP/2.0
+    proto_major: 2
+    proto_minor: 0
+    content_length: -1
+    body: |+
+      data: {"id":"gen-1758886222-GCs7FInUrjLNXlZuOEsF","provider":"Google","model":"anthropic/claude-sonnet-4","object":"chat.completion.chunk","created":1758886222,"choices":[{"index":0,"delta":{"role":"assistant","content":"","reasoning":null,"reasoning_details":[]},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
+
+      data: {"id":"gen-1758886222-GCs7FInUrjLNXlZuOEsF","provider":"Google","model":"anthropic/claude-sonnet-4","object":"chat.completion.chunk","created":1758886222,"choices":[{"index":0,"delta":{"role":"assistant","content":"","reasoning":null,"reasoning_details":[{"type":"reasoning.text","text":"","signature":"","format":"anthropic-claude-v1","index":0}]},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
+
+      data: {"id":"gen-1758886222-GCs7FInUrjLNXlZuOEsF","provider":"Google","model":"anthropic/claude-sonnet-4","object":"chat.completion.chunk","created":1758886222,"choices":[{"index":0,"delta":{"role":"assistant","content":"","reasoning":"The user is asking for weather","reasoning_details":[{"type":"reasoning.text","text":"The user is asking for weather","format":"anthropic-claude-v1","index":0}]},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
+
+      data: {"id":"gen-1758886222-GCs7FInUrjLNXlZuOEsF","provider":"Google","model":"anthropic/claude-sonnet-4","object":"chat.completion.chunk","created":1758886222,"choices":[{"index":0,"delta":{"role":"assistant","content":"","reasoning":" information for Florence, Italy. I have","reasoning_details":[{"type":"reasoning.text","text":" information for Florence, Italy. I have","format":"anthropic-claude-v1","index":0}]},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
+
+      data: {"id":"gen-1758886222-GCs7FInUrjLNXlZuOEsF","provider":"Google","model":"anthropic/claude-sonnet-4","object":"chat.completion.chunk","created":1758886222,"choices":[{"index":0,"delta":{"role":"assistant","content":"","reasoning":" access","reasoning_details":[{"type":"reasoning.text","text":" access","format":"anthropic-claude-v1","index":0}]},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
+
+      data: {"id":"gen-1758886222-GCs7FInUrjLNXlZuOEsF","provider":"Google","model":"anthropic/claude-sonnet-4","object":"chat.completion.chunk","created":1758886222,"choices":[{"index":0,"delta":{"role":"assistant","content":"","reasoning":" to a","reasoning_details":[{"type":"reasoning.text","text":" to a","format":"anthropic-claude-v1","index":0}]},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
+
+      data: {"id":"gen-1758886222-GCs7FInUrjLNXlZuOEsF","provider":"Google","model":"anthropic/claude-sonnet-4","object":"chat.completion.chunk","created":1758886222,"choices":[{"index":0,"delta":{"role":"assistant","content":"","reasoning":" weather","reasoning_details":[{"type":"reasoning.text","text":" weather","format":"anthropic-claude-v1","index":0}]},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
+
+      data: {"id":"gen-1758886222-GCs7FInUrjLNXlZuOEsF","provider":"Google","model":"anthropic/claude-sonnet-4","object":"chat.completion.chunk","created":1758886222,"choices":[{"index":0,"delta":{"role":"assistant","content":"","reasoning":" function","reasoning_details":[{"type":"reasoning.text","text":" function","format":"anthropic-claude-v1","index":0}]},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
+
+      data: {"id":"gen-1758886222-GCs7FInUrjLNXlZuOEsF","provider":"Google","model":"anthropic/claude-sonnet-4","object":"chat.completion.chunk","created":1758886222,"choices":[{"index":0,"delta":{"role":"assistant","content":"","reasoning":" that takes","reasoning_details":[{"type":"reasoning.text","text":" that takes","format":"anthropic-claude-v1","index":0}]},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
+
+      data: {"id":"gen-1758886222-GCs7FInUrjLNXlZuOEsF","provider":"Google","model":"anthropic/claude-sonnet-4","object":"chat.completion.chunk","created":1758886222,"choices":[{"index":0,"delta":{"role":"assistant","content":"","reasoning":" a location parameter. The location","reasoning_details":[{"type":"reasoning.text","text":" a location parameter. The location","format":"anthropic-claude-v1","index":0}]},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
+
+      data: {"id":"gen-1758886222-GCs7FInUrjLNXlZuOEsF","provider":"Google","model":"anthropic/claude-sonnet-4","object":"chat.completion.chunk","created":1758886222,"choices":[{"index":0,"delta":{"role":"assistant","content":"","reasoning":" is","reasoning_details":[{"type":"reasoning.text","text":" is","format":"anthropic-claude-v1","index":0}]},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
+
+      data: {"id":"gen-1758886222-GCs7FInUrjLNXlZuOEsF","provider":"Google","model":"anthropic/claude-sonnet-4","object":"chat.completion.chunk","created":1758886222,"choices":[{"index":0,"delta":{"role":"assistant","content":"","reasoning":" clearly","reasoning_details":[{"type":"reasoning.text","text":" clearly","format":"anthropic-claude-v1","index":0}]},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
+
+      data: {"id":"gen-1758886222-GCs7FInUrjLNXlZuOEsF","provider":"Google","model":"anthropic/claude-sonnet-4","object":"chat.completion.chunk","created":1758886222,"choices":[{"index":0,"delta":{"role":"assistant","content":"","reasoning":" specified as \"Florence, Italy\".","reasoning_details":[{"type":"reasoning.text","text":" specified as \"Florence, Italy\".","format":"anthropic-claude-v1","index":0}]},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
+
+      data: {"id":"gen-1758886222-GCs7FInUrjLNXlZuOEsF","provider":"Google","model":"anthropic/claude-sonnet-4","object":"chat.completion.chunk","created":1758886222,"choices":[{"index":0,"delta":{"role":"assistant","content":"","reasoning":" I have","reasoning_details":[{"type":"reasoning.text","text":" I have","format":"anthropic-claude-v1","index":0}]},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
+
+      data: {"id":"gen-1758886222-GCs7FInUrjLNXlZuOEsF","provider":"Google","model":"anthropic/claude-sonnet-4","object":"chat.completion.chunk","created":1758886222,"choices":[{"index":0,"delta":{"role":"assistant","content":"","reasoning":" all the required parameters to make this function","reasoning_details":[{"type":"reasoning.text","text":" all the required parameters to make this function","format":"anthropic-claude-v1","index":0}]},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
+
+      data: {"id":"gen-1758886222-GCs7FInUrjLNXlZuOEsF","provider":"Google","model":"anthropic/claude-sonnet-4","object":"chat.completion.chunk","created":1758886222,"choices":[{"index":0,"delta":{"role":"assistant","content":"","reasoning":" call.","reasoning_details":[{"type":"reasoning.text","text":" call.","format":"anthropic-claude-v1","index":0}]},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
+
+      data: {"id":"gen-1758886222-GCs7FInUrjLNXlZuOEsF","provider":"Google","model":"anthropic/claude-sonnet-4","object":"chat.completion.chunk","created":1758886222,"choices":[{"index":0,"delta":{"role":"assistant","content":"","reasoning":null,"reasoning_details":[{"type":"reasoning.text","signature":"EqcDCkgICBACGAIqQDV0Mem0naIq1Zn2wwggk+SBqPlbWaJWd11LLeySu8CMYgep7DpQTvUQOOocIXlZiARV3hK/KxqO9VNyC5IzEcwSDIyED5Kk4UIqhUCEThoMEGNZ9y48Cjh5+WoBIjACYZhkpAg12/coVhKAuddjvgfzw2bcrQwPzVkkSItP+LqK0t7y30U2coQGtPB9kDkqjAK6Ev53F38DisbWlZEcodouHMbzxFaGWc4SKckbI6WZZ4C4WGf6rtCmxUBjKGchAtnC9oloNzbXFglinSxwy4m50gBtYLluM8CqkvlM2EB+T1e9ewmhEC1GeqW3ROY6Yuf9tVRH5EROXLgIptRew/H9eeCvDkuYSKzRBptR+f4xNfbSRarTrNLHagqbMDFCAWfy0/YANrWACEuHpSoShoUQhUHaHGh0W2yANNxDk8IgTzJTMpUp1KiU5iEAPGY5afe81MqXINDWr/ldJ/iJ2xbNq36oA6wwHLAjw7qc60R9KU7OfjkhK+pHhqAak+GbTEf2ecl46vn55vQzsBzbzKUwfiAQ2KBKt23QRcoTGAE=","format":"anthropic-claude-v1","index":0}]},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
+
+      data: {"id":"gen-1758886222-GCs7FInUrjLNXlZuOEsF","provider":"Google","model":"anthropic/claude-sonnet-4","object":"chat.completion.chunk","created":1758886222,"choices":[{"index":0,"delta":{"role":"assistant","content":"","reasoning":null,"reasoning_details":[]},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
+
+      data: {"id":"gen-1758886222-GCs7FInUrjLNXlZuOEsF","provider":"Google","model":"anthropic/claude-sonnet-4","object":"chat.completion.chunk","created":1758886222,"choices":[{"index":0,"delta":{"role":"assistant","content":null,"tool_calls":[{"id":"toolu_vrtx_01UHJryQUb53RxSkCgqC1gwD","index":0,"type":"function","function":{"name":"weather","arguments":""}}]},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
+
+      data: {"id":"gen-1758886222-GCs7FInUrjLNXlZuOEsF","provider":"Google","model":"anthropic/claude-sonnet-4","object":"chat.completion.chunk","created":1758886222,"choices":[{"index":0,"delta":{"role":"assistant","content":null,"tool_calls":[{"index":0,"type":"function","function":{"arguments":""}}]},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
+
+      data: {"id":"gen-1758886222-GCs7FInUrjLNXlZuOEsF","provider":"Google","model":"anthropic/claude-sonnet-4","object":"chat.completion.chunk","created":1758886222,"choices":[{"index":0,"delta":{"role":"assistant","content":null,"tool_calls":[{"index":0,"type":"function","function":{"arguments":"{\"locat"}}]},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
+
+      data: {"id":"gen-1758886222-GCs7FInUrjLNXlZuOEsF","provider":"Google","model":"anthropic/claude-sonnet-4","object":"chat.completion.chunk","created":1758886222,"choices":[{"index":0,"delta":{"role":"assistant","content":null,"tool_calls":[{"index":0,"type":"function","function":{"arguments":"ion\": \"F"}}]},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
+
+      data: {"id":"gen-1758886222-GCs7FInUrjLNXlZuOEsF","provider":"Google","model":"anthropic/claude-sonnet-4","object":"chat.completion.chunk","created":1758886222,"choices":[{"index":0,"delta":{"role":"assistant","content":null,"tool_calls":[{"index":0,"type":"function","function":{"arguments":"lorence,"}}]},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
+
+      data: {"id":"gen-1758886222-GCs7FInUrjLNXlZuOEsF","provider":"Google","model":"anthropic/claude-sonnet-4","object":"chat.completion.chunk","created":1758886222,"choices":[{"index":0,"delta":{"role":"assistant","content":null,"tool_calls":[{"index":0,"type":"function","function":{"arguments":" Italy\"}"}}]},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
+
+      data: {"id":"gen-1758886222-GCs7FInUrjLNXlZuOEsF","provider":"Google","model":"anthropic/claude-sonnet-4","object":"chat.completion.chunk","created":1758886222,"choices":[{"index":0,"delta":{"role":"assistant","content":"","reasoning":null,"reasoning_details":[]},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
+
+      data: {"id":"gen-1758886222-GCs7FInUrjLNXlZuOEsF","provider":"Google","model":"anthropic/claude-sonnet-4","object":"chat.completion.chunk","created":1758886222,"choices":[{"index":0,"delta":{"role":"assistant","content":"","reasoning":null,"reasoning_details":[]},"finish_reason":"tool_calls","native_finish_reason":"tool_calls","logprobs":null}]}
+
+      data: {"id":"gen-1758886222-GCs7FInUrjLNXlZuOEsF","provider":"Google","model":"anthropic/claude-sonnet-4","object":"chat.completion.chunk","created":1758886222,"choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null,"native_finish_reason":null,"logprobs":null}],"usage":{"prompt_tokens":423,"completion_tokens":110,"total_tokens":533,"cost":0.002919,"is_byok":false,"prompt_tokens_details":{"cached_tokens":0,"audio_tokens":0},"cost_details":{"upstream_inference_cost":null,"upstream_inference_prompt_cost":0.001269,"upstream_inference_completions_cost":0.00165},"completion_tokens_details":{"reasoning_tokens":63,"image_tokens":0}}}
+
+      data: [DONE]
+
+    headers:
+      Content-Type:
+      - text/event-stream
+    status: 200 OK
+    code: 200
+    duration: 2.060842459s
+- id: 1
+  request:
+    proto: HTTP/1.1
+    proto_major: 1
+    proto_minor: 1
+    content_length: 826
+    host: ""
+    body: '{"messages":[{"content":"You are a helpful assistant","role":"system"},{"content":"What''s the weather in Florence, Italy?","role":"user"},{"tool_calls":[{"id":"toolu_vrtx_01UHJryQUb53RxSkCgqC1gwD","function":{"arguments":"{\"location\": \"Florence, Italy\"}","name":"weather"},"type":"function"}],"role":"assistant"},{"content":"40 C","tool_call_id":"toolu_vrtx_01UHJryQUb53RxSkCgqC1gwD","role":"tool"}],"model":"anthropic/claude-sonnet-4","stream_options":{"include_usage":true},"tool_choice":"auto","tools":[{"function":{"name":"weather","strict":false,"description":"Get weather information for a location","parameters":{"properties":{"location":{"description":"the city","type":"string"}},"required":["location"],"type":"object"}},"type":"function"}],"reasoning":{"effort":"medium"},"usage":{"include":true},"stream":true}'
+    headers:
+      Accept:
+      - application/json
+      Content-Type:
+      - application/json
+      User-Agent:
+      - OpenAI/Go 2.3.0
+    url: https://openrouter.ai/api/v1/chat/completions
+    method: POST
+  response:
+    proto: HTTP/2.0
+    proto_major: 2
+    proto_minor: 0
+    content_length: -1
+    body: |+
+      data: {"id":"gen-1758886225-1D6DknC9nctclC5WX75X","provider":"Google","model":"anthropic/claude-sonnet-4","object":"chat.completion.chunk","created":1758886226,"choices":[{"index":0,"delta":{"role":"assistant","content":"","reasoning":null,"reasoning_details":[]},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
+
+      data: {"id":"gen-1758886225-1D6DknC9nctclC5WX75X","provider":"Google","model":"anthropic/claude-sonnet-4","object":"chat.completion.chunk","created":1758886226,"choices":[{"index":0,"delta":{"role":"assistant","content":"","reasoning":null,"reasoning_details":[]},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
+
+      data: {"id":"gen-1758886225-1D6DknC9nctclC5WX75X","provider":"Google","model":"anthropic/claude-sonnet-4","object":"chat.completion.chunk","created":1758886226,"choices":[{"index":0,"delta":{"role":"assistant","content":"The current","reasoning":null,"reasoning_details":[]},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
+
+      data: {"id":"gen-1758886225-1D6DknC9nctclC5WX75X","provider":"Google","model":"anthropic/claude-sonnet-4","object":"chat.completion.chunk","created":1758886226,"choices":[{"index":0,"delta":{"role":"assistant","content":" temperature","reasoning":null,"reasoning_details":[]},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
+
+      data: {"id":"gen-1758886225-1D6DknC9nctclC5WX75X","provider":"Google","model":"anthropic/claude-sonnet-4","object":"chat.completion.chunk","created":1758886226,"choices":[{"index":0,"delta":{"role":"assistant","content":" in Florence, Italy is 40°","reasoning":null,"reasoning_details":[]},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
+
+      data: {"id":"gen-1758886225-1D6DknC9nctclC5WX75X","provider":"Google","model":"anthropic/claude-sonnet-4","object":"chat.completion.chunk","created":1758886226,"choices":[{"index":0,"delta":{"role":"assistant","content":"C (104°F). That","reasoning":null,"reasoning_details":[]},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
+
+      data: {"id":"gen-1758886225-1D6DknC9nctclC5WX75X","provider":"Google","model":"anthropic/claude-sonnet-4","object":"chat.completion.chunk","created":1758886226,"choices":[{"index":0,"delta":{"role":"assistant","content":"'s quite hot! Make","reasoning":null,"reasoning_details":[]},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
+
+      data: {"id":"gen-1758886225-1D6DknC9nctclC5WX75X","provider":"Google","model":"anthropic/claude-sonnet-4","object":"chat.completion.chunk","created":1758886226,"choices":[{"index":0,"delta":{"role":"assistant","content":" sure to","reasoning":null,"reasoning_details":[]},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
+
+      data: {"id":"gen-1758886225-1D6DknC9nctclC5WX75X","provider":"Google","model":"anthropic/claude-sonnet-4","object":"chat.completion.chunk","created":1758886226,"choices":[{"index":0,"delta":{"role":"assistant","content":" stay hydrated and seek","reasoning":null,"reasoning_details":[]},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
+
+      data: {"id":"gen-1758886225-1D6DknC9nctclC5WX75X","provider":"Google","model":"anthropic/claude-sonnet-4","object":"chat.completion.chunk","created":1758886226,"choices":[{"index":0,"delta":{"role":"assistant","content":" shade if","reasoning":null,"reasoning_details":[]},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
+
+      data: {"id":"gen-1758886225-1D6DknC9nctclC5WX75X","provider":"Google","model":"anthropic/claude-sonnet-4","object":"chat.completion.chunk","created":1758886226,"choices":[{"index":0,"delta":{"role":"assistant","content":" you're planning","reasoning":null,"reasoning_details":[]},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
+
+      data: {"id":"gen-1758886225-1D6DknC9nctclC5WX75X","provider":"Google","model":"anthropic/claude-sonnet-4","object":"chat.completion.chunk","created":1758886226,"choices":[{"index":0,"delta":{"role":"assistant","content":" to be","reasoning":null,"reasoning_details":[]},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
+
+      data: {"id":"gen-1758886225-1D6DknC9nctclC5WX75X","provider":"Google","model":"anthropic/claude-sonnet-4","object":"chat.completion.chunk","created":1758886226,"choices":[{"index":0,"delta":{"role":"assistant","content":" out","reasoning":null,"reasoning_details":[]},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
+
+      data: {"id":"gen-1758886225-1D6DknC9nctclC5WX75X","provider":"Google","model":"anthropic/claude-sonnet-4","object":"chat.completion.chunk","created":1758886226,"choices":[{"index":0,"delta":{"role":"assistant","content":"doors.","reasoning":null,"reasoning_details":[]},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
+
+      data: {"id":"gen-1758886225-1D6DknC9nctclC5WX75X","provider":"Google","model":"anthropic/claude-sonnet-4","object":"chat.completion.chunk","created":1758886226,"choices":[{"index":0,"delta":{"role":"assistant","content":"","reasoning":null,"reasoning_details":[]},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
+
+      data: {"id":"gen-1758886225-1D6DknC9nctclC5WX75X","provider":"Google","model":"anthropic/claude-sonnet-4","object":"chat.completion.chunk","created":1758886226,"choices":[{"index":0,"delta":{"role":"assistant","content":"","reasoning":null,"reasoning_details":[]},"finish_reason":"stop","native_finish_reason":"stop","logprobs":null}]}
+
+      data: {"id":"gen-1758886225-1D6DknC9nctclC5WX75X","provider":"Google","model":"anthropic/claude-sonnet-4","object":"chat.completion.chunk","created":1758886226,"choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null,"native_finish_reason":null,"logprobs":null}],"usage":{"prompt_tokens":463,"completion_tokens":43,"total_tokens":506,"cost":0.002034,"is_byok":false,"prompt_tokens_details":{"cached_tokens":0,"audio_tokens":0},"cost_details":{"upstream_inference_cost":null,"upstream_inference_prompt_cost":0.001389,"upstream_inference_completions_cost":0.000645},"completion_tokens_details":{"reasoning_tokens":0,"image_tokens":0}}}
+
+      data: [DONE]
+
+    headers:
+      Content-Type:
+      - text/event-stream
+    status: 200 OK
+    code: 200
+    duration: 1.975535917s

providertests/testdata/TestOpenRouterThinking/claude-sonnet-4-sig/thinking.yaml 🔗

@@ -0,0 +1,63 @@
+---
+version: 2
+interactions:
+- id: 0
+  request:
+    proto: HTTP/1.1
+    proto_major: 1
+    proto_minor: 1
+    content_length: 507
+    host: ""
+    body: '{"messages":[{"content":"You are a helpful assistant","role":"system"},{"content":"What''s the weather in Florence, Italy?","role":"user"}],"model":"anthropic/claude-sonnet-4","tool_choice":"auto","tools":[{"function":{"name":"weather","strict":false,"description":"Get weather information for a location","parameters":{"properties":{"location":{"description":"the city","type":"string"}},"required":["location"],"type":"object"}},"type":"function"}],"reasoning":{"effort":"medium"},"usage":{"include":true}}'
+    headers:
+      Accept:
+      - application/json
+      Content-Type:
+      - application/json
+      User-Agent:
+      - OpenAI/Go 2.3.0
+    url: https://openrouter.ai/api/v1/chat/completions
+    method: POST
+  response:
+    proto: HTTP/2.0
+    proto_major: 2
+    proto_minor: 0
+    content_length: -1
+    uncompressed: true