mirror of
https://github.com/router-for-me/CLIProxyAPIPlus.git
synced 2026-03-21 16:40:22 +00:00
feat(thinking): normalize effort levels in adaptive thinking requests to prevent validation errors
This commit is contained in:
@@ -120,6 +120,8 @@ func ConvertGeminiRequestToClaude(modelName string, inputRawJSON []byte, stream
|
|||||||
supportsAdaptive := mi != nil && mi.Thinking != nil && len(mi.Thinking.Levels) > 0
|
supportsAdaptive := mi != nil && mi.Thinking != nil && len(mi.Thinking.Levels) > 0
|
||||||
supportsMax := supportsAdaptive && thinking.HasLevel(mi.Thinking.Levels, string(thinking.LevelMax))
|
supportsMax := supportsAdaptive && thinking.HasLevel(mi.Thinking.Levels, string(thinking.LevelMax))
|
||||||
|
|
||||||
|
// MapToClaudeEffort normalizes levels (e.g. minimal→low, xhigh→high) to avoid
|
||||||
|
// validation errors since validate treats same-provider unsupported levels as errors.
|
||||||
thinkingLevel := thinkingConfig.Get("thinkingLevel")
|
thinkingLevel := thinkingConfig.Get("thinkingLevel")
|
||||||
if !thinkingLevel.Exists() {
|
if !thinkingLevel.Exists() {
|
||||||
thinkingLevel = thinkingConfig.Get("thinking_level")
|
thinkingLevel = thinkingConfig.Get("thinking_level")
|
||||||
@@ -134,12 +136,12 @@ func ConvertGeminiRequestToClaude(modelName string, inputRawJSON []byte, stream
|
|||||||
out, _ = sjson.Delete(out, "thinking.budget_tokens")
|
out, _ = sjson.Delete(out, "thinking.budget_tokens")
|
||||||
out, _ = sjson.Delete(out, "output_config.effort")
|
out, _ = sjson.Delete(out, "output_config.effort")
|
||||||
default:
|
default:
|
||||||
effort, ok := thinking.MapToClaudeEffort(level, supportsMax)
|
if mapped, ok := thinking.MapToClaudeEffort(level, supportsMax); ok {
|
||||||
if ok {
|
level = mapped
|
||||||
out, _ = sjson.Set(out, "thinking.type", "adaptive")
|
|
||||||
out, _ = sjson.Delete(out, "thinking.budget_tokens")
|
|
||||||
out, _ = sjson.Set(out, "output_config.effort", effort)
|
|
||||||
}
|
}
|
||||||
|
out, _ = sjson.Set(out, "thinking.type", "adaptive")
|
||||||
|
out, _ = sjson.Delete(out, "thinking.budget_tokens")
|
||||||
|
out, _ = sjson.Set(out, "output_config.effort", level)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
switch level {
|
switch level {
|
||||||
@@ -173,12 +175,12 @@ func ConvertGeminiRequestToClaude(modelName string, inputRawJSON []byte, stream
|
|||||||
default:
|
default:
|
||||||
level, ok := thinking.ConvertBudgetToLevel(budget)
|
level, ok := thinking.ConvertBudgetToLevel(budget)
|
||||||
if ok {
|
if ok {
|
||||||
effort, ok := thinking.MapToClaudeEffort(level, supportsMax)
|
if mapped, okM := thinking.MapToClaudeEffort(level, supportsMax); okM {
|
||||||
if ok {
|
level = mapped
|
||||||
out, _ = sjson.Set(out, "thinking.type", "adaptive")
|
|
||||||
out, _ = sjson.Delete(out, "thinking.budget_tokens")
|
|
||||||
out, _ = sjson.Set(out, "output_config.effort", effort)
|
|
||||||
}
|
}
|
||||||
|
out, _ = sjson.Set(out, "thinking.type", "adaptive")
|
||||||
|
out, _ = sjson.Delete(out, "thinking.budget_tokens")
|
||||||
|
out, _ = sjson.Set(out, "output_config.effort", level)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -74,6 +74,8 @@ func ConvertOpenAIRequestToClaude(modelName string, inputRawJSON []byte, stream
|
|||||||
supportsMax := supportsAdaptive && thinking.HasLevel(mi.Thinking.Levels, string(thinking.LevelMax))
|
supportsMax := supportsAdaptive && thinking.HasLevel(mi.Thinking.Levels, string(thinking.LevelMax))
|
||||||
|
|
||||||
// Claude 4.6 supports adaptive thinking with output_config.effort.
|
// Claude 4.6 supports adaptive thinking with output_config.effort.
|
||||||
|
// MapToClaudeEffort normalizes levels (e.g. minimal→low, xhigh→high) to avoid
|
||||||
|
// validation errors since validate treats same-provider unsupported levels as errors.
|
||||||
if supportsAdaptive {
|
if supportsAdaptive {
|
||||||
switch effort {
|
switch effort {
|
||||||
case "none":
|
case "none":
|
||||||
@@ -85,7 +87,6 @@ func ConvertOpenAIRequestToClaude(modelName string, inputRawJSON []byte, stream
|
|||||||
out, _ = sjson.Delete(out, "thinking.budget_tokens")
|
out, _ = sjson.Delete(out, "thinking.budget_tokens")
|
||||||
out, _ = sjson.Delete(out, "output_config.effort")
|
out, _ = sjson.Delete(out, "output_config.effort")
|
||||||
default:
|
default:
|
||||||
// Map non-Claude effort levels into Claude 4.6 effort vocabulary.
|
|
||||||
if mapped, ok := thinking.MapToClaudeEffort(effort, supportsMax); ok {
|
if mapped, ok := thinking.MapToClaudeEffort(effort, supportsMax); ok {
|
||||||
effort = mapped
|
effort = mapped
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,6 +62,8 @@ func ConvertOpenAIResponsesRequestToClaude(modelName string, inputRawJSON []byte
|
|||||||
supportsMax := supportsAdaptive && thinking.HasLevel(mi.Thinking.Levels, string(thinking.LevelMax))
|
supportsMax := supportsAdaptive && thinking.HasLevel(mi.Thinking.Levels, string(thinking.LevelMax))
|
||||||
|
|
||||||
// Claude 4.6 supports adaptive thinking with output_config.effort.
|
// Claude 4.6 supports adaptive thinking with output_config.effort.
|
||||||
|
// MapToClaudeEffort normalizes levels (e.g. minimal→low, xhigh→high) to avoid
|
||||||
|
// validation errors since validate treats same-provider unsupported levels as errors.
|
||||||
if supportsAdaptive {
|
if supportsAdaptive {
|
||||||
switch effort {
|
switch effort {
|
||||||
case "none":
|
case "none":
|
||||||
@@ -73,7 +75,6 @@ func ConvertOpenAIResponsesRequestToClaude(modelName string, inputRawJSON []byte
|
|||||||
out, _ = sjson.Delete(out, "thinking.budget_tokens")
|
out, _ = sjson.Delete(out, "thinking.budget_tokens")
|
||||||
out, _ = sjson.Delete(out, "output_config.effort")
|
out, _ = sjson.Delete(out, "output_config.effort")
|
||||||
default:
|
default:
|
||||||
// Map non-Claude effort levels into Claude 4.6 effort vocabulary.
|
|
||||||
if mapped, ok := thinking.MapToClaudeEffort(effort, supportsMax); ok {
|
if mapped, ok := thinking.MapToClaudeEffort(effort, supportsMax); ok {
|
||||||
effort = mapped
|
effort = mapped
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -232,19 +232,14 @@ func ConvertClaudeRequestToCodex(modelName string, inputRawJSON []byte, _ bool)
|
|||||||
}
|
}
|
||||||
case "adaptive", "auto":
|
case "adaptive", "auto":
|
||||||
// Adaptive thinking can carry an explicit effort in output_config.effort (Claude 4.6).
|
// Adaptive thinking can carry an explicit effort in output_config.effort (Claude 4.6).
|
||||||
// Preserve it when present; otherwise keep the previous "max capacity" sentinel.
|
// Pass through directly; ApplyThinking handles clamping to target model's levels.
|
||||||
effort := ""
|
effort := ""
|
||||||
if v := rootResult.Get("output_config.effort"); v.Exists() && v.Type == gjson.String {
|
if v := rootResult.Get("output_config.effort"); v.Exists() && v.Type == gjson.String {
|
||||||
effort = strings.ToLower(strings.TrimSpace(v.String()))
|
effort = strings.ToLower(strings.TrimSpace(v.String()))
|
||||||
}
|
}
|
||||||
switch effort {
|
if effort != "" {
|
||||||
case "minimal", "low", "medium", "high":
|
|
||||||
reasoningEffort = effort
|
reasoningEffort = effort
|
||||||
case "max":
|
} else {
|
||||||
reasoningEffort = string(thinking.LevelXHigh)
|
|
||||||
default:
|
|
||||||
// Keep adaptive/auto as a high level sentinel; ApplyThinking resolves it
|
|
||||||
// to model-specific max capability.
|
|
||||||
reasoningEffort = string(thinking.LevelXHigh)
|
reasoningEffort = string(thinking.LevelXHigh)
|
||||||
}
|
}
|
||||||
case "disabled":
|
case "disabled":
|
||||||
|
|||||||
@@ -171,7 +171,8 @@ func ConvertClaudeRequestToCLI(modelName string, inputRawJSON []byte, _ bool) []
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Map Anthropic thinking -> Gemini thinkingBudget/include_thoughts when type==enabled
|
// Map Anthropic thinking -> Gemini CLI thinkingConfig when enabled
|
||||||
|
// Translator only does format conversion, ApplyThinking handles model capability validation.
|
||||||
if t := gjson.GetBytes(rawJSON, "thinking"); t.Exists() && t.IsObject() {
|
if t := gjson.GetBytes(rawJSON, "thinking"); t.Exists() && t.IsObject() {
|
||||||
switch t.Get("type").String() {
|
switch t.Get("type").String() {
|
||||||
case "enabled":
|
case "enabled":
|
||||||
@@ -181,9 +182,19 @@ func ConvertClaudeRequestToCLI(modelName string, inputRawJSON []byte, _ bool) []
|
|||||||
out, _ = sjson.Set(out, "request.generationConfig.thinkingConfig.includeThoughts", true)
|
out, _ = sjson.Set(out, "request.generationConfig.thinkingConfig.includeThoughts", true)
|
||||||
}
|
}
|
||||||
case "adaptive", "auto":
|
case "adaptive", "auto":
|
||||||
// Keep adaptive/auto as a high level sentinel; ApplyThinking resolves it
|
// For adaptive thinking:
|
||||||
// to model-specific max capability.
|
// - If output_config.effort is explicitly present, pass through as thinkingLevel.
|
||||||
out, _ = sjson.Set(out, "request.generationConfig.thinkingConfig.thinkingLevel", "high")
|
// - Otherwise, treat it as "enabled with target-model maximum" and emit high.
|
||||||
|
// ApplyThinking handles clamping to target model's supported levels.
|
||||||
|
effort := ""
|
||||||
|
if v := gjson.GetBytes(rawJSON, "output_config.effort"); v.Exists() && v.Type == gjson.String {
|
||||||
|
effort = strings.ToLower(strings.TrimSpace(v.String()))
|
||||||
|
}
|
||||||
|
if effort != "" {
|
||||||
|
out, _ = sjson.Set(out, "request.generationConfig.thinkingConfig.thinkingLevel", effort)
|
||||||
|
} else {
|
||||||
|
out, _ = sjson.Set(out, "request.generationConfig.thinkingConfig.thinkingLevel", "high")
|
||||||
|
}
|
||||||
out, _ = sjson.Set(out, "request.generationConfig.thinkingConfig.includeThoughts", true)
|
out, _ = sjson.Set(out, "request.generationConfig.thinkingConfig.includeThoughts", true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -164,19 +164,15 @@ func ConvertClaudeRequestToGemini(modelName string, inputRawJSON []byte, _ bool)
|
|||||||
}
|
}
|
||||||
case "adaptive", "auto":
|
case "adaptive", "auto":
|
||||||
// For adaptive thinking:
|
// For adaptive thinking:
|
||||||
// - If output_config.effort is explicitly present, map it to thinkingLevel.
|
// - If output_config.effort is explicitly present, pass through as thinkingLevel.
|
||||||
// - Otherwise, treat it as "enabled with target-model maximum" and emit thinkingBudget=max.
|
// - Otherwise, treat it as "enabled with target-model maximum" and emit thinkingBudget=max.
|
||||||
|
// ApplyThinking handles clamping to target model's supported levels.
|
||||||
effort := ""
|
effort := ""
|
||||||
if v := gjson.GetBytes(rawJSON, "output_config.effort"); v.Exists() && v.Type == gjson.String {
|
if v := gjson.GetBytes(rawJSON, "output_config.effort"); v.Exists() && v.Type == gjson.String {
|
||||||
effort = strings.ToLower(strings.TrimSpace(v.String()))
|
effort = strings.ToLower(strings.TrimSpace(v.String()))
|
||||||
}
|
}
|
||||||
if effort != "" {
|
if effort != "" {
|
||||||
level := effort
|
out, _ = sjson.Set(out, "generationConfig.thinkingConfig.thinkingLevel", effort)
|
||||||
switch level {
|
|
||||||
case "xhigh", "max":
|
|
||||||
level = "high"
|
|
||||||
}
|
|
||||||
out, _ = sjson.Set(out, "generationConfig.thinkingConfig.thinkingLevel", level)
|
|
||||||
} else {
|
} else {
|
||||||
maxBudget := 0
|
maxBudget := 0
|
||||||
if mi := registry.LookupModelInfo(modelName, "gemini"); mi != nil && mi.Thinking != nil {
|
if mi := registry.LookupModelInfo(modelName, "gemini"); mi != nil && mi.Thinking != nil {
|
||||||
|
|||||||
@@ -77,19 +77,14 @@ func ConvertClaudeRequestToOpenAI(modelName string, inputRawJSON []byte, stream
|
|||||||
}
|
}
|
||||||
case "adaptive", "auto":
|
case "adaptive", "auto":
|
||||||
// Adaptive thinking can carry an explicit effort in output_config.effort (Claude 4.6).
|
// Adaptive thinking can carry an explicit effort in output_config.effort (Claude 4.6).
|
||||||
// Preserve it when present; otherwise keep the previous "max capacity" sentinel.
|
// Pass through directly; ApplyThinking handles clamping to target model's levels.
|
||||||
effort := ""
|
effort := ""
|
||||||
if v := root.Get("output_config.effort"); v.Exists() && v.Type == gjson.String {
|
if v := root.Get("output_config.effort"); v.Exists() && v.Type == gjson.String {
|
||||||
effort = strings.ToLower(strings.TrimSpace(v.String()))
|
effort = strings.ToLower(strings.TrimSpace(v.String()))
|
||||||
}
|
}
|
||||||
switch effort {
|
if effort != "" {
|
||||||
case "minimal", "low", "medium", "high":
|
|
||||||
out, _ = sjson.Set(out, "reasoning_effort", effort)
|
out, _ = sjson.Set(out, "reasoning_effort", effort)
|
||||||
case "max":
|
} else {
|
||||||
out, _ = sjson.Set(out, "reasoning_effort", string(thinking.LevelXHigh))
|
|
||||||
default:
|
|
||||||
// Keep adaptive/auto as a high level sentinel; ApplyThinking resolves it
|
|
||||||
// to model-specific max capability.
|
|
||||||
out, _ = sjson.Set(out, "reasoning_effort", string(thinking.LevelXHigh))
|
out, _ = sjson.Set(out, "reasoning_effort", string(thinking.LevelXHigh))
|
||||||
}
|
}
|
||||||
case "disabled":
|
case "disabled":
|
||||||
|
|||||||
Reference in New Issue
Block a user