fix(gateway): 修复 Qwen3.8 推理参数并增加上游自愈
统一 Qwen3.8 固定思考、推理档位、预算互斥与温度下限规则,并覆盖 Chat Completions 和原生 Responses 请求。 新增安全参数纠正、有限重试和并发安全的进程内 LRU 学习;补充共享行为向量、httptest 回归与真实模型验收用例。 验证:go test ./... -count=1 通过,gofmt 检查通过;真实 Qwen3.8 流式、非流式及缓存重学习验收通过。
This commit is contained in:
@@ -12,7 +12,8 @@ import (
|
||||
const OpenAIReasoningEffortValidationMessage = "reasoning_effort must be one of: none, minimal, low, medium, high, xhigh, max"
|
||||
|
||||
var (
|
||||
openAIReasoningEfforts = map[string]struct{}{
|
||||
chatReasoningEffortOrder = []string{"none", "minimal", "low", "medium", "high", "xhigh", "max"}
|
||||
openAIReasoningEfforts = map[string]struct{}{
|
||||
"none": {},
|
||||
"minimal": {},
|
||||
"low": {},
|
||||
@@ -50,9 +51,25 @@ func ValidateOpenAIReasoningEffort(value any) error {
|
||||
|
||||
func applyOpenAIChatReasoningParams(body map[string]any, candidate store.RuntimeModelCandidate) {
|
||||
effort := normalizedReasoningString(body["reasoning_effort"])
|
||||
model := chatReasoningModelName(body, candidate)
|
||||
if isAliyunBailianOpenAI(candidate) && isAliyunQwen38MaxPreview(model) {
|
||||
applyAliyunQwen38Reasoning(body, effort)
|
||||
if temperature, ok := finiteFloatFromAny(body["temperature"]); ok && temperature < 0.6 {
|
||||
body["temperature"] = 0.6
|
||||
}
|
||||
return
|
||||
}
|
||||
if effort == "" || !isOpenAIReasoningEffort(effort) {
|
||||
return
|
||||
}
|
||||
resolved, state := resolveCandidateReasoningEffort(effort, candidate)
|
||||
if state == reasoningCapabilityUnsupported {
|
||||
delete(body, "reasoning_effort")
|
||||
return
|
||||
}
|
||||
if resolved != "" {
|
||||
effort = resolved
|
||||
}
|
||||
body["reasoning_effort"] = effort
|
||||
|
||||
switch {
|
||||
@@ -67,8 +84,183 @@ func applyOpenAIChatReasoningParams(body map[string]any, candidate store.Runtime
|
||||
}
|
||||
}
|
||||
|
||||
func applyOpenAIResponsesReasoningParams(body map[string]any, candidate store.RuntimeModelCandidate) {
|
||||
reasoning, _ := body["reasoning"].(map[string]any)
|
||||
if reasoning != nil {
|
||||
reasoning = cloneBody(reasoning)
|
||||
}
|
||||
effort := ""
|
||||
if reasoning != nil {
|
||||
effort = normalizedReasoningString(reasoning["effort"])
|
||||
}
|
||||
model := chatReasoningModelName(body, candidate)
|
||||
qwen38 := isAliyunBailianOpenAI(candidate) && isAliyunQwen38MaxPreview(model)
|
||||
if qwen38 {
|
||||
if effort == "" {
|
||||
if enabled, ok := body["enable_thinking"].(bool); ok && !enabled {
|
||||
effort = "low"
|
||||
}
|
||||
} else {
|
||||
effort = qwen38MaxPreviewReasoningEffort(effort)
|
||||
}
|
||||
} else if effort != "" && isOpenAIReasoningEffort(effort) {
|
||||
resolved, state := resolveCandidateReasoningEffort(effort, candidate)
|
||||
if state == reasoningCapabilityUnsupported {
|
||||
effort = ""
|
||||
} else if resolved != "" {
|
||||
effort = resolved
|
||||
}
|
||||
}
|
||||
|
||||
if effort != "" {
|
||||
if reasoning == nil {
|
||||
reasoning = map[string]any{}
|
||||
}
|
||||
reasoning["effort"] = effort
|
||||
} else if reasoning != nil {
|
||||
delete(reasoning, "effort")
|
||||
}
|
||||
if len(reasoning) > 0 {
|
||||
body["reasoning"] = reasoning
|
||||
} else {
|
||||
delete(body, "reasoning")
|
||||
}
|
||||
delete(body, "reasoning_effort")
|
||||
delete(body, "enable_thinking")
|
||||
if qwen38 {
|
||||
if temperature, ok := finiteFloatFromAny(body["temperature"]); ok && temperature < 0.6 {
|
||||
body["temperature"] = 0.6
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func applyAliyunQwen38Reasoning(body map[string]any, effort string) {
|
||||
defer delete(body, "thinking_budget_tokens")
|
||||
requestedDisable := false
|
||||
if enabled, ok := body["enable_thinking"].(bool); ok && !enabled {
|
||||
requestedDisable = true
|
||||
}
|
||||
body["enable_thinking"] = true
|
||||
budget, hasBudget := nonNegativeIntFromAny(body["thinking_budget"])
|
||||
if !hasBudget {
|
||||
budget, hasBudget = nonNegativeIntFromAny(body["thinking_budget_tokens"])
|
||||
}
|
||||
if hasBudget {
|
||||
if budget > 262144 {
|
||||
budget = 262144
|
||||
}
|
||||
body["thinking_budget"] = budget
|
||||
delete(body, "reasoning_effort")
|
||||
return
|
||||
}
|
||||
delete(body, "thinking_budget")
|
||||
if effort == "" {
|
||||
if requestedDisable {
|
||||
body["reasoning_effort"] = "low"
|
||||
}
|
||||
return
|
||||
}
|
||||
body["reasoning_effort"] = qwen38MaxPreviewReasoningEffort(effort)
|
||||
}
|
||||
|
||||
func qwen38MaxPreviewReasoningEffort(effort string) string {
|
||||
switch effort {
|
||||
case "none", "minimal", "low":
|
||||
return "low"
|
||||
case "medium":
|
||||
return "medium"
|
||||
default:
|
||||
return "xhigh"
|
||||
}
|
||||
}
|
||||
|
||||
type reasoningCapabilityState int
|
||||
|
||||
const (
|
||||
reasoningCapabilityUnknown reasoningCapabilityState = iota
|
||||
reasoningCapabilityUnsupported
|
||||
reasoningCapabilitySupported
|
||||
)
|
||||
|
||||
func resolveCandidateReasoningEffort(effort string, candidate store.RuntimeModelCandidate) (string, reasoningCapabilityState) {
|
||||
supported := map[string]struct{}{}
|
||||
declaredLevels := false
|
||||
explicitlyUnsupported := false
|
||||
explicitlySupported := false
|
||||
for _, key := range []string{"text_generate", "tools_call", "image_analysis", "video_understanding", "audio_understanding", "omni"} {
|
||||
capability, ok := candidate.Capabilities[key].(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if value, ok := capability["supportThinking"].(bool); ok && !value {
|
||||
explicitlyUnsupported = true
|
||||
continue
|
||||
}
|
||||
if value, ok := capability["supportThinking"].(bool); ok && value {
|
||||
explicitlySupported = true
|
||||
}
|
||||
rawLevels, ok := capability["thinkingEffortLevels"].([]any)
|
||||
if !ok {
|
||||
if stringsValue, stringsOK := capability["thinkingEffortLevels"].([]string); stringsOK {
|
||||
rawLevels = make([]any, len(stringsValue))
|
||||
for index, value := range stringsValue {
|
||||
rawLevels[index] = value
|
||||
}
|
||||
ok = true
|
||||
}
|
||||
}
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
declaredLevels = true
|
||||
for _, raw := range rawLevels {
|
||||
level := normalizedReasoningString(raw)
|
||||
if isOpenAIReasoningEffort(level) {
|
||||
supported[level] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(supported) == 0 {
|
||||
if declaredLevels || (explicitlyUnsupported && !explicitlySupported) {
|
||||
return "", reasoningCapabilityUnsupported
|
||||
}
|
||||
return effort, reasoningCapabilityUnknown
|
||||
}
|
||||
requestedIndex := reasoningEffortIndex(effort)
|
||||
best := ""
|
||||
bestDistance := len(chatReasoningEffortOrder) + 1
|
||||
for index, candidateEffort := range chatReasoningEffortOrder {
|
||||
if _, ok := supported[candidateEffort]; !ok {
|
||||
continue
|
||||
}
|
||||
distance := index - requestedIndex
|
||||
if distance < 0 {
|
||||
distance = -distance
|
||||
}
|
||||
if distance < bestDistance {
|
||||
best = candidateEffort
|
||||
bestDistance = distance
|
||||
}
|
||||
}
|
||||
return best, reasoningCapabilitySupported
|
||||
}
|
||||
|
||||
func reasoningEffortIndex(effort string) int {
|
||||
for index, value := range chatReasoningEffortOrder {
|
||||
if value == effort {
|
||||
return index
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func applyAliyunReasoning(body map[string]any, candidate store.RuntimeModelCandidate, effort string) {
|
||||
defer delete(body, "thinking_budget_tokens")
|
||||
budget, hasBudget := positiveIntFromAny(body["thinking_budget"])
|
||||
if !hasBudget {
|
||||
budget, hasBudget = positiveIntFromAny(body["thinking_budget_tokens"])
|
||||
}
|
||||
delete(body, "thinking_budget")
|
||||
|
||||
if effort == "none" {
|
||||
body["enable_thinking"] = false
|
||||
@@ -94,7 +286,7 @@ func applyAliyunReasoning(body map[string]any, candidate store.RuntimeModelCandi
|
||||
|
||||
delete(body, "reasoning_effort")
|
||||
if isAliyunThinkingBudgetModel(model) {
|
||||
if budget, ok := positiveIntFromAny(body["thinking_budget_tokens"]); ok {
|
||||
if hasBudget {
|
||||
body["thinking_budget"] = budget
|
||||
}
|
||||
}
|
||||
@@ -232,17 +424,6 @@ func highMaxReasoningEffort(effort string) string {
|
||||
return "high"
|
||||
}
|
||||
|
||||
func qwen38MaxPreviewReasoningEffort(effort string) string {
|
||||
switch effort {
|
||||
case "low", "minimal":
|
||||
return "low"
|
||||
case "medium":
|
||||
return "medium"
|
||||
default:
|
||||
return "xhigh"
|
||||
}
|
||||
}
|
||||
|
||||
func zhipuReasoningEffort(effort string) string {
|
||||
if effort == "max" {
|
||||
return "xhigh"
|
||||
@@ -306,6 +487,49 @@ func positiveIntFromAny(value any) (int, bool) {
|
||||
return int(math.Floor(number)), true
|
||||
}
|
||||
|
||||
func nonNegativeIntFromAny(value any) (int, bool) {
|
||||
number, ok := finiteFloatFromAny(value)
|
||||
if !ok || number < 0 {
|
||||
return 0, false
|
||||
}
|
||||
return int(math.Floor(number)), true
|
||||
}
|
||||
|
||||
func finiteFloatFromAny(value any) (float64, bool) {
|
||||
if value == nil {
|
||||
return 0, false
|
||||
}
|
||||
var number float64
|
||||
switch typed := value.(type) {
|
||||
case int:
|
||||
number = float64(typed)
|
||||
case int64:
|
||||
number = float64(typed)
|
||||
case float64:
|
||||
number = typed
|
||||
case float32:
|
||||
number = float64(typed)
|
||||
case jsonNumber:
|
||||
parsed, err := typed.Float64()
|
||||
if err != nil {
|
||||
return 0, false
|
||||
}
|
||||
number = parsed
|
||||
case string:
|
||||
parsed, err := strconv.ParseFloat(strings.TrimSpace(typed), 64)
|
||||
if err != nil {
|
||||
return 0, false
|
||||
}
|
||||
number = parsed
|
||||
default:
|
||||
return 0, false
|
||||
}
|
||||
if math.IsNaN(number) || math.IsInf(number, 0) {
|
||||
return 0, false
|
||||
}
|
||||
return number, true
|
||||
}
|
||||
|
||||
type jsonNumber interface {
|
||||
Float64() (float64, error)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user