1package providertests
2
3import (
4 "net/http"
5 "os"
6 "testing"
7
8 "charm.land/fantasy"
9 "charm.land/fantasy/providers/openai"
10 "charm.land/fantasy/providers/openaicompat"
11 "charm.land/x/vcr"
12 "github.com/stretchr/testify/require"
13)
14
15func TestOpenAICompatibleCommon(t *testing.T) {
16 testCommon(t, []builderPair{
17 {"xai-grok-4-fast", builderXAIGrok4Fast, nil, nil},
18 {"xai-grok-code-fast", builderXAIGrokCodeFast, nil, nil},
19 {"groq-kimi-k2", builderGroq, nil, nil},
20 {"zai-glm-4-7-flash", builderZAIGLM47Flash, nil, nil},
21 {"huggingface-kimi-k2", builderHuggingFace, nil, nil},
22 {"llama-cpp-gpt-oss", builderLlamaCppGptOss, nil, nil},
23 {"avian-kimi", builderAvianKimi, nil, nil},
24 })
25}
26
27func TestOpenAICompatObjectGeneration(t *testing.T) {
28 testObjectGeneration(t, []builderPair{
29 {"xai-grok-4-fast", builderXAIGrok4Fast, nil, nil},
30 {"xai-grok-code-fast", builderXAIGrokCodeFast, nil, nil},
31 {"zai-glm-4-7-flash", builderZAIGLM47Flash, nil, nil},
32 })
33}
34
35func TestOpenAICompatibleThinking(t *testing.T) {
36 opts := fantasy.ProviderOptions{
37 openaicompat.Name: &openaicompat.ProviderOptions{
38 ReasoningEffort: openai.ReasoningEffortOption(openai.ReasoningEffortHigh),
39 },
40 }
41 testThinking(t, []builderPair{
42 {"xai-grok-3-mini", builderXAIGrok3Mini, opts, nil},
43 {"zai-glm-4-7-flash", builderZAIGLM47Flash, opts, nil},
44 {"llama-cpp-gpt-oss", builderLlamaCppGptOss, opts, nil},
45 {"avian-kimi", builderAvianKimi, nil, nil},
46 }, testOpenAICompatThinking)
47}
48
49func testOpenAICompatThinking(t *testing.T, result *fantasy.AgentResult) {
50 reasoningContentCount := 0
51 for _, step := range result.Steps {
52 for _, msg := range step.Messages {
53 for _, content := range msg.Content {
54 if content.GetType() == fantasy.ContentTypeReasoning {
55 reasoningContentCount += 1
56 }
57 }
58 }
59 }
60 require.Greater(t, reasoningContentCount, 0, "expected reasoning content, got none")
61}
62
63func builderXAIGrokCodeFast(t *testing.T, r *vcr.Recorder) (fantasy.LanguageModel, error) {
64 provider, err := openaicompat.New(
65 openaicompat.WithBaseURL("https://api.x.ai/v1"),
66 openaicompat.WithAPIKey(os.Getenv("FANTASY_XAI_API_KEY")),
67 openaicompat.WithHTTPClient(&http.Client{Transport: r}),
68 )
69 if err != nil {
70 return nil, err
71 }
72 return provider.LanguageModel(t.Context(), "grok-code-fast-1")
73}
74
75func builderXAIGrok4Fast(t *testing.T, r *vcr.Recorder) (fantasy.LanguageModel, error) {
76 provider, err := openaicompat.New(
77 openaicompat.WithBaseURL("https://api.x.ai/v1"),
78 openaicompat.WithAPIKey(os.Getenv("FANTASY_XAI_API_KEY")),
79 openaicompat.WithHTTPClient(&http.Client{Transport: r}),
80 )
81 if err != nil {
82 return nil, err
83 }
84 return provider.LanguageModel(t.Context(), "grok-4-fast")
85}
86
87func builderXAIGrok3Mini(t *testing.T, r *vcr.Recorder) (fantasy.LanguageModel, error) {
88 provider, err := openaicompat.New(
89 openaicompat.WithBaseURL("https://api.x.ai/v1"),
90 openaicompat.WithAPIKey(os.Getenv("FANTASY_XAI_API_KEY")),
91 openaicompat.WithHTTPClient(&http.Client{Transport: r}),
92 )
93 if err != nil {
94 return nil, err
95 }
96 return provider.LanguageModel(t.Context(), "grok-3-mini")
97}
98
99func builderZAIGLM47Flash(t *testing.T, r *vcr.Recorder) (fantasy.LanguageModel, error) {
100 provider, err := openaicompat.New(
101 openaicompat.WithBaseURL("https://api.z.ai/api/coding/paas/v4"),
102 openaicompat.WithAPIKey(os.Getenv("FANTASY_ZAI_API_KEY")),
103 openaicompat.WithHTTPClient(&http.Client{Transport: r}),
104 )
105 if err != nil {
106 return nil, err
107 }
108 return provider.LanguageModel(t.Context(), "glm-4.7-flash")
109}
110
111func builderGroq(t *testing.T, r *vcr.Recorder) (fantasy.LanguageModel, error) {
112 provider, err := openaicompat.New(
113 openaicompat.WithBaseURL("https://api.groq.com/openai/v1"),
114 openaicompat.WithAPIKey(os.Getenv("FANTASY_GROQ_API_KEY")),
115 openaicompat.WithHTTPClient(&http.Client{Transport: r}),
116 )
117 if err != nil {
118 return nil, err
119 }
120 return provider.LanguageModel(t.Context(), "moonshotai/kimi-k2-instruct-0905")
121}
122
123func builderHuggingFace(t *testing.T, r *vcr.Recorder) (fantasy.LanguageModel, error) {
124 provider, err := openaicompat.New(
125 openaicompat.WithBaseURL("https://router.huggingface.co/v1"),
126 openaicompat.WithAPIKey(os.Getenv("FANTASY_HUGGINGFACE_API_KEY")),
127 openaicompat.WithHTTPClient(&http.Client{Transport: r}),
128 )
129 if err != nil {
130 return nil, err
131 }
132 return provider.LanguageModel(t.Context(), "moonshotai/Kimi-K2-Instruct-0905:groq")
133}
134
135func builderLlamaCppGptOss(t *testing.T, r *vcr.Recorder) (fantasy.LanguageModel, error) {
136 provider, err := openaicompat.New(
137 openaicompat.WithBaseURL("http://localhost:8080/v1"),
138 openaicompat.WithHTTPClient(&http.Client{Transport: r}),
139 )
140 if err != nil {
141 return nil, err
142 }
143 return provider.LanguageModel(t.Context(), "openai/gpt-oss-20b")
144}
145
146func builderAvianKimi(t *testing.T, r *vcr.Recorder) (fantasy.LanguageModel, error) {
147 provider, err := openaicompat.New(
148 openaicompat.WithBaseURL("https://api.avian.io/v1"),
149 openaicompat.WithAPIKey(os.Getenv("FANTASY_AVIAN_API_KEY")),
150 openaicompat.WithHTTPClient(&http.Client{Transport: r}),
151 )
152 if err != nil {
153 return nil, err
154 }
155 return provider.LanguageModel(t.Context(), "moonshotai/kimi-k2.5")
156}