diff --git a/openrouter/language_model_hooks.go b/openrouter/language_model_hooks.go index 3fea2e0cc27bcf1d7922a02bde0c43bbbc087b6d..4efb37630399e93b9d18798e4e89dc6f85c348d9 100644 --- a/openrouter/language_model_hooks.go +++ b/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 diff --git a/openrouter/provider_options.go b/openrouter/provider_options.go index add85ef43b5d5413594a484b68d8aed2995b7698..a34634c2dcf7bdfb1861776f2864e723dde7a541 100644 --- a/openrouter/provider_options.go +++ b/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"` diff --git a/providertests/openrouter_test.go b/providertests/openrouter_test.go index 1eae72e447522dd7d1d15b1e00fed9c7c2b526dc..50a58790961eb6377252e2f0ba899b4af8559708 100644 --- a/providertests/openrouter_test.go +++ b/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) { diff --git a/providertests/testdata/TestOpenRouterThinking/claude-sonnet-4-sig/thinking-streaming.yaml b/providertests/testdata/TestOpenRouterThinking/claude-sonnet-4-sig/thinking-streaming.yaml new file mode 100644 index 0000000000000000000000000000000000000000..b2733a4b7576f71b8ee7542e24d293c5fb837094 --- /dev/null +++ b/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 diff --git a/providertests/testdata/TestOpenRouterThinking/claude-sonnet-4-sig/thinking.yaml b/providertests/testdata/TestOpenRouterThinking/claude-sonnet-4-sig/thinking.yaml new file mode 100644 index 0000000000000000000000000000000000000000..2a93dfdf71641209a77cbc5c45ed5f2fd63549d7 --- /dev/null +++ b/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 + body: "\n \n\n \n\n \n\n \n\n \n\n \n\n \n{\"id\":\"gen-1758886216-kupFW3C9y5fWMmr81VGm\",\"provider\":\"Google\",\"model\":\"anthropic/claude-sonnet-4\",\"object\":\"chat.completion\",\"created\":1758886217,\"choices\":[{\"logprobs\":null,\"finish_reason\":\"tool_calls\",\"native_finish_reason\":\"tool_calls\",\"index\":0,\"message\":{\"role\":\"assistant\",\"content\":\"\",\"refusal\":null,\"reasoning\":\"The user is asking for weather information for Florence, Italy. I have a weather function available that takes a location parameter. The location is clearly specified as \\\"Florence, Italy\\\" so I have all the required parameters to make the function call.\",\"tool_calls\":[{\"id\":\"toolu_vrtx_01K9jYsQNzEo76QhQWkq8mVo\",\"index\":0,\"type\":\"function\",\"function\":{\"name\":\"weather\",\"arguments\":\"{\\\"location\\\": \\\"Florence, Italy\\\"}\"}}],\"reasoning_details\":[{\"type\":\"reasoning.text\",\"text\":\"The user is asking for weather information for Florence, Italy. I have a weather function available that takes a location parameter. The location is clearly specified as \\\"Florence, Italy\\\" so I have all the required parameters to make the function call.\",\"signature\":\"EqgDCkgICBACGAIqQH3i+EYbrEqM2caKYcx9HEwpGgOd/G7xfS5up7EL8JnSBTWrVOVt1E38nhHS1ZWlIWXp8WV+PEEguQ//7i+TSuwSDD50xEV84CjFcgGDXBoMDpRm3jMOWfDmboU3IjA4EPKoj3XsF7lZoij2vT5AOOnR3imbg9kzBUSuFk+aKZmqrssu8NcsjdXu+X0mu8kqjQINBWiHsALTlXs9Wj4C3Cr5XBzYFRKDIB5DtpvZBoIJs1KZUSwnyWO/Ea4tUKze3qhLjfbncIDF+bnUnYPeI92nfNM1KXoL8vXl3Cmgi1UAj+FlmTKTx8Tr25a6fUftd8xHOGEMEUHBELNvYx+D6ySc9FBeo0MdTWD90fJ2XOWFkfslUpcjrX2L/XmV2zNaVT0zZa5t0SRtZI1dOsULfaBZyWVrBWQDO4wQ3K2dlALatxeGdN61tOUmwugku0TwvTWu688rOpCm/qwa4c+Tp9lrRv2cGu7rHMv9XwgShXS9GK8IRk3kYSXZ5iU/ILS/s7Z/CIsPmRKR7XvJOyPrueJmVT1vpt91P1S+lTN5JBgB\",\"format\":\"anthropic-claude-v1\",\"index\":0}]}}],\"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}}}" + headers: + Content-Type: + - application/json + status: 200 OK + code: 200 + duration: 2.072194083s +- id: 1 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 772 + 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_01K9jYsQNzEo76QhQWkq8mVo","function":{"arguments":"{\"location\": \"Florence, Italy\"}","name":"weather"},"type":"function"}],"role":"assistant"},{"content":"40 C","tool_call_id":"toolu_vrtx_01K9jYsQNzEo76QhQWkq8mVo","role":"tool"}],"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 + body: "\n \n\n \n\n \n\n \n\n \n\n \n{\"id\":\"gen-1758886219-UVtefmQJ8EXWCY2hjKCu\",\"provider\":\"Google\",\"model\":\"anthropic/claude-sonnet-4\",\"object\":\"chat.completion\",\"created\":1758886220,\"choices\":[{\"logprobs\":null,\"finish_reason\":\"stop\",\"native_finish_reason\":\"stop\",\"index\":0,\"message\":{\"role\":\"assistant\",\"content\":\"The current weather in Florence, Italy is 40°C (104°F). That's quite hot! It's a very warm day in Florence.\",\"refusal\":null,\"reasoning\":null}}],\"usage\":{\"prompt_tokens\":463,\"completion_tokens\":34,\"total_tokens\":497,\"cost\":0.001899,\"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.00051},\"completion_tokens_details\":{\"reasoning_tokens\":0,\"image_tokens\":0}}}" + headers: + Content-Type: + - application/json + status: 200 OK + code: 200 + duration: 2.108739458s