package clients import ( "encoding/json" "os" "testing" "github.com/easyai/easyai-ai-gateway/apps/api/internal/store" ) func TestQwen38MatchesCrossServiceBehaviorVectors(t *testing.T) { raw, err := os.ReadFile("testdata/qwen38-reasoning-vectors.json") if err != nil { t.Fatalf("read behavior vectors: %v", err) } var vectors []struct { Name string `json:"name"` Input map[string]any `json:"input"` Expected map[string]any `json:"expected"` } if err := json.Unmarshal(raw, &vectors); err != nil { t.Fatalf("decode behavior vectors: %v", err) } candidate := store.RuntimeModelCandidate{Provider: "aliyun-bailian-openai", ProviderModelName: "qwen3.8-max-preview"} for _, vector := range vectors { t.Run(vector.Name, func(t *testing.T) { body := cloneBody(vector.Input) body["model"] = "qwen3.8-max-preview" applyOpenAIChatReasoningParams(body, candidate) for _, key := range []string{"enable_thinking", "reasoning_effort", "thinking_budget", "temperature"} { expected, expectedPresent := vector.Expected[key] actual, actualPresent := body[key] if expectedPresent != actualPresent || (expectedPresent && normalizedJSONScalar(actual) != normalizedJSONScalar(expected)) { t.Fatalf("%s mismatch: expected=%#v actual=%#v body=%+v", key, expected, actual, body) } } }) } } func normalizedJSONScalar(value any) any { if number, ok := value.(int); ok { return float64(number) } return value } func TestQwen38MaxPreviewReasoningNormalization(t *testing.T) { tests := []struct { name string body map[string]any effort any budget any temp any enabled any noEffort bool }{ {name: "none", body: map[string]any{"reasoning_effort": "none"}, effort: "low"}, {name: "minimal", body: map[string]any{"reasoning_effort": "minimal"}, effort: "low"}, {name: "medium", body: map[string]any{"reasoning_effort": "medium"}, effort: "medium"}, {name: "high", body: map[string]any{"reasoning_effort": "high"}, effort: "xhigh"}, {name: "max", body: map[string]any{"reasoning_effort": "max"}, effort: "xhigh"}, {name: "unspecified", body: map[string]any{}, noEffort: true}, {name: "forced on", body: map[string]any{"enable_thinking": false}, effort: "low"}, {name: "budget wins", body: map[string]any{"reasoning_effort": "high", "thinking_budget": 999999}, budget: 262144, noEffort: true}, {name: "zero budget", body: map[string]any{"reasoning_effort": "medium", "thinking_budget_tokens": 0}, budget: 0, noEffort: true}, {name: "temperature floor", body: map[string]any{"temperature": 0.1}, temp: 0.6, noEffort: true}, } candidate := store.RuntimeModelCandidate{ Provider: "aliyun-bailian-openai", ProviderModelName: "qwen3.8-max-preview", } for _, test := range tests { t.Run(test.name, func(t *testing.T) { body := cloneBody(test.body) body["model"] = "qwen3.8-max-preview" applyOpenAIChatReasoningParams(body, candidate) if body["enable_thinking"] != true { t.Fatalf("Qwen3.8 must always enable thinking: %+v", body) } if test.noEffort { if _, ok := body["reasoning_effort"]; ok { t.Fatalf("reasoning_effort must be omitted: %+v", body) } } else if body["reasoning_effort"] != test.effort { t.Fatalf("expected effort %#v, got %+v", test.effort, body) } if test.budget != nil && body["thinking_budget"] != test.budget { t.Fatalf("expected budget %#v, got %+v", test.budget, body) } if test.temp != nil && body["temperature"] != test.temp { t.Fatalf("expected temperature %#v, got %+v", test.temp, body) } }) } } func TestGenericReasoningCapabilityUsesNearestTierAndIgnoresModeSwitch(t *testing.T) { body := map[string]any{"reasoning_effort": "medium"} applyOpenAIChatReasoningParams(body, store.RuntimeModelCandidate{ Provider: "openai", Capabilities: map[string]any{ "text_generate": map[string]any{ "supportThinking": true, "supportThinkingModeSwitch": false, "thinkingEffortLevels": []any{"low", "high"}, }, }, }) if body["reasoning_effort"] != "low" { t.Fatalf("equidistant effort must choose lower tier, got %+v", body) } } func TestGenericReasoningKeepsUnknownAndRemovesExplicitUnsupported(t *testing.T) { unknown := map[string]any{"reasoning_effort": "high"} applyOpenAIChatReasoningParams(unknown, store.RuntimeModelCandidate{Provider: "openai"}) if unknown["reasoning_effort"] != "high" { t.Fatalf("unknown capability should preserve effort: %+v", unknown) } unsupported := map[string]any{"reasoning_effort": "high"} applyOpenAIChatReasoningParams(unsupported, store.RuntimeModelCandidate{ Provider: "openai", Capabilities: map[string]any{ "text_generate": map[string]any{"supportThinking": false}, }, }) if _, ok := unsupported["reasoning_effort"]; ok { t.Fatalf("explicit unsupported capability should remove effort: %+v", unsupported) } } func TestQwen38ResponsesReasoningNormalization(t *testing.T) { candidate := store.RuntimeModelCandidate{ Provider: "aliyun-bailian-openai", ProviderModelName: "qwen3.8-max-preview", } tests := []struct { name string requested string expected string }{ {name: "none", requested: "none", expected: "low"}, {name: "minimal", requested: "minimal", expected: "low"}, {name: "medium", requested: "medium", expected: "medium"}, {name: "high", requested: "high", expected: "xhigh"}, {name: "max", requested: "max", expected: "xhigh"}, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { body := map[string]any{ "model": "qwen3.8-max-preview", "reasoning": map[string]any{"effort": test.requested, "summary": "auto"}, "temperature": 0.1, } applyOpenAIResponsesReasoningParams(body, candidate) reasoning := body["reasoning"].(map[string]any) if reasoning["effort"] != test.expected || reasoning["summary"] != "auto" { t.Fatalf("unexpected Responses reasoning: %+v", body) } if body["temperature"] != 0.6 { t.Fatalf("Qwen3.8 Responses temperature was not floored: %+v", body) } }) } } func TestQwen38ResponsesForcedThinkingAndDefault(t *testing.T) { candidate := store.RuntimeModelCandidate{Provider: "aliyun-bailian-openai", ProviderModelName: "qwen3.8-max-preview"} forced := map[string]any{"enable_thinking": false} applyOpenAIResponsesReasoningParams(forced, candidate) if forced["reasoning"].(map[string]any)["effort"] != "low" { t.Fatalf("disabled Qwen3.8 Responses request must become low: %+v", forced) } if _, ok := forced["enable_thinking"]; ok { t.Fatalf("Chat-only alias leaked into Responses body: %+v", forced) } defaulted := map[string]any{"reasoning": map[string]any{"summary": "auto"}} applyOpenAIResponsesReasoningParams(defaulted, candidate) reasoning := defaulted["reasoning"].(map[string]any) if _, ok := reasoning["effort"]; ok || reasoning["summary"] != "auto" { t.Fatalf("unspecified effort must preserve upstream xhigh default: %+v", defaulted) } }