统一 Qwen3.8 固定思考、推理档位、预算互斥与温度下限规则,并覆盖 Chat Completions 和原生 Responses 请求。 新增安全参数纠正、有限重试和并发安全的进程内 LRU 学习;补充共享行为向量、httptest 回归与真实模型验收用例。 验证:go test ./... -count=1 通过,gofmt 检查通过;真实 Qwen3.8 流式、非流式及缓存重学习验收通过。
185 lines
6.9 KiB
Go
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)
|
|
}
|
|
}
|