Files
easyai-ai-gateway/apps/api/internal/clients/chat_reasoning_qwen38_test.go
T
easyai 9c093974a1 fix(gateway): 修复 Qwen3.8 推理参数并增加上游自愈
统一 Qwen3.8 固定思考、推理档位、预算互斥与温度下限规则,并覆盖 Chat Completions 和原生 Responses 请求。

新增安全参数纠正、有限重试和并发安全的进程内 LRU 学习;补充共享行为向量、httptest 回归与真实模型验收用例。

验证:go test ./... -count=1 通过,gofmt 检查通过;真实 Qwen3.8 流式、非流式及缓存重学习验收通过。
2026-08-01 22:50:45 +08:00

185 lines
6.9 KiB
Go

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)
}
}