package clients import ( "fmt" "math" "strconv" "strings" "github.com/easyai/easyai-ai-gateway/apps/api/internal/store" ) const OpenAIReasoningEffortValidationMessage = "reasoning_effort must be one of: none, minimal, low, medium, high, xhigh" var ( openAIReasoningEfforts = map[string]struct{}{ "none": {}, "minimal": {}, "low": {}, "medium": {}, "high": {}, "xhigh": {}, } volcesChatReasoningEfforts = map[string]struct{}{ "minimal": {}, "low": {}, "medium": {}, "high": {}, } zhipuReasoningEfforts = map[string]struct{}{ "none": {}, "minimal": {}, "low": {}, "medium": {}, "high": {}, "xhigh": {}, } ) func ValidateOpenAIReasoningEffort(value any) error { effort := normalizedReasoningString(value) if effort == "" { return nil } if isOpenAIReasoningEffort(effort) { return nil } return &ClientError{Code: "invalid_parameter", Message: OpenAIReasoningEffortValidationMessage, Retryable: false} } func applyOpenAIChatReasoningParams(body map[string]any, candidate store.RuntimeModelCandidate) { effort := normalizedReasoningString(body["reasoning_effort"]) if effort == "" || !isOpenAIReasoningEffort(effort) { return } body["reasoning_effort"] = effort switch { case isAliyunBailianOpenAI(candidate): applyAliyunReasoning(body, candidate, effort) case isDeepSeekOpenAI(candidate): applyHighMaxThinkingReasoning(body, effort) case isZhipuOpenAI(candidate): applyZhipuReasoning(body, candidate, effort) case isVolcesOpenAI(candidate): applyVolcesReasoning(body, candidate, effort) } } func applyAliyunReasoning(body map[string]any, candidate store.RuntimeModelCandidate, effort string) { defer delete(body, "thinking_budget_tokens") if effort == "none" { body["enable_thinking"] = false delete(body, "reasoning_effort") return } body["enable_thinking"] = true model := chatReasoningModelName(body, candidate) if isAliyunHighMaxReasoningModel(model) { body["reasoning_effort"] = highMaxReasoningEffort(effort) return } delete(body, "reasoning_effort") if isAliyunThinkingBudgetModel(model) { if budget, ok := positiveIntFromAny(body["thinking_budget_tokens"]); ok { body["thinking_budget"] = budget } } } func applyHighMaxThinkingReasoning(body map[string]any, effort string) { if effort == "none" { body["thinking"] = map[string]any{"type": "disabled"} delete(body, "reasoning_effort") return } body["thinking"] = map[string]any{"type": "enabled"} body["reasoning_effort"] = highMaxReasoningEffort(effort) } func applyZhipuReasoning(body map[string]any, candidate store.RuntimeModelCandidate, effort string) { if effort == "none" { body["thinking"] = map[string]any{"type": "disabled"} delete(body, "reasoning_effort") return } body["thinking"] = map[string]any{"type": "enabled"} if !isZhipuReasoningEffortModel(chatReasoningModelName(body, candidate)) { delete(body, "reasoning_effort") return } if mapped := zhipuReasoningEffort(effort); mapped != "" { body["reasoning_effort"] = mapped return } delete(body, "reasoning_effort") } func applyVolcesReasoning(body map[string]any, _ store.RuntimeModelCandidate, effort string) { if effort == "none" { body["thinking"] = map[string]any{"type": "disabled"} delete(body, "reasoning_effort") return } body["thinking"] = map[string]any{"type": "enabled"} if mapped := volcesChatReasoningEffort(effort); mapped != "" { body["reasoning_effort"] = mapped return } delete(body, "reasoning_effort") } func normalizedReasoningString(value any) string { if value == nil { return "" } text, ok := value.(string) if !ok { text = fmt.Sprint(value) } return strings.ToLower(strings.TrimSpace(text)) } func isOpenAIReasoningEffort(effort string) bool { _, ok := openAIReasoningEfforts[effort] return ok } func chatReasoningModelName(body map[string]any, candidate store.RuntimeModelCandidate) string { for _, value := range []any{ body["model"], candidate.ProviderModelName, candidate.ModelName, candidate.ModelAlias, } { if text := normalizedReasoningString(value); text != "" { return text } } return "" } func providerCode(candidate store.RuntimeModelCandidate) string { return normalizedReasoningString(candidate.Provider) } func baseURLCode(candidate store.RuntimeModelCandidate) string { return normalizedReasoningString(candidate.BaseURL) } func isAliyunBailianOpenAI(candidate store.RuntimeModelCandidate) bool { provider := providerCode(candidate) baseURL := baseURLCode(candidate) return provider == "aliyun-bailian-openai" || strings.Contains(baseURL, "dashscope.") } func isVolcesOpenAI(candidate store.RuntimeModelCandidate) bool { provider := providerCode(candidate) baseURL := baseURLCode(candidate) return provider == "volces-openai" || strings.Contains(baseURL, "volces.com") || strings.Contains(baseURL, "byteplus.com") } func isDeepSeekOpenAI(candidate store.RuntimeModelCandidate) bool { provider := providerCode(candidate) baseURL := baseURLCode(candidate) return provider == "deepseek-openai" || strings.Contains(baseURL, "api.deepseek.com") } func isZhipuOpenAI(candidate store.RuntimeModelCandidate) bool { provider := providerCode(candidate) baseURL := baseURLCode(candidate) return provider == "zhipu-openai" || strings.Contains(baseURL, "bigmodel.cn") || strings.Contains(baseURL, "api.z.ai") } func isAliyunHighMaxReasoningModel(model string) bool { return strings.Contains(model, "deepseek-v4") || strings.HasPrefix(model, "glm-") } func isAliyunThinkingBudgetModel(model string) bool { return strings.Contains(model, "qwen") || strings.Contains(model, "qwq") || strings.Contains(model, "qvq") || strings.Contains(model, "kimi") } func isVolcesReasoningEffortModel(model string) bool { return strings.HasPrefix(model, "doubao-seed-2-") } func isZhipuReasoningEffortModel(model string) bool { return model == "" || strings.HasPrefix(model, "glm-5.2") || strings.HasPrefix(model, "glm-5-2") } func highMaxReasoningEffort(effort string) string { if effort == "xhigh" { return "max" } return "high" } func zhipuReasoningEffort(effort string) string { if _, ok := zhipuReasoningEfforts[effort]; ok { return effort } return "" } func volcesChatReasoningEffort(effort string) string { switch effort { case "none": return "minimal" case "xhigh": return "high" default: if _, ok := volcesChatReasoningEfforts[effort]; ok { return effort } return "" } } func positiveIntFromAny(value any) (int, 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 := normalizedReasoningString(typed) if parsed == "" { return 0, false } value, err := strconv.ParseFloat(parsed, 64) if err != nil { return 0, false } number = value default: return 0, false } if math.IsNaN(number) || math.IsInf(number, 0) || number <= 0 { return 0, false } return int(math.Floor(number)), true } type jsonNumber interface { Float64() (float64, error) }