返回 DeepSeek-Reasonix
effort_test.go
根目录 / internal / config / effort_test.go
1 package config
2
3 import (
4 "strings"
5 "testing"
6 )
7
8 func TestDeepSeekV4FlashEffortCapabilityIncludesLow(t *testing.T) {
9 flash := &ProviderEntry{Kind: "openai", BaseURL: "https://api.deepseek.com", Model: "deepseek-v4-flash"}
10 cap := EffortCapabilityForEntry(flash)
11 want := []string{"auto", "disabled", "low", "high", "max"}
12 if !cap.Supported || len(cap.Levels) != len(want) {
13 t.Fatalf("Flash capability = %+v, want %v", cap, want)
14 }
15 for i := range want {
16 if cap.Levels[i] != want[i] {
17 t.Fatalf("Flash levels[%d] = %q, want %q", i, cap.Levels[i], want[i])
18 }
19 }
20 if cap.Default != "high" {
21 t.Fatalf("Flash default = %q, want high", cap.Default)
22 }
23 if got, err := NormalizeEffort(flash, "low"); err != nil || got != "low" {
24 t.Fatalf("Flash low = %q/%v, want low/nil", got, err)
25 }
26 if got, err := NormalizeEffort(flash, "xhigh"); err != nil || got != "high" {
27 t.Fatalf("Flash xhigh = %q/%v, want high/nil", got, err)
28 }
29 flash.ReasoningProtocol = ReasoningProtocolDeepSeek
30 if got := EffortCapabilityForEntry(flash); len(got.Levels) != len(want) || got.Levels[2] != "low" {
31 t.Fatalf("explicit DeepSeek Flash capability = %+v, want model-specific low", got)
32 }
33 if got, err := NormalizeEffort(flash, "low"); err != nil || got != "low" {
34 t.Fatalf("explicit DeepSeek Flash low = %q/%v, want low/nil", got, err)
35 }
36 if got, err := NormalizeEffort(flash, "xhigh"); err != nil || got != "high" {
37 t.Fatalf("explicit DeepSeek Flash xhigh = %q/%v, want high/nil", got, err)
38 }
39
40 pro := &ProviderEntry{Kind: "openai", BaseURL: "https://api.deepseek.com", Model: "deepseek-v4-pro"}
41 if got, err := NormalizeEffort(pro, "low"); err != nil || got != "high" {
42 t.Fatalf("Pro low = %q/%v, want existing high mapping", got, err)
43 }
44 if got, err := NormalizeEffort(pro, "xhigh"); err != nil || got != "max" {
45 t.Fatalf("Pro xhigh = %q/%v, want max/nil", got, err)
46 }
47 for _, level := range EffortCapabilityForEntry(pro).Levels {
48 if level == "low" {
49 t.Fatalf("Pro capability unexpectedly exposes low: %+v", EffortCapabilityForEntry(pro))
50 }
51 }
52 }
53
54 func TestIsMiniMaxEntry(t *testing.T) {
55 for _, tc := range []struct {
56 baseURL string
57 want bool
58 }{
59 {"https://api.minimaxi.com/v1", true},
60 {"https://api.minimaxi.com", true},
61 {"https://api.minimaxi.com/anthropic", true},
62 // Subdomain variants of the canonical host.
63 {"https://eu.minimaxi.com/v1", true},
64 {"https://us.minimaxi.com/v1", true},
65 // Bare apex (no api. prefix) is rejected: it would only match if
66 // the user pointed their base_url at the apex domain, which is a
67 // misconfiguration — not a path we want to silently accept.
68 {"https://minimaxi.com/v1", false},
69 {"https://minimaxi.com", false},
70 // Other vendors must not match.
71 {"https://api.deepseek.com", false},
72 {"https://api.xiaomimimo.com/v1", false},
73 {"https://api.minimax.example.com", false}, // wrong spelling — must not match
74 {"", false},
75 } {
76 e := &ProviderEntry{Kind: "openai", BaseURL: tc.baseURL}
77 if got := isMiniMaxEntry(e); got != tc.want {
78 t.Errorf("baseURL=%q: isMiniMaxEntry=%v, want %v", tc.baseURL, got, tc.want)
79 }
80 }
81 }
82
83 func TestIsZhipuEntry(t *testing.T) {
84 for _, tc := range []struct {
85 baseURL string
86 want bool
87 }{
88 {"https://open.bigmodel.cn/api/paas/v4", true},
89 {"https://open.bigmodel.cn", true},
90 {"https://api.bigmodel.cn/v1", true},
91 {"https://api.z.ai/api/paas/v4", true},
92 {"https://open.z.ai/v1", true},
93 // Bare apexes rejected.
94 {"https://bigmodel.cn/v1", false},
95 {"https://z.ai", false},
96 // Other vendors must not match.
97 {"https://api.deepseek.com", false},
98 {"https://api.minimaxi.com/v1", false},
99 {"", false},
100 } {
101 e := &ProviderEntry{Kind: "openai", BaseURL: tc.baseURL}
102 if got := isZhipuEntry(e); got != tc.want {
103 t.Errorf("baseURL=%q: isZhipuEntry=%v, want %v", tc.baseURL, got, tc.want)
104 }
105 }
106 }
107
108 func TestIsLongCatEntry(t *testing.T) {
109 for _, tc := range []struct {
110 baseURL string
111 want bool
112 }{
113 {"https://api.longcat.chat/openai/v1", true},
114 {"https://api.longcat.chat/anthropic", true},
115 {"https://longcat.chat/openai/v1", false},
116 {"https://api.deepseek.com", false},
117 {"", false},
118 } {
119 e := &ProviderEntry{Kind: "openai", BaseURL: tc.baseURL}
120 if got := isLongCatEntry(e); got != tc.want {
121 t.Errorf("baseURL=%q: isLongCatEntry=%v, want %v", tc.baseURL, got, tc.want)
122 }
123 }
124 }
125
126 func TestIsOllamaCloudEntry(t *testing.T) {
127 for _, tc := range []struct {
128 baseURL string
129 want bool
130 }{
131 {"https://ollama.com/v1", true},
132 {"https://api.ollama.com/v1", true},
133 {"https://localhost:11434/v1", false},
134 {"http://127.0.0.1:11434/v1", false},
135 {"https://api.openai.com/v1", false},
136 {"", false},
137 } {
138 e := &ProviderEntry{Kind: "openai", BaseURL: tc.baseURL}
139 if got := isOllamaCloudEntry(e); got != tc.want {
140 t.Errorf("baseURL=%q: isOllamaCloudEntry=%v, want %v", tc.baseURL, got, tc.want)
141 }
142 }
143 }
144
145 func TestIsMimoEntry(t *testing.T) {
146 for _, tc := range []struct {
147 baseURL string
148 want bool
149 }{
150 {"https://api.xiaomimimo.com/v1", true},
151 {"https://api.xiaomimimo.com/v1/responses", true},
152 {"https://api.deepseek.com", false},
153 {"https://dashscope.aliyuncs.com/compatible-mode/v1", false},
154 {"https://api.xiaomimimo.com.attacker.example/v1", false},
155 {"https://example.com/?u=api.xiaomimimo.com", false},
156 {"", false},
157 } {
158 if got := isMimoEntry(&ProviderEntry{BaseURL: tc.baseURL}); got != tc.want {
159 t.Errorf("baseURL=%q: isMimoEntry=%v, want %v", tc.baseURL, got, tc.want)
160 }
161 }
162 if isMimoEntry(nil) {
163 t.Fatal("isMimoEntry(nil) must be false")
164 }
165 }
166
167 func TestMimoEffortSupportsNone(t *testing.T) {
168 e := &ProviderEntry{
169 Kind: "responses",
170 BaseURL: "https://api.xiaomimimo.com/v1",
171 ReasoningProtocol: ReasoningProtocolOpenAI,
172 }
173 cap := EffortCapabilityForEntry(e)
174 if !cap.Supported {
175 t.Fatal("MiMo effort must be supported")
176 }
177 for _, level := range []string{"auto", "none", "low", "medium", "high"} {
178 if !containsString(cap.Levels, level) {
179 t.Errorf("MiMo capability missing %q: %v", level, cap.Levels)
180 }
181 }
182 got, err := NormalizeEffort(e, "none")
183 if err != nil || got != "none" {
184 t.Fatalf("MiMo none = %q/%v, want none/nil", got, err)
185 }
186 for _, level := range []string{"low", "medium", "high"} {
187 if got, err := NormalizeEffort(e, level); err != nil || got != level {
188 t.Fatalf("MiMo %s = %q/%v, want %s/nil", level, got, err, level)
189 }
190 }
191 if _, err := NormalizeEffort(e, "xhigh"); err == nil {
192 t.Fatal("MiMo xhigh must be rejected")
193 }
194 // A plain OpenAI endpoint keeps rejecting none.
195 plain := &ProviderEntry{Kind: "openai", BaseURL: "https://example.com/v1", ReasoningProtocol: ReasoningProtocolOpenAI}
196 if got, err := NormalizeEffort(plain, "none"); err == nil || got != "" {
197 t.Fatalf("plain OpenAI none = %q/%v, want error", got, err)
198 }
199 }
200
201 func TestEffortCapabilityZhipu(t *testing.T) {
202 e := &ProviderEntry{Kind: "openai", BaseURL: "https://open.bigmodel.cn/api/paas/v4", Model: "glm-4.5-air"}
203 cap := EffortCapabilityForEntry(e)
204 if !cap.Supported {
205 t.Fatalf("GLM entry should expose /effort, got %+v", cap)
206 }
207 wantLevels := []string{"auto", "enabled", "disabled"}
208 if len(cap.Levels) != len(wantLevels) {
209 t.Fatalf("levels = %v, want %v", cap.Levels, wantLevels)
210 }
211 for i, l := range wantLevels {
212 if cap.Levels[i] != l {
213 t.Errorf("levels[%d] = %q, want %q", i, cap.Levels[i], l)
214 }
215 }
216 if cap.Default != "enabled" {
217 t.Errorf("default = %q, want enabled (GLM ships with thinking on)", cap.Default)
218 }
219 }
220
221 func TestEffortCapabilityExplicitGLMProtocolOnGateway(t *testing.T) {
222 e := &ProviderEntry{
223 Name: "glm-gateway",
224 Kind: "openai",
225 BaseURL: "https://gateway.example.com/v1",
226 Model: "glm-5.2",
227 ReasoningProtocol: ReasoningProtocolGLM,
228 }
229 cap := EffortCapabilityForEntry(e)
230 want := []string{"auto", "enabled", "disabled"}
231 if !cap.Supported || cap.Default != "enabled" || len(cap.Levels) != len(want) {
232 t.Fatalf("explicit GLM capability = %+v, want levels %v default enabled", cap, want)
233 }
234 for i := range want {
235 if cap.Levels[i] != want[i] {
236 t.Fatalf("explicit GLM levels[%d] = %q, want %q", i, cap.Levels[i], want[i])
237 }
238 }
239 if got, err := NormalizeEffort(e, "disabled"); err != nil || got != "disabled" {
240 t.Fatalf("explicit GLM disabled = %q/%v, want disabled/nil", got, err)
241 }
242 if got, err := NormalizeEffort(e, "high"); err != nil || got != "enabled" {
243 t.Fatalf("explicit GLM legacy high = %q/%v, want enabled/nil", got, err)
244 }
245 }
246
247 func TestGLMModelRegistryUpgradesLegacyGatewayConfig(t *testing.T) {
248 for _, model := range []string{"glm-5", "glm-5.1", "glm-5.2"} {
249 t.Run(model, func(t *testing.T) {
250 // This is the shape persisted by an older Token Rhythm installation:
251 // no reasoning_protocol and no model_overrides metadata.
252 e := &ProviderEntry{
253 Name: "token-rhythm",
254 Kind: "openai",
255 BaseURL: "https://tokenrhythm.studio/v1",
256 Model: model,
257 }
258 if got := ReasoningProtocolForEntry(e); got != ReasoningProtocolGLM {
259 t.Fatalf("ReasoningProtocolForEntry() = %q, want %q", got, ReasoningProtocolGLM)
260 }
261 cap := EffortCapabilityForEntry(e)
262 want := []string{"auto", "enabled", "disabled"}
263 if !cap.Supported || cap.Default != "enabled" || !stringSlicesEqual(cap.Levels, want) {
264 t.Fatalf("legacy gateway GLM capability = %+v, want levels %v default enabled", cap, want)
265 }
266 })
267 }
268
269 nonGLM := &ProviderEntry{Kind: "openai", BaseURL: "https://tokenrhythm.studio/v1", Model: "my-glm-5.2-alias"}
270 if got := ReasoningProtocolForEntry(nonGLM); got != "" {
271 t.Fatalf("non-exact GLM alias protocol = %q, want empty without explicit override", got)
272 }
273 otherGateway := &ProviderEntry{Kind: "openai", BaseURL: "https://opencode.ai/zen/go/v1", Model: "glm-5.2"}
274 if got := ReasoningProtocolForEntry(otherGateway); got != "" {
275 t.Fatalf("unrelated gateway GLM protocol = %q, want empty without explicit override", got)
276 }
277 }
278
279 func TestEffortCapabilityLongCat(t *testing.T) {
280 e := &ProviderEntry{Kind: "openai", BaseURL: "https://api.longcat.chat/openai/v1", Model: "LongCat-2.0"}
281 cap := EffortCapabilityForEntry(e)
282 if !cap.Supported {
283 t.Fatalf("LongCat entry should expose /effort, got %+v", cap)
284 }
285 wantLevels := []string{"auto", "enabled", "disabled"}
286 if len(cap.Levels) != len(wantLevels) {
287 t.Fatalf("levels = %v, want %v", cap.Levels, wantLevels)
288 }
289 for i, l := range wantLevels {
290 if cap.Levels[i] != l {
291 t.Errorf("levels[%d] = %q, want %q", i, cap.Levels[i], l)
292 }
293 }
294 if cap.Default != "enabled" {
295 t.Errorf("default = %q, want enabled", cap.Default)
296 }
297 }
298
299 func TestEffortCapabilityOllamaCloud(t *testing.T) {
300 e := &ProviderEntry{Kind: "openai", BaseURL: "https://ollama.com/v1", Model: "nemotron-3-nano:30b"}
301 cap := EffortCapabilityForEntry(e)
302 if !cap.Supported {
303 t.Fatalf("Ollama Cloud entry should expose /effort, got %+v", cap)
304 }
305 wantLevels := []string{"auto", "none", "low", "medium", "high", "max"}
306 if len(cap.Levels) != len(wantLevels) {
307 t.Fatalf("levels = %v, want %v", cap.Levels, wantLevels)
308 }
309 for i, l := range wantLevels {
310 if cap.Levels[i] != l {
311 t.Errorf("levels[%d] = %q, want %q", i, cap.Levels[i], l)
312 }
313 }
314 if cap.Default != "auto" {
315 t.Errorf("default = %q, want auto", cap.Default)
316 }
317 }
318
319 func TestNormalizeEffortOllamaCloud(t *testing.T) {
320 e := &ProviderEntry{Kind: "openai", BaseURL: "https://ollama.com/v1", Model: "nemotron-3-nano:30b"}
321 cases := []struct {
322 in, want string
323 }{
324 {"auto", ""},
325 {"none", "none"},
326 {"disabled", "none"},
327 {"off", "none"},
328 {"low", "low"},
329 {"medium", "medium"},
330 {"high", "high"},
331 {"xhigh", "max"},
332 {"max", "max"},
333 }
334 for _, tc := range cases {
335 got, err := NormalizeEffort(e, tc.in)
336 if err != nil {
337 t.Errorf("NormalizeEffort(%q) returned error: %v", tc.in, err)
338 continue
339 }
340 if got != tc.want {
341 t.Errorf("NormalizeEffort(%q) = %q, want %q", tc.in, got, tc.want)
342 }
343 }
344 }
345
346 func TestNormalizeEffortZhipu(t *testing.T) {
347 e := &ProviderEntry{Kind: "openai", BaseURL: "https://open.bigmodel.cn/api/paas/v4", Model: "glm-4.5-air"}
348 cases := []struct {
349 in, want string
350 }{
351 {"auto", ""}, // auto == leave to provider default == empty
352 {"enabled", "enabled"},
353 {"disabled", "disabled"},
354 {"ENABLED", "enabled"}, // case-insensitive
355 // Stale values from other vendors resolve to a valid GLM level rather
356 // than failing the /effort command.
357 {"off", "disabled"}, // retired DeepSeek "no thinking"
358 {"low", "enabled"},
359 {"medium", "enabled"},
360 {"high", "enabled"},
361 {"xhigh", "enabled"},
362 {"max", "enabled"},
363 }
364 for _, tc := range cases {
365 got, err := NormalizeEffort(e, tc.in)
366 if err != nil {
367 t.Errorf("NormalizeEffort(%q) returned error: %v", tc.in, err)
368 continue
369 }
370 if got != tc.want {
371 t.Errorf("NormalizeEffort(%q) = %q, want %q", tc.in, got, tc.want)
372 }
373 }
374 }
375
376 func TestNormalizeEffortLongCat(t *testing.T) {
377 e := &ProviderEntry{Kind: "openai", BaseURL: "https://api.longcat.chat/openai/v1", Model: "LongCat-2.0"}
378 cases := []struct {
379 in, want string
380 }{
381 {"auto", ""},
382 {"enabled", "enabled"},
383 {"disabled", "disabled"},
384 {"ENABLED", "enabled"},
385 {"off", "disabled"},
386 {"low", "enabled"},
387 {"medium", "enabled"},
388 {"high", "enabled"},
389 {"xhigh", "enabled"},
390 {"max", "enabled"},
391 }
392 for _, tc := range cases {
393 got, err := NormalizeEffort(e, tc.in)
394 if err != nil {
395 t.Errorf("NormalizeEffort(%q) returned error: %v", tc.in, err)
396 continue
397 }
398 if got != tc.want {
399 t.Errorf("NormalizeEffort(%q) = %q, want %q", tc.in, got, tc.want)
400 }
401 }
402 }
403
404 func TestEffortCapabilityMiniMax(t *testing.T) {
405 e := &ProviderEntry{Kind: "openai", BaseURL: "https://api.minimaxi.com/v1", Model: "MiniMax-M3"}
406 cap := EffortCapabilityForEntry(e)
407 if !cap.Supported {
408 t.Fatalf("M3 entry should expose /effort, got %+v", cap)
409 }
410 wantLevels := []string{"auto", "adaptive", "disabled"}
411 if len(cap.Levels) != len(wantLevels) {
412 t.Fatalf("levels = %v, want %v", cap.Levels, wantLevels)
413 }
414 for i, l := range wantLevels {
415 if cap.Levels[i] != l {
416 t.Errorf("levels[%d] = %q, want %q", i, cap.Levels[i], l)
417 }
418 }
419 if cap.Default != "adaptive" {
420 t.Errorf("default = %q, want adaptive (M3 ships with thinking on)", cap.Default)
421 }
422 }
423
424 func TestNormalizeEffortMiniMax(t *testing.T) {
425 e := &ProviderEntry{Kind: "openai", BaseURL: "https://api.minimaxi.com/v1", Model: "MiniMax-M3"}
426 cases := []struct {
427 in, want string
428 }{
429 {"auto", ""}, // auto == "leave to provider default" == empty
430 {"adaptive", "adaptive"},
431 {"disabled", "disabled"},
432 {"ADAPTIVE", "adaptive"}, // case-insensitive
433 // Stale values from other vendors still resolve to a valid M3 level
434 // rather than failing the /effort command.
435 {"off", "disabled"}, // retired DeepSeek "no thinking" → M3 actually supports "disabled"
436 {"low", "adaptive"},
437 {"medium", "adaptive"},
438 {"high", "adaptive"},
439 {"xhigh", "disabled"},
440 {"max", "disabled"},
441 }
442 for _, tc := range cases {
443 got, err := NormalizeEffort(e, tc.in)
444 if err != nil {
445 t.Errorf("NormalizeEffort(%q) returned error: %v", tc.in, err)
446 continue
447 }
448 if got != tc.want {
449 t.Errorf("NormalizeEffort(%q) = %q, want %q", tc.in, got, tc.want)
450 }
451 }
452 }
453
454 func TestNormalizeEffortMiniMaxRejectsGarbage(t *testing.T) {
455 e := &ProviderEntry{Kind: "openai", BaseURL: "https://api.minimaxi.com/v1", Model: "MiniMax-M3"}
456 // "turbo" and similar unrecognised inputs reach the MiniMax switch and
457 // are rejected with the MiniMax-specific usage hint. Empty input is
458 // rejected earlier with the generic hint; both are valid user-facing
459 // errors. "off" is *not* in this list — it's a retired level we now
460 // migrate to "adaptive" (tested in TestNormalizeEffortMiniMax above).
461 cases := map[string]string{
462 "turbo": "auto|adaptive|disabled",
463 "": "auto|<level>",
464 }
465 for in, wantHint := range cases {
466 _, err := NormalizeEffort(e, in)
467 if err == nil {
468 t.Errorf("NormalizeEffort(%q) should be rejected", in)
469 continue
470 }
471 if !strings.Contains(err.Error(), wantHint) {
472 t.Errorf("NormalizeEffort(%q) error = %q, want it to mention %q", in, err.Error(), wantHint)
473 }
474 }
475 }
476
477 func TestEffectiveEffortMiniMax(t *testing.T) {
478 e := &ProviderEntry{Kind: "openai", BaseURL: "https://api.minimaxi.com/v1", Model: "MiniMax-M3"}
479 // unset → EffectiveEffort stays empty so the wire layer defaults to adaptive
480 if got := EffectiveEffort(e); got != "" {
481 t.Errorf("unset EffectiveEffort = %q, want \"\"", got)
482 }
483 // explicit value is preserved verbatim
484 e.Effort = "disabled"
485 if got := EffectiveEffort(e); got != "disabled" {
486 t.Errorf("explicit EffectiveEffort = %q, want disabled", got)
487 }
488 }
489
489 lines GO