返回 DeepSeek-Reasonix
provider_presets_test.go
根目录 / internal / config / provider_presets_test.go
1 package config
2
3 import "testing"
4
5 func TestCuratedProviderPresetsCoverRequestedProviders(t *testing.T) {
6 wantIDs := []string{
7 "deepseek-anthropic",
8 "longcat-openai",
9 "longcat-anthropic",
10 "token-rhythm",
11 "kimi-cn",
12 "kimi-global",
13 "kimi-coding-plan",
14 "mimo-api",
15 "mimo-anthropic",
16 "mimo-token-plan-cn",
17 "mimo-token-plan-cn-anthropic",
18 "mimo-token-plan-sgp",
19 "mimo-token-plan-sgp-anthropic",
20 "mimo-token-plan-ams",
21 "mimo-token-plan-ams-anthropic",
22 "minimax-cn-api",
23 "minimax-global-api",
24 "minimax-cn-anthropic",
25 "minimax-global-anthropic",
26 "deepseek-responses",
27 "glm-cn",
28 "zai-global",
29 "glm-coding-plan-cn",
30 "glm-coding-plan-cn-anthropic",
31 "zai-coding-plan-global",
32 "zai-coding-plan-global-anthropic",
33 "opencode-go",
34 "opencode-go-anthropic",
35 "opencode-zen-anthropic",
36 "qwen-cn",
37 "qwen-global",
38 "qwen-coding-plan-cn",
39 "qwen-coding-plan-cn-anthropic",
40 "qwen-coding-plan-global",
41 "qwen-coding-plan-global-anthropic",
42 "stepfun",
43 "stepfun-anthropic",
44 "novita",
45 "gmi",
46 "vercel-ai-gateway",
47 "huggingface",
48 "nvidia",
49 "kilocode",
50 "ollama-cloud",
51 }
52 got := map[string]ProviderPreset{}
53 for _, preset := range CuratedProviderPresets() {
54 got[preset.ID] = preset
55 if preset.ID == "" || preset.Label == "" || preset.KeyEnv == "" {
56 t.Fatalf("preset has missing identity fields: %+v", preset)
57 }
58 if len(preset.Entries) == 0 {
59 t.Fatalf("preset %q has no entries", preset.ID)
60 }
61 for _, entry := range preset.Entries {
62 if entry.APIKeyEnv == "" {
63 t.Fatalf("preset %q entry %q has no api_key_env", preset.ID, entry.Name)
64 }
65 if entry.PresetID != preset.ID || entry.PresetVersion != ProviderPresetVersion {
66 t.Fatalf("preset %q entry %q metadata = %q/%d, want %q/%d", preset.ID, entry.Name, entry.PresetID, entry.PresetVersion, preset.ID, ProviderPresetVersion)
67 }
68 var cfg Config
69 if err := cfg.UpsertProvider(entry); err != nil {
70 t.Fatalf("preset %q entry %q failed validation: %v", preset.ID, entry.Name, err)
71 }
72 }
73 }
74 for _, id := range wantIDs {
75 if _, ok := got[id]; !ok {
76 t.Fatalf("missing preset %q", id)
77 }
78 }
79 }
80
81 func TestDeepSeekAnthropicPresetIsOptionalAndModelScoped(t *testing.T) {
82 preset, ok := CuratedProviderPreset("deepseek-anthropic")
83 if !ok || len(preset.Entries) != 1 {
84 t.Fatalf("DeepSeek Anthropic preset = %+v, want one entry", preset)
85 }
86 entry := preset.Entries[0]
87 if entry.Kind != "anthropic" || entry.BaseURL != deepSeekAnthropicBaseURL || entry.Default != "deepseek-v4-flash" || entry.Thinking != "enabled" || !EffectiveWebSearch(&entry) || entry.Vision || entry.APIKeyEnv != "DEEPSEEK_API_KEY" {
88 t.Fatalf("DeepSeek Anthropic preset entry = %+v", entry)
89 }
90 var cfg Config
91 if err := cfg.UpsertProvider(entry); err != nil {
92 t.Fatalf("UpsertProvider: %v", err)
93 }
94 flash, ok := cfg.ResolveModel("deepseek-anthropic/deepseek-v4-flash")
95 if !ok {
96 t.Fatal("Flash model did not resolve")
97 }
98 pro, ok := cfg.ResolveModel("deepseek-anthropic/deepseek-v4-pro")
99 if !ok {
100 t.Fatal("Pro model did not resolve")
101 }
102 if cap := EffortCapabilityForEntry(flash); cap.Default != "high" || !containsString(cap.Levels, "disabled") || !containsString(cap.Levels, "low") || !containsString(cap.Levels, "max") {
103 t.Fatalf("Flash effort capability = %+v", cap)
104 }
105 if got, err := NormalizeEffort(flash, "low"); err != nil || got != "low" {
106 t.Fatalf("Flash low effort = %q/%v, want low/nil", got, err)
107 }
108 if cap := EffortCapabilityForEntry(pro); cap.Default != "high" || containsString(cap.Levels, "low") || !containsString(cap.Levels, "max") {
109 t.Fatalf("Pro effort capability = %+v", cap)
110 }
111 }
112
113 func TestDeepSeekResponsesPresetMatchesOfficialSupport(t *testing.T) {
114 preset, ok := CuratedProviderPreset("deepseek-responses")
115 if !ok || len(preset.Entries) != 1 {
116 t.Fatalf("deepseek responses preset = %+v, found=%v", preset, ok)
117 }
118 entry := preset.Entries[0]
119 if entry.Kind != "responses" || entry.BaseURL != "https://api.deepseek.com" || entry.ResponsesMode != "stateless" {
120 t.Fatalf("deepseek responses endpoint = %+v", entry)
121 }
122 if len(entry.Models) != 1 || entry.Models[0] != "deepseek-v4-flash" || entry.Default != "deepseek-v4-flash" {
123 t.Fatalf("deepseek responses models = %v default=%q", entry.Models, entry.Default)
124 }
125 if entry.ModelsURL != "" {
126 t.Fatalf("deepseek responses models URL = %q, want static supported-model list", entry.ModelsURL)
127 }
128 if !EffectiveWebSearch(&entry) || entry.Vision || entry.VisionModels != nil {
129 t.Fatalf("deepseek responses capabilities = web_search:%t vision:%t vision_models:%v", EffectiveWebSearch(&entry), entry.Vision, entry.VisionModels)
130 }
131 }
132
133 func TestCuratedProviderPresetsDisplayOrder(t *testing.T) {
134 wantPrefix := []string{
135 "deepseek-responses",
136 "deepseek-anthropic",
137 "glm-cn",
138 "zai-global",
139 "glm-coding-plan-cn",
140 "glm-coding-plan-cn-anthropic",
141 "zai-coding-plan-global",
142 "zai-coding-plan-global-anthropic",
143 "longcat-openai",
144 "longcat-anthropic",
145 "token-rhythm",
146 "kimi-cn",
147 "kimi-global",
148 "kimi-coding-plan",
149 "minimax-cn-api",
150 "minimax-global-api",
151 "minimax-cn-anthropic",
152 "minimax-global-anthropic",
153 }
154 got := CuratedProviderPresets()
155 if len(got) < len(wantPrefix) {
156 t.Fatalf("got %d presets, want at least %d", len(got), len(wantPrefix))
157 }
158 for i, want := range wantPrefix {
159 if got[i].ID != want {
160 t.Fatalf("preset[%d] = %q, want %q", i, got[i].ID, want)
161 }
162 }
163 }
164
165 func TestTokenRhythmPresetMatchesPublicAPIIntegration(t *testing.T) {
166 preset, ok := CuratedProviderPreset("token-rhythm")
167 if !ok || len(preset.Entries) != 1 {
168 t.Fatalf("Token Rhythm preset = %+v, want one entry", preset)
169 }
170 if preset.Label != "Token Rhythm" || preset.KeyEnv != "TOKEN_RHYTHM_API_KEY" {
171 t.Fatalf("Token Rhythm identity = label %q key %q", preset.Label, preset.KeyEnv)
172 }
173 entry := preset.Entries[0]
174 if entry.Kind != "openai" || entry.BaseURL != "https://tokenrhythm.studio/v1" || entry.ModelsURL != "https://tokenrhythm.studio/v1/models" {
175 t.Fatalf("Token Rhythm endpoint mismatch: %+v", entry)
176 }
177 if entry.DefaultModel() != "deepseek-v4-flash" || !entry.HasModel("qwen3.8-max") || entry.HasModel("qwen-image-2.0") {
178 t.Fatalf("Token Rhythm chat catalog mismatch: models=%v default=%q", entry.Models, entry.DefaultModel())
179 }
180
181 var cfg Config
182 if err := cfg.UpsertProvider(entry); err != nil {
183 t.Fatalf("upsert Token Rhythm preset: %v", err)
184 }
185 deepseek, ok := cfg.ResolveModel("token-rhythm/deepseek-v4-flash")
186 if !ok || deepseek.ContextWindow != 1_000_000 || ReasoningProtocolForEntry(deepseek) != ReasoningProtocolDeepSeek {
187 t.Fatalf("Token Rhythm DeepSeek capability mismatch: %+v", deepseek)
188 }
189 kimi, ok := cfg.ResolveModel("token-rhythm/kimi-k2.7-code")
190 if !ok || kimi.ContextWindow != 256_000 || !EffectiveVision(kimi) {
191 t.Fatalf("Token Rhythm Kimi capability mismatch: %+v", kimi)
192 }
193 glm, ok := cfg.ResolveModel("token-rhythm/glm-5.1")
194 if !ok || glm.ContextWindow != 200_000 || EffectiveVision(glm) || ReasoningProtocolForEntry(glm) != ReasoningProtocolGLM {
195 t.Fatalf("Token Rhythm GLM capability mismatch: %+v", glm)
196 }
197 glmCap := EffortCapabilityForEntry(glm)
198 if !glmCap.Supported || glmCap.Default != "enabled" || !stringSlicesEqual(glmCap.Levels, []string{"auto", "enabled", "disabled"}) {
199 t.Fatalf("Token Rhythm GLM effort mismatch: %+v", glmCap)
200 }
201 flash0731, ok := cfg.ResolveModel("token-rhythm/deepseek-v4-flash-0731")
202 if !ok || ReasoningProtocolForEntry(flash0731) != ReasoningProtocolDeepSeek {
203 t.Fatalf("Token Rhythm DeepSeek 0731 protocol mismatch: %+v", flash0731)
204 }
205 flashCap := EffortCapabilityForEntry(flash0731)
206 if !flashCap.Supported || flashCap.Default != "high" || !stringSlicesEqual(flashCap.Levels, []string{"auto", "disabled", "low", "high", "max"}) {
207 t.Fatalf("Token Rhythm DeepSeek 0731 effort mismatch: %+v", flashCap)
208 }
209 }
210
211 func TestCuratedProviderPresetsStepFunUsesOfficialBaseURLs(t *testing.T) {
212 tests := []struct {
213 id string
214 kind string
215 baseURL string
216 }{
217 {
218 id: "stepfun",
219 kind: "openai",
220 baseURL: "https://api.stepfun.com/step_plan/v1",
221 },
222 {
223 id: "stepfun-anthropic",
224 kind: "anthropic",
225 baseURL: "https://api.stepfun.com/step_plan",
226 },
227 }
228 for _, tt := range tests {
229 t.Run(tt.id, func(t *testing.T) {
230 preset, ok := CuratedProviderPreset(tt.id)
231 if !ok {
232 t.Fatalf("missing preset %q", tt.id)
233 }
234 if len(preset.Entries) != 1 {
235 t.Fatalf("preset %q has %d entries, want 1", tt.id, len(preset.Entries))
236 }
237 entry := preset.Entries[0]
238 if entry.Kind != tt.kind {
239 t.Fatalf("preset %q kind = %q, want %q", tt.id, entry.Kind, tt.kind)
240 }
241 if entry.BaseURL != tt.baseURL {
242 t.Fatalf("preset %q base_url = %q, want %q", tt.id, entry.BaseURL, tt.baseURL)
243 }
244 })
245 }
246 }
247
248 func TestCuratedProviderPresetReturnsDeepCopy(t *testing.T) {
249 preset, ok := CuratedProviderPreset("minimax-cn-api")
250 if !ok {
251 t.Fatal("missing minimax-cn-api preset")
252 }
253 preset.Entries[0].Models[0] = "mutated"
254 preset.Entries[0].ExtraBody["reasoning_split"] = false
255 preset.Entries[0].PresetID = "mutated"
256
257 fresh, ok := CuratedProviderPreset("minimax-cn-api")
258 if !ok {
259 t.Fatal("missing fresh minimax-cn-api preset")
260 }
261 if got := fresh.Entries[0].Models[0]; got != "MiniMax-M3" {
262 t.Fatalf("fresh minimax first model = %q, want MiniMax-M3", got)
263 }
264 if got, _ := fresh.Entries[0].ExtraBody["reasoning_split"].(bool); !got {
265 t.Fatalf("fresh minimax reasoning_split = %v, want true", fresh.Entries[0].ExtraBody["reasoning_split"])
266 }
267 if got := fresh.Entries[0].PresetID; got != "minimax-cn-api" {
268 t.Fatalf("fresh minimax preset_id = %q, want minimax-cn-api", got)
269 }
270
271 qwen, ok := CuratedProviderPreset("qwen-cn")
272 if !ok {
273 t.Fatal("missing qwen-cn preset")
274 }
275 qwen.Entries[0].ModelOverrides["glm-5"] = ProviderModelOverride{ContextWindow: 1}
276 freshQwen, ok := CuratedProviderPreset("qwen-cn")
277 if !ok {
278 t.Fatal("missing fresh qwen-cn preset")
279 }
280 if got := freshQwen.Entries[0].ModelOverrides["glm-5"].ContextWindow; got != 202_752 {
281 t.Fatalf("fresh qwen glm-5 context window = %d, want 202752", got)
282 }
283 }
284
285 func TestCuratedProviderPresetCapabilities(t *testing.T) {
286 var cfg Config
287 for _, preset := range CuratedProviderPresets() {
288 for _, entry := range preset.Entries {
289 if err := cfg.UpsertProvider(entry); err != nil {
290 t.Fatalf("upsert preset %q: %v", preset.ID, err)
291 }
292 }
293 }
294
295 kimiCN, ok := cfg.Provider("kimi-cn")
296 if !ok {
297 t.Fatal("kimi-cn provider missing")
298 }
299 if kimiCN.DefaultModel() != "kimi-k2.7-code" || !kimiCN.HasVisionModel("kimi-k2.7-code-highspeed") || kimiCN.BalanceURL == "" {
300 t.Fatalf("kimi-cn capability mismatch: %+v", kimiCN)
301 }
302 kimiCNK3, ok := cfg.ResolveModel("kimi-cn/kimi-k3")
303 if !ok {
304 t.Fatal("kimi-cn/kimi-k3 did not resolve")
305 }
306 if !EffectiveVision(kimiCNK3) || kimiCNK3.ContextWindow != 1_048_576 ||
307 ReasoningProtocolForEntry(kimiCNK3) != ReasoningProtocolOpenAI ||
308 !stringSlicesEqual(kimiCNK3.SupportedEfforts, []string{"low", "high", "max"}) ||
309 EffectiveEffort(kimiCNK3) != "max" {
310 t.Fatalf("kimi-cn/kimi-k3 capability mismatch: %+v", kimiCNK3)
311 }
312 kimiCNK27, ok := cfg.ResolveModel("kimi-cn/kimi-k2.7-code")
313 if !ok || ReasoningProtocolForEntry(kimiCNK27) != ReasoningProtocolNone {
314 t.Fatalf("kimi-cn K2.7 reasoning protocol changed: %+v", kimiCNK27)
315 }
316 kimiGlobal, ok := cfg.Provider("kimi-global")
317 if !ok {
318 t.Fatal("kimi-global provider missing")
319 }
320 if kimiGlobal.BaseURL != "https://api.moonshot.ai/v1" || kimiGlobal.APIKeyEnv != "MOONSHOT_API_KEY" {
321 t.Fatalf("kimi-global endpoint/key mismatch: %+v", kimiGlobal)
322 }
323 kimiGlobalK3, ok := cfg.ResolveModel("kimi-global/kimi-k3")
324 if !ok || !EffectiveVision(kimiGlobalK3) || kimiGlobalK3.ContextWindow != 1_048_576 || EffectiveEffort(kimiGlobalK3) != "max" {
325 t.Fatalf("kimi-global/kimi-k3 capability mismatch: %+v", kimiGlobalK3)
326 }
327 kimiPlan, ok := cfg.Provider("kimi-coding-plan")
328 if !ok {
329 t.Fatal("kimi-coding-plan provider missing")
330 }
331 if kimiPlan.Kind != "anthropic" || kimiPlan.DefaultModel() != "kimi-for-coding" || !kimiPlan.HasVisionModel("kimi-for-coding") || kimiPlan.Thinking != "adaptive" || kimiPlan.HasModel("kimi-code") {
332 t.Fatalf("kimi-coding-plan capability mismatch: %+v", kimiPlan)
333 }
334
335 longcat, ok := cfg.ResolveModel("longcat-openai/LongCat-2.0")
336 if !ok {
337 t.Fatal("longcat-openai/LongCat-2.0 did not resolve")
338 }
339 if longcat.BaseURL != "https://api.longcat.chat/openai/v1" || longcat.ModelsURL != "https://api.longcat.chat/openai/v1/models" || longcat.APIKeyEnv != "LONGCAT_API_KEY" {
340 t.Fatalf("longcat-openai endpoint/key mismatch: %+v", longcat)
341 }
342 if longcat.ContextWindow != longCat20ContextWindow {
343 t.Fatalf("longcat-openai context_window = %d, want %d", longcat.ContextWindow, longCat20ContextWindow)
344 }
345 if cap := EffortCapabilityForEntry(longcat); !cap.Supported || cap.Default != "enabled" || !containsString(cap.Levels, "disabled") {
346 t.Fatalf("longcat-openai effort capability = %+v, want enabled/disabled", cap)
347 }
348 if price := longcat.PriceForModel("LongCat-2.0"); price == nil || price.Currency != "¥" || price.Input != 2 || price.Output != 8 || price.CacheHit != 0.04 {
349 t.Fatalf("LongCat-2.0 price = %+v, want RMB discounted pricing", price)
350 }
351 longcatAnthropic, ok := cfg.Provider("longcat-anthropic")
352 if !ok {
353 t.Fatal("longcat-anthropic provider missing")
354 }
355 if longcatAnthropic.Kind != "anthropic" || longcatAnthropic.BaseURL != "https://api.longcat.chat/anthropic" || longcatAnthropic.ModelsURL != "https://api.longcat.chat/anthropic/v1/models" || !longcatAnthropic.AuthHeader || longcatAnthropic.Thinking != "enabled" {
356 t.Fatalf("longcat-anthropic capability mismatch: %+v", longcatAnthropic)
357 }
358 if longcatAnthropic.ContextWindow != longCat20ContextWindow {
359 t.Fatalf("longcat-anthropic context_window = %d, want %d", longcatAnthropic.ContextWindow, longCat20ContextWindow)
360 }
361
362 mimo, ok := cfg.Provider("mimo-api")
363 if !ok {
364 t.Fatal("mimo-api provider missing")
365 }
366 if !mimo.NoProxy {
367 t.Fatal("mimo-api preset should bypass configured proxy for China-only endpoint")
368 }
369 if mimo.DefaultModel() != "mimo-v2.5-pro" || !mimo.HasVisionModel("mimo-v2.5") || mimo.HasVisionModel("mimo-v2.5-pro") {
370 t.Fatalf("mimo vision capability mismatch: %+v", mimo.VisionModels)
371 }
372 if price := mimo.PriceForModel("mimo-v2.5-pro"); price == nil || price.Currency != "¥" {
373 t.Fatalf("mimo-v2.5-pro price = %+v, want RMB pricing", price)
374 }
375 mimoAnthropic, ok := cfg.Provider("mimo-anthropic")
376 if !ok {
377 t.Fatal("mimo-anthropic provider missing")
378 }
379 if mimoAnthropic.Kind != "anthropic" || mimoAnthropic.BaseURL != "https://api.xiaomimimo.com/anthropic" || mimoAnthropic.Thinking != "adaptive" {
380 t.Fatalf("mimo-anthropic capability mismatch: %+v", mimoAnthropic)
381 }
382 mimoPlan, ok := cfg.Provider("mimo-token-plan-cn")
383 if !ok {
384 t.Fatal("mimo-token-plan-cn provider missing")
385 }
386 if !mimoPlan.NoProxy || mimoPlan.APIKeyEnv != "MIMO_TOKEN_PLAN_API_KEY" || !mimoPlan.HasVisionModel("mimo-v2.5") {
387 t.Fatalf("mimo-token-plan-cn capability mismatch: %+v", mimoPlan)
388 }
389 mimoSGP, ok := cfg.Provider("mimo-token-plan-sgp")
390 if !ok {
391 t.Fatal("mimo-token-plan-sgp provider missing")
392 }
393 if mimoSGP.NoProxy || mimoSGP.BaseURL != "https://token-plan-sgp.xiaomimimo.com/v1" {
394 t.Fatalf("mimo-token-plan-sgp endpoint/proxy mismatch: %+v", mimoSGP)
395 }
396 mimoPlanAnthropic, ok := cfg.Provider("mimo-token-plan-cn-anthropic")
397 if !ok {
398 t.Fatal("mimo-token-plan-cn-anthropic provider missing")
399 }
400 if mimoPlanAnthropic.Kind != "anthropic" || !mimoPlanAnthropic.NoProxy || mimoPlanAnthropic.BaseURL != "https://token-plan-cn.xiaomimimo.com/anthropic" {
401 t.Fatalf("mimo-token-plan-cn-anthropic capability mismatch: %+v", mimoPlanAnthropic)
402 }
403
404 minimax, ok := cfg.ResolveModel("minimax-cn-api/MiniMax-M3")
405 if !ok {
406 t.Fatal("minimax-cn-api/MiniMax-M3 did not resolve")
407 }
408 if cap := EffortCapabilityForEntry(minimax); !cap.Supported || cap.Default != "adaptive" || !containsString(cap.Levels, "disabled") {
409 t.Fatalf("minimax effort capability = %+v, want adaptive/disabled", cap)
410 }
411 minimaxGlobalAPI, ok := cfg.Provider("minimax-global-api")
412 if !ok {
413 t.Fatal("minimax-global-api provider missing")
414 }
415 if minimaxGlobalAPI.BaseURL != "https://api.minimax.io/v1" || !minimaxGlobalAPI.HasModel("MiniMax-M2.7-highspeed") {
416 t.Fatalf("minimax-global-api capability mismatch: %+v", minimaxGlobalAPI)
417 }
418 minimaxPlan, ok := cfg.Provider("minimax-cn-anthropic")
419 if !ok {
420 t.Fatal("minimax-cn-anthropic provider missing")
421 }
422 if minimaxPlan.Kind != "anthropic" || !minimaxPlan.AuthHeader || !minimaxPlan.HasVisionModel("MiniMax-M3") || !minimaxPlan.HasModel("MiniMax-M2.7-highspeed") {
423 t.Fatalf("minimax-cn-anthropic capability mismatch: %+v", minimaxPlan)
424 }
425 minimaxGlobal, ok := cfg.Provider("minimax-global-anthropic")
426 if !ok {
427 t.Fatal("minimax-global-anthropic provider missing")
428 }
429 if minimaxGlobal.Kind != "anthropic" || !minimaxGlobal.AuthHeader || minimaxGlobal.BaseURL != "https://api.minimax.io/anthropic" {
430 t.Fatalf("minimax-global-anthropic capability mismatch: %+v", minimaxGlobal)
431 }
432
433 glm, ok := cfg.ResolveModel("glm-cn/glm-5.2")
434 if !ok {
435 t.Fatal("glm-cn/glm-5.2 did not resolve")
436 }
437 if cap := EffortCapabilityForEntry(glm); !cap.Supported || cap.Default != "enabled" || !containsString(cap.Levels, "disabled") {
438 t.Fatalf("glm effort capability = %+v, want enabled/disabled", cap)
439 }
440 if !glm.HasVisionModel("glm-5v-turbo") {
441 t.Fatalf("glm vision capability mismatch: %+v", glm.VisionModels)
442 }
443 zaiGlobal, ok := cfg.ResolveModel("zai-global/glm-5.2")
444 if !ok {
445 t.Fatal("zai-global/glm-5.2 did not resolve")
446 }
447 if cap := EffortCapabilityForEntry(zaiGlobal); !cap.Supported || cap.Default != "enabled" {
448 t.Fatalf("zai-global effort capability = %+v, want enabled", cap)
449 }
450 glmPlanCN, ok := cfg.Provider("glm-coding-plan-cn")
451 if !ok {
452 t.Fatal("glm-coding-plan-cn provider missing")
453 }
454 if !glmPlanCN.NoProxy || glmPlanCN.DefaultModel() != "glm-5.2" || glmPlanCN.ContextWindow != 1000000 {
455 t.Fatalf("glm-coding-plan-cn capability mismatch: %+v", glmPlanCN)
456 }
457 glmPlanAnthropic, ok := cfg.Provider("glm-coding-plan-cn-anthropic")
458 if !ok {
459 t.Fatal("glm-coding-plan-cn-anthropic provider missing")
460 }
461 if glmPlanAnthropic.Kind != "anthropic" || !glmPlanAnthropic.AuthHeader || glmPlanAnthropic.DefaultModel() != "glm-5.2" || glmPlanAnthropic.ContextWindow != 1000000 {
462 t.Fatalf("glm-coding-plan-cn-anthropic capability mismatch: %+v", glmPlanAnthropic)
463 }
464 zaiPlanGlobal, ok := cfg.Provider("zai-coding-plan-global")
465 if !ok {
466 t.Fatal("zai-coding-plan-global provider missing")
467 }
468 if zaiPlanGlobal.NoProxy || zaiPlanGlobal.BaseURL != "https://api.z.ai/api/coding/paas/v4" || zaiPlanGlobal.DefaultModel() != "glm-5.2" {
469 t.Fatalf("zai-coding-plan-global capability mismatch: %+v", zaiPlanGlobal)
470 }
471 zaiPlanAnthropic, ok := cfg.Provider("zai-coding-plan-global-anthropic")
472 if !ok {
473 t.Fatal("zai-coding-plan-global-anthropic provider missing")
474 }
475 if zaiPlanAnthropic.Kind != "anthropic" || !zaiPlanAnthropic.AuthHeader || zaiPlanAnthropic.BaseURL != "https://api.z.ai/api/anthropic" || zaiPlanAnthropic.DefaultModel() != "glm-5.2" || zaiPlanAnthropic.ContextWindow != 1000000 {
476 t.Fatalf("zai-coding-plan-global-anthropic capability mismatch: %+v", zaiPlanAnthropic)
477 }
478
479 deepseek, ok := cfg.ResolveModel("opencode-go/deepseek-v4-pro")
480 if !ok {
481 t.Fatal("opencode-go/deepseek-v4-pro did not resolve")
482 }
483 if protocol := ReasoningProtocolForEntry(deepseek); protocol != ReasoningProtocolDeepSeek {
484 t.Fatalf("opencode deepseek protocol = %q, want deepseek", protocol)
485 }
486 if cap := EffortCapabilityForEntry(deepseek); !cap.Supported || cap.Default != "high" || !containsString(cap.Levels, "max") {
487 t.Fatalf("opencode deepseek effort capability = %+v, want high/max", cap)
488 }
489
490 kimi, ok := cfg.ResolveModel("opencode-go/kimi-k2.6")
491 if !ok {
492 t.Fatal("opencode-go/kimi-k2.6 did not resolve")
493 }
494 if protocol := ReasoningProtocolForEntry(kimi); protocol != ReasoningProtocolOpenAI {
495 t.Fatalf("opencode kimi protocol = %q, want openai", protocol)
496 }
497 if cap := EffortCapabilityForEntry(kimi); !cap.Supported || cap.Default != "high" || !containsString(cap.Levels, "medium") {
498 t.Fatalf("opencode kimi effort capability = %+v, want low/medium/high", cap)
499 }
500 kimiK3, ok := cfg.ResolveModel("opencode-go/kimi-k3")
501 if !ok {
502 t.Fatal("opencode-go/kimi-k3 did not resolve")
503 }
504 if protocol := ReasoningProtocolForEntry(kimiK3); protocol != ReasoningProtocolOpenAI {
505 t.Fatalf("opencode Kimi K3 protocol = %q, want openai", protocol)
506 }
507 if cap := EffortCapabilityForEntry(kimiK3); !cap.Supported || cap.Default != "max" || !containsString(cap.Levels, "high") || !containsString(cap.Levels, "max") {
508 t.Fatalf("opencode Kimi K3 effort capability = %+v, want high/max", cap)
509 }
510 for _, level := range []string{"high", "max"} {
511 if got, err := NormalizeEffort(kimiK3, level); err != nil || got != level {
512 t.Fatalf("opencode Kimi K3 /effort %s = %q, %v; want %s", level, got, err, level)
513 }
514 }
515 if kimiK3.ContextWindow != 1_048_576 || !EffectiveVision(kimiK3) {
516 t.Fatalf("opencode Kimi K3 context/vision capability mismatch: %+v", kimiK3)
517 }
518
519 plain, ok := cfg.ResolveModel("opencode-go/glm-5.2")
520 if !ok {
521 t.Fatal("opencode-go/glm-5.2 did not resolve")
522 }
523 if cap := EffortCapabilityForEntry(plain); cap.Supported {
524 t.Fatalf("opencode plain model effort capability = %+v, want unsupported without override", cap)
525 }
526 zen, ok := cfg.Provider("opencode-zen-anthropic")
527 if !ok {
528 t.Fatal("opencode-zen-anthropic provider missing")
529 }
530 if zen.Kind != "anthropic" || zen.BaseURL != "https://opencode.ai/zen" || !zen.HasModel("qwen3.6-plus") {
531 t.Fatalf("opencode-zen-anthropic capability mismatch: %+v", zen)
532 }
533 goAnthropic, ok := cfg.Provider("opencode-go-anthropic")
534 if !ok {
535 t.Fatal("opencode-go-anthropic provider missing")
536 }
537 if goAnthropic.Kind != "anthropic" || goAnthropic.BaseURL != "https://opencode.ai/zen/go" || goAnthropic.DefaultModel() != "qwen3.7-plus" || !goAnthropic.HasModel("minimax-m3") {
538 t.Fatalf("opencode-go-anthropic capability mismatch: %+v", goAnthropic)
539 }
540
541 qwenCN, ok := cfg.Provider("qwen-cn")
542 if !ok {
543 t.Fatal("qwen-cn provider missing")
544 }
545 if !qwenCN.NoProxy || qwenCN.DefaultModel() != "qwen3.7-plus" || !qwenCN.HasVisionModel("qwen3.7-plus") || !qwenCN.HasModel("qwen3.7-max") {
546 t.Fatalf("qwen-cn capability mismatch: %+v", qwenCN)
547 }
548
549 qwenGlobal, ok := cfg.Provider("qwen-global")
550 if !ok {
551 t.Fatal("qwen-global provider missing")
552 }
553 if qwenGlobal.NoProxy || qwenGlobal.BaseURL != "https://dashscope-intl.aliyuncs.com/compatible-mode/v1" {
554 t.Fatalf("qwen-global endpoint/proxy mismatch: %+v", qwenGlobal)
555 }
556 qwenPlanCN, ok := cfg.Provider("qwen-coding-plan-cn")
557 if !ok {
558 t.Fatal("qwen-coding-plan-cn provider missing")
559 }
560 if !qwenPlanCN.NoProxy || !qwenPlanCN.HasModel("qwen3.6-plus") || !qwenPlanCN.HasVisionModel("qwen3.7-plus") || qwenPlanCN.HasVisionModel("qwen3-coder-plus") {
561 t.Fatalf("qwen-coding-plan-cn capability mismatch: %+v", qwenPlanCN)
562 }
563 qwenPlanCNAnthropic, ok := cfg.Provider("qwen-coding-plan-cn-anthropic")
564 if !ok {
565 t.Fatal("qwen-coding-plan-cn-anthropic provider missing")
566 }
567 if qwenPlanCNAnthropic.Kind != "anthropic" || !qwenPlanCNAnthropic.NoProxy || qwenPlanCNAnthropic.BaseURL != "https://coding.dashscope.aliyuncs.com/apps/anthropic" {
568 t.Fatalf("qwen-coding-plan-cn-anthropic capability mismatch: %+v", qwenPlanCNAnthropic)
569 }
570 qwenPlanGlobal, ok := cfg.Provider("qwen-coding-plan-global")
571 if !ok {
572 t.Fatal("qwen-coding-plan-global provider missing")
573 }
574 if qwenPlanGlobal.NoProxy || !qwenPlanGlobal.HasModel("qwen3.6-plus") || qwenPlanGlobal.BaseURL != "https://coding-intl.dashscope.aliyuncs.com/v1" {
575 t.Fatalf("qwen-coding-plan-global capability mismatch: %+v", qwenPlanGlobal)
576 }
577 qwenProviders := []string{
578 "qwen-cn",
579 "qwen-global",
580 "qwen-coding-plan-cn",
581 "qwen-coding-plan-cn-anthropic",
582 "qwen-coding-plan-global",
583 "qwen-coding-plan-global-anthropic",
584 }
585 qwenContextWindows := map[string]int{
586 "qwen3.7-plus": 1_000_000,
587 "qwen3-coder-plus": 1_000_000,
588 "qwen3-max-2026-01-23": 262_144,
589 "qwen3-coder-next": 262_144,
590 "MiniMax-M2.5": 196_608,
591 "glm-5": 202_752,
592 "glm-4.7": 202_752,
593 "kimi-k2.5": 262_144,
594 }
595 for _, providerID := range qwenProviders {
596 for model, want := range qwenContextWindows {
597 resolved, ok := cfg.ResolveModel(providerID + "/" + model)
598 if !ok {
599 t.Fatalf("resolve %s/%s failed", providerID, model)
600 }
601 if got := resolved.ContextWindow; got != want {
602 t.Fatalf("%s/%s context window = %d, want %d", providerID, model, got, want)
603 }
604 }
605 }
606
607 gmi, ok := cfg.Provider("gmi")
608 if !ok {
609 t.Fatal("gmi provider missing")
610 }
611 if got := gmi.Headers["User-Agent"]; got != "Reasonix" {
612 t.Fatalf("gmi User-Agent header = %q, want Reasonix", got)
613 }
614 vercel, ok := cfg.Provider("vercel-ai-gateway")
615 if !ok {
616 t.Fatal("vercel-ai-gateway provider missing")
617 }
618 if vercel.Kind != "anthropic" || !vercel.AuthHeader || vercel.DefaultModel() != "anthropic/claude-sonnet-4.6" || !vercel.HasModel("moonshotai/kimi-k2.7-code") {
619 t.Fatalf("vercel-ai-gateway capability mismatch: %+v", vercel)
620 }
621
622 ollama, ok := cfg.ResolveModel("ollama-cloud/nemotron-3-nano:30b")
623 if !ok {
624 t.Fatal("ollama-cloud/nemotron-3-nano:30b did not resolve")
625 }
626 if cap := EffortCapabilityForEntry(ollama); !cap.Supported || cap.Default != "auto" || !containsString(cap.Levels, "max") || !containsString(cap.Levels, "none") {
627 t.Fatalf("ollama-cloud effort capability = %+v, want none/max", cap)
628 }
629 }
630
631 func TestCuratedProviderPresetDeepSeekReasoningProtocolScope(t *testing.T) {
632 var cfg Config
633 for _, preset := range CuratedProviderPresets() {
634 for _, entry := range preset.Entries {
635 if err := cfg.UpsertProvider(entry); err != nil {
636 t.Fatalf("upsert preset %q: %v", preset.ID, err)
637 }
638 }
639 }
640
641 tests := []struct {
642 ref string
643 want string
644 }{
645 {ref: "opencode-go/deepseek-v4-pro", want: ReasoningProtocolDeepSeek},
646 {ref: "opencode-go/deepseek-v4-flash", want: ReasoningProtocolDeepSeek},
647 {ref: "ollama-cloud/deepseek-v4-pro", want: ReasoningProtocolDeepSeek},
648 {ref: "ollama-cloud/deepseek-v4-flash", want: ReasoningProtocolDeepSeek},
649 {ref: "novita/deepseek/deepseek-v4-pro"},
650 {ref: "novita/deepseek/deepseek-v4-flash"},
651 {ref: "gmi/deepseek-ai/DeepSeek-V4-Pro"},
652 {ref: "gmi/deepseek-ai/DeepSeek-V4-Flash"},
653 {ref: "nvidia/deepseek-ai/deepseek-v4-pro"},
654 {ref: "vercel-ai-gateway/deepseek/deepseek-v4-pro"},
655 }
656 for _, tc := range tests {
657 t.Run(tc.ref, func(t *testing.T) {
658 entry, ok := cfg.ResolveModel(tc.ref)
659 if !ok {
660 t.Fatalf("ResolveModel(%q) failed", tc.ref)
661 }
662 if got := ReasoningProtocolForEntry(entry); got != tc.want {
663 t.Fatalf("ReasoningProtocolForEntry(%q) = %q, want %q", tc.ref, got, tc.want)
664 }
665 })
666 }
667 }
668
668 lines GO