1package providertests
2
3import (
4 "net/http"
5 "os"
6 "testing"
7
8 "charm.land/fantasy"
9 "charm.land/fantasy/providers/anthropic"
10 "charm.land/fantasy/providers/openrouter"
11 "charm.land/x/vcr"
12 "github.com/stretchr/testify/require"
13)
14
15var openrouterTestModels = []testModel{
16 {"anthropic", "anthropic/claude-haiku-4.5", false},
17 {"openai", "openai/gpt-5.4-nano", false},
18 {"gemini", "google/gemini-3.1-flash-lite-preview", false},
19 {"grok", "x-ai/grok-4.1-fast", false},
20 {"glm", "z-ai/glm-5-turbo", false},
21 {"kimi", "moonshotai/kimi-k2.5", false},
22}
23
24func TestOpenRouterCommon(t *testing.T) {
25 var pairs []builderPair
26 for _, m := range openrouterTestModels {
27 pairs = append(pairs, builderPair{m.name, openrouterBuilder(m.model), nil, nil})
28 }
29 testCommon(t, pairs)
30}
31
32func TestOpenRouterCommonWithAnthropicCache(t *testing.T) {
33 testCommon(t, []builderPair{
34 {"anthropic", openrouterBuilder("anthropic/claude-haiku-4.5"), nil, addAnthropicCaching},
35 })
36}
37
38func TestOpenRouterThinking(t *testing.T) {
39 opts := fantasy.ProviderOptions{
40 openrouter.Name: &openrouter.ProviderOptions{
41 Reasoning: &openrouter.ReasoningOptions{
42 Effort: openrouter.ReasoningEffortOption(openrouter.ReasoningEffortMedium),
43 },
44 },
45 }
46
47 var pairs []builderPair
48 for _, m := range openrouterTestModels {
49 if !m.reasoning {
50 continue
51 }
52 pairs = append(pairs, builderPair{m.name, openrouterBuilder(m.model), opts, nil})
53 }
54 testThinking(t, pairs, testOpenrouterThinking)
55
56 // test anthropic signature
57 testThinking(t, []builderPair{
58 {"anthropic", openrouterBuilder("anthropic/claude-haiku-4.5"), opts, nil},
59 }, testOpenrouterThinkingWithSignature)
60}
61
62func testOpenrouterThinkingWithSignature(t *testing.T, result *fantasy.AgentResult) {
63 reasoningContentCount := 0
64 signaturesCount := 0
65 // Test if we got the signature
66 for _, step := range result.Steps {
67 for _, msg := range step.Messages {
68 for _, content := range msg.Content {
69 if content.GetType() == fantasy.ContentTypeReasoning {
70 reasoningContentCount += 1
71 reasoningContent, ok := fantasy.AsContentType[fantasy.ReasoningPart](content)
72 if !ok {
73 continue
74 }
75 if len(reasoningContent.ProviderOptions) == 0 {
76 continue
77 }
78
79 anthropicReasoningMetadata, ok := reasoningContent.ProviderOptions[anthropic.Name]
80 if !ok {
81 continue
82 }
83 if reasoningContent.Text != "" {
84 if typed, ok := anthropicReasoningMetadata.(*anthropic.ReasoningOptionMetadata); ok {
85 require.NotEmpty(t, typed.Signature)
86 signaturesCount += 1
87 }
88 }
89 }
90 }
91 }
92 }
93 require.Greater(t, reasoningContentCount, 0)
94 require.Greater(t, signaturesCount, 0)
95 require.Equal(t, reasoningContentCount, signaturesCount)
96 // we also add the anthropic metadata so test that
97 testAnthropicThinking(t, result)
98}
99
100func testOpenrouterThinking(t *testing.T, result *fantasy.AgentResult) {
101 reasoningContentCount := 0
102 for _, step := range result.Steps {
103 for _, msg := range step.Messages {
104 for _, content := range msg.Content {
105 if content.GetType() == fantasy.ContentTypeReasoning {
106 reasoningContentCount += 1
107 }
108 }
109 }
110 }
111 require.Greater(t, reasoningContentCount, 0)
112}
113
114func openrouterBuilder(model string) builderFunc {
115 return func(t *testing.T, r *vcr.Recorder) (fantasy.LanguageModel, error) {
116 provider, err := openrouter.New(
117 openrouter.WithAPIKey(os.Getenv("FANTASY_OPENROUTER_API_KEY")),
118 openrouter.WithHTTPClient(&http.Client{Transport: r}),
119 )
120 if err != nil {
121 return nil, err
122 }
123 return provider.LanguageModel(t.Context(), model)
124 }
125}