1package providertests
2
3import (
4 "context"
5 "fmt"
6 "net/http"
7 "os"
8 "strconv"
9 "strings"
10 "testing"
11
12 "github.com/charmbracelet/fantasy/ai"
13 "github.com/charmbracelet/fantasy/openrouter"
14 "github.com/stretchr/testify/require"
15 "gopkg.in/dnaeon/go-vcr.v4/pkg/recorder"
16)
17
18func TestOpenRouterCommon(t *testing.T) {
19 testCommon(t, []builderPair{
20 {"kimi-k2", builderOpenRouterKimiK2, nil},
21 {"grok-code-fast-1", builderOpenRouterGrokCodeFast1, nil},
22 {"claude-sonnet-4", builderOpenRouterClaudeSonnet4, nil},
23 {"grok-4-fast-free", builderOpenRouterGrok4FastFree, nil},
24 {"gemini-2.5-flash", builderOpenRouterGemini25Flash, nil},
25 {"gemini-2.0-flash", builderOpenRouterGemini20Flash, nil},
26 {"deepseek-chat-v3.1-free", builderOpenRouterDeepseekV31Free, nil},
27 {"qwen3-235b-a22b-2507", builderOpenRouterQwen3Instruct, nil},
28 {"gpt-5", builderOpenRouterGPT5, nil},
29 {"glm-4.5", builderOpenRouterGLM45, nil},
30 })
31 opts := ai.ProviderOptions{
32 openrouter.Name: &openrouter.ProviderOptions{
33 Reasoning: &openrouter.ReasoningOptions{
34 Effort: openrouter.ReasoningEffortOption(openrouter.ReasoningEffortMedium),
35 },
36 },
37 }
38 testThinking(t, []builderPair{
39 {"gpt-5", builderOpenRouterGPT5, opts},
40 {"glm-4.5", builderOpenRouterGLM45, opts},
41 }, testOpenrouterThinking)
42}
43
44func testOpenrouterThinking(t *testing.T, result *ai.AgentResult) {
45 reasoningContentCount := 0
46 for _, step := range result.Steps {
47 for _, msg := range step.Messages {
48 for _, content := range msg.Content {
49 if content.GetType() == ai.ContentTypeReasoning {
50 reasoningContentCount += 1
51 }
52 }
53 }
54 }
55 require.Greater(t, reasoningContentCount, 0)
56}
57
58func TestWithUniqueToolCallIDs(t *testing.T) {
59 type CalculatorInput struct {
60 A int `json:"a" description:"first number"`
61 B int `json:"b" description:"second number"`
62 }
63
64 addTool := ai.NewAgentTool(
65 "add",
66 "Add two numbers",
67 func(ctx context.Context, input CalculatorInput, _ ai.ToolCall) (ai.ToolResponse, error) {
68 result := input.A + input.B
69 return ai.NewTextResponse(strings.TrimSpace(strconv.Itoa(result))), nil
70 },
71 )
72 multiplyTool := ai.NewAgentTool(
73 "multiply",
74 "Multiply two numbers",
75 func(ctx context.Context, input CalculatorInput, _ ai.ToolCall) (ai.ToolResponse, error) {
76 result := input.A * input.B
77 return ai.NewTextResponse(strings.TrimSpace(strconv.Itoa(result))), nil
78 },
79 )
80 checkResult := func(t *testing.T, result *ai.AgentResult) {
81 require.Len(t, result.Steps, 2)
82
83 var toolCalls []ai.ToolCallContent
84 for _, content := range result.Steps[0].Content {
85 if content.GetType() == ai.ContentTypeToolCall {
86 toolCalls = append(toolCalls, content.(ai.ToolCallContent))
87 }
88 }
89 for _, tc := range toolCalls {
90 require.False(t, tc.Invalid)
91 require.Contains(t, tc.ToolCallID, "test-")
92 }
93 require.Len(t, toolCalls, 2)
94
95 finalText := result.Response.Content.Text()
96 require.Contains(t, finalText, "5", "expected response to contain '5', got: %q", finalText)
97 require.Contains(t, finalText, "6", "expected response to contain '6', got: %q", finalText)
98 }
99
100 id := 0
101 generateIDFunc := func() string {
102 id += 1
103 return fmt.Sprintf("test-%d", id)
104 }
105
106 t.Run("unique tool call ids", func(t *testing.T) {
107 r := newRecorder(t)
108
109 provider := openrouter.New(
110 openrouter.WithAPIKey(os.Getenv("OPENROUTER_API_KEY")),
111 openrouter.WithHTTPClient(&http.Client{Transport: r}),
112 openrouter.WithLanguageUniqueToolCallIds(),
113 openrouter.WithLanguageModelGenerateIDFunc(generateIDFunc),
114 )
115 languageModel, err := provider.LanguageModel("moonshotai/kimi-k2-0905")
116 require.NoError(t, err, "failed to build language model")
117
118 agent := ai.NewAgent(
119 languageModel,
120 ai.WithSystemPrompt("You are a helpful assistant. CRITICAL: Always use both add and multiply at the same time ALWAYS."),
121 ai.WithTools(addTool),
122 ai.WithTools(multiplyTool),
123 )
124 result, err := agent.Generate(t.Context(), ai.AgentCall{
125 Prompt: "Add and multiply the number 2 and 3",
126 MaxOutputTokens: ai.IntOption(4000),
127 })
128 require.NoError(t, err, "failed to generate")
129 checkResult(t, result)
130 })
131 t.Run("stream unique tool call ids", func(t *testing.T) {
132 r := newRecorder(t)
133
134 provider := openrouter.New(
135 openrouter.WithAPIKey(os.Getenv("OPENROUTER_API_KEY")),
136 openrouter.WithHTTPClient(&http.Client{Transport: r}),
137 openrouter.WithLanguageUniqueToolCallIds(),
138 openrouter.WithLanguageModelGenerateIDFunc(generateIDFunc),
139 )
140 languageModel, err := provider.LanguageModel("moonshotai/kimi-k2-0905")
141 require.NoError(t, err, "failed to build language model")
142
143 agent := ai.NewAgent(
144 languageModel,
145 ai.WithSystemPrompt("You are a helpful assistant. Always use both add and multiply at the same time."),
146 ai.WithTools(addTool),
147 ai.WithTools(multiplyTool),
148 )
149 result, err := agent.Stream(t.Context(), ai.AgentStreamCall{
150 Prompt: "Add and multiply the number 2 and 3",
151 MaxOutputTokens: ai.IntOption(4000),
152 })
153 require.NoError(t, err, "failed to generate")
154 checkResult(t, result)
155 })
156}
157
158func builderOpenRouterKimiK2(r *recorder.Recorder) (ai.LanguageModel, error) {
159 provider := openrouter.New(
160 openrouter.WithAPIKey(os.Getenv("OPENROUTER_API_KEY")),
161 openrouter.WithHTTPClient(&http.Client{Transport: r}),
162 )
163 return provider.LanguageModel("moonshotai/kimi-k2-0905")
164}
165
166func builderOpenRouterGrokCodeFast1(r *recorder.Recorder) (ai.LanguageModel, error) {
167 provider := openrouter.New(
168 openrouter.WithAPIKey(os.Getenv("OPENROUTER_API_KEY")),
169 openrouter.WithHTTPClient(&http.Client{Transport: r}),
170 )
171 return provider.LanguageModel("x-ai/grok-code-fast-1")
172}
173
174func builderOpenRouterGrok4FastFree(r *recorder.Recorder) (ai.LanguageModel, error) {
175 provider := openrouter.New(
176 openrouter.WithAPIKey(os.Getenv("OPENROUTER_API_KEY")),
177 openrouter.WithHTTPClient(&http.Client{Transport: r}),
178 )
179 return provider.LanguageModel("x-ai/grok-4-fast:free")
180}
181
182func builderOpenRouterGemini25Flash(r *recorder.Recorder) (ai.LanguageModel, error) {
183 provider := openrouter.New(
184 openrouter.WithAPIKey(os.Getenv("OPENROUTER_API_KEY")),
185 openrouter.WithHTTPClient(&http.Client{Transport: r}),
186 )
187 return provider.LanguageModel("google/gemini-2.5-flash")
188}
189
190func builderOpenRouterGemini20Flash(r *recorder.Recorder) (ai.LanguageModel, error) {
191 provider := openrouter.New(
192 openrouter.WithAPIKey(os.Getenv("OPENROUTER_API_KEY")),
193 openrouter.WithHTTPClient(&http.Client{Transport: r}),
194 )
195 return provider.LanguageModel("google/gemini-2.0-flash-001")
196}
197
198func builderOpenRouterDeepseekV31Free(r *recorder.Recorder) (ai.LanguageModel, error) {
199 provider := openrouter.New(
200 openrouter.WithAPIKey(os.Getenv("OPENROUTER_API_KEY")),
201 openrouter.WithHTTPClient(&http.Client{Transport: r}),
202 )
203 return provider.LanguageModel("deepseek/deepseek-chat-v3.1:free")
204}
205
206func builderOpenRouterClaudeSonnet4(r *recorder.Recorder) (ai.LanguageModel, error) {
207 provider := openrouter.New(
208 openrouter.WithAPIKey(os.Getenv("OPENROUTER_API_KEY")),
209 openrouter.WithHTTPClient(&http.Client{Transport: r}),
210 )
211 return provider.LanguageModel("anthropic/claude-sonnet-4")
212}
213
214func builderOpenRouterGPT5(r *recorder.Recorder) (ai.LanguageModel, error) {
215 provider := openrouter.New(
216 openrouter.WithAPIKey(os.Getenv("OPENROUTER_API_KEY")),
217 openrouter.WithHTTPClient(&http.Client{Transport: r}),
218 )
219 return provider.LanguageModel("openai/gpt-5")
220}
221
222func builderOpenRouterGLM45(r *recorder.Recorder) (ai.LanguageModel, error) {
223 provider := openrouter.New(
224 openrouter.WithAPIKey(os.Getenv("OPENROUTER_API_KEY")),
225 openrouter.WithHTTPClient(&http.Client{Transport: r}),
226 )
227 return provider.LanguageModel("z-ai/glm-4.5")
228}
229
230func builderOpenRouterQwen3Instruct(r *recorder.Recorder) (ai.LanguageModel, error) {
231 provider := openrouter.New(
232 openrouter.WithAPIKey(os.Getenv("OPENROUTER_API_KEY")),
233 openrouter.WithHTTPClient(&http.Client{Transport: r}),
234 )
235 return provider.LanguageModel("qwen/qwen3-235b-a22b-2507")
236}