mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-02-18 20:30:51 +08:00
Merge pull request #1519 from router-for-me/thinking
feat(translator): support Claude thinking type adaptive
This commit is contained in:
@@ -866,7 +866,7 @@ func GetAntigravityModelConfig() map[string]*AntigravityModelConfig {
|
|||||||
"gemini-3-flash": {Thinking: &ThinkingSupport{Min: 128, Max: 32768, ZeroAllowed: false, DynamicAllowed: true, Levels: []string{"minimal", "low", "medium", "high"}}},
|
"gemini-3-flash": {Thinking: &ThinkingSupport{Min: 128, Max: 32768, ZeroAllowed: false, DynamicAllowed: true, Levels: []string{"minimal", "low", "medium", "high"}}},
|
||||||
"claude-sonnet-4-5-thinking": {Thinking: &ThinkingSupport{Min: 1024, Max: 128000, ZeroAllowed: true, DynamicAllowed: true}, MaxCompletionTokens: 64000},
|
"claude-sonnet-4-5-thinking": {Thinking: &ThinkingSupport{Min: 1024, Max: 128000, ZeroAllowed: true, DynamicAllowed: true}, MaxCompletionTokens: 64000},
|
||||||
"claude-opus-4-5-thinking": {Thinking: &ThinkingSupport{Min: 1024, Max: 128000, ZeroAllowed: true, DynamicAllowed: true}, MaxCompletionTokens: 64000},
|
"claude-opus-4-5-thinking": {Thinking: &ThinkingSupport{Min: 1024, Max: 128000, ZeroAllowed: true, DynamicAllowed: true}, MaxCompletionTokens: 64000},
|
||||||
"claude-opus-4-6-thinking": {Thinking: &ThinkingSupport{Min: 1024, Max: 128000, ZeroAllowed: true, DynamicAllowed: true}, MaxCompletionTokens: 128000},
|
"claude-opus-4-6-thinking": {Thinking: &ThinkingSupport{Min: 1024, Max: 128000, ZeroAllowed: true, DynamicAllowed: true}, MaxCompletionTokens: 64000},
|
||||||
"claude-sonnet-4-5": {MaxCompletionTokens: 64000},
|
"claude-sonnet-4-5": {MaxCompletionTokens: 64000},
|
||||||
"gpt-oss-120b-medium": {},
|
"gpt-oss-120b-medium": {},
|
||||||
"tab_flash_lite_preview": {},
|
"tab_flash_lite_preview": {},
|
||||||
|
|||||||
@@ -344,7 +344,8 @@ func ConvertClaudeRequestToAntigravity(modelName string, inputRawJSON []byte, _
|
|||||||
// Inject interleaved thinking hint when both tools and thinking are active
|
// Inject interleaved thinking hint when both tools and thinking are active
|
||||||
hasTools := toolDeclCount > 0
|
hasTools := toolDeclCount > 0
|
||||||
thinkingResult := gjson.GetBytes(rawJSON, "thinking")
|
thinkingResult := gjson.GetBytes(rawJSON, "thinking")
|
||||||
hasThinking := thinkingResult.Exists() && thinkingResult.IsObject() && thinkingResult.Get("type").String() == "enabled"
|
thinkingType := thinkingResult.Get("type").String()
|
||||||
|
hasThinking := thinkingResult.Exists() && thinkingResult.IsObject() && (thinkingType == "enabled" || thinkingType == "adaptive")
|
||||||
isClaudeThinking := util.IsClaudeThinkingModel(modelName)
|
isClaudeThinking := util.IsClaudeThinkingModel(modelName)
|
||||||
|
|
||||||
if hasTools && hasThinking && isClaudeThinking {
|
if hasTools && hasThinking && isClaudeThinking {
|
||||||
@@ -377,12 +378,18 @@ func ConvertClaudeRequestToAntigravity(modelName string, inputRawJSON []byte, _
|
|||||||
|
|
||||||
// Map Anthropic thinking -> Gemini thinkingBudget/include_thoughts when type==enabled
|
// Map Anthropic thinking -> Gemini thinkingBudget/include_thoughts when type==enabled
|
||||||
if t := gjson.GetBytes(rawJSON, "thinking"); enableThoughtTranslate && t.Exists() && t.IsObject() {
|
if t := gjson.GetBytes(rawJSON, "thinking"); enableThoughtTranslate && t.Exists() && t.IsObject() {
|
||||||
if t.Get("type").String() == "enabled" {
|
switch t.Get("type").String() {
|
||||||
|
case "enabled":
|
||||||
if b := t.Get("budget_tokens"); b.Exists() && b.Type == gjson.Number {
|
if b := t.Get("budget_tokens"); b.Exists() && b.Type == gjson.Number {
|
||||||
budget := int(b.Int())
|
budget := int(b.Int())
|
||||||
out, _ = sjson.Set(out, "request.generationConfig.thinkingConfig.thinkingBudget", budget)
|
out, _ = sjson.Set(out, "request.generationConfig.thinkingConfig.thinkingBudget", budget)
|
||||||
out, _ = sjson.Set(out, "request.generationConfig.thinkingConfig.includeThoughts", true)
|
out, _ = sjson.Set(out, "request.generationConfig.thinkingConfig.includeThoughts", true)
|
||||||
}
|
}
|
||||||
|
case "adaptive":
|
||||||
|
// Keep adaptive as a high level sentinel; ApplyThinking resolves it
|
||||||
|
// to model-specific max capability.
|
||||||
|
out, _ = sjson.Set(out, "request.generationConfig.thinkingConfig.thinkingLevel", "high")
|
||||||
|
out, _ = sjson.Set(out, "request.generationConfig.thinkingConfig.includeThoughts", true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if v := gjson.GetBytes(rawJSON, "temperature"); v.Exists() && v.Type == gjson.Number {
|
if v := gjson.GetBytes(rawJSON, "temperature"); v.Exists() && v.Type == gjson.Number {
|
||||||
|
|||||||
@@ -222,6 +222,10 @@ func ConvertClaudeRequestToCodex(modelName string, inputRawJSON []byte, _ bool)
|
|||||||
reasoningEffort = effort
|
reasoningEffort = effort
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
case "adaptive":
|
||||||
|
// Claude adaptive means "enable with max capacity"; keep it as highest level
|
||||||
|
// and let ApplyThinking normalize per target model capability.
|
||||||
|
reasoningEffort = string(thinking.LevelXHigh)
|
||||||
case "disabled":
|
case "disabled":
|
||||||
if effort, ok := thinking.ConvertBudgetToLevel(0); ok && effort != "" {
|
if effort, ok := thinking.ConvertBudgetToLevel(0); ok && effort != "" {
|
||||||
reasoningEffort = effort
|
reasoningEffort = effort
|
||||||
|
|||||||
@@ -173,12 +173,18 @@ func ConvertClaudeRequestToCLI(modelName string, inputRawJSON []byte, _ bool) []
|
|||||||
|
|
||||||
// Map Anthropic thinking -> Gemini thinkingBudget/include_thoughts when type==enabled
|
// Map Anthropic thinking -> Gemini thinkingBudget/include_thoughts when type==enabled
|
||||||
if t := gjson.GetBytes(rawJSON, "thinking"); t.Exists() && t.IsObject() {
|
if t := gjson.GetBytes(rawJSON, "thinking"); t.Exists() && t.IsObject() {
|
||||||
if t.Get("type").String() == "enabled" {
|
switch t.Get("type").String() {
|
||||||
|
case "enabled":
|
||||||
if b := t.Get("budget_tokens"); b.Exists() && b.Type == gjson.Number {
|
if b := t.Get("budget_tokens"); b.Exists() && b.Type == gjson.Number {
|
||||||
budget := int(b.Int())
|
budget := int(b.Int())
|
||||||
out, _ = sjson.Set(out, "request.generationConfig.thinkingConfig.thinkingBudget", budget)
|
out, _ = sjson.Set(out, "request.generationConfig.thinkingConfig.thinkingBudget", budget)
|
||||||
out, _ = sjson.Set(out, "request.generationConfig.thinkingConfig.includeThoughts", true)
|
out, _ = sjson.Set(out, "request.generationConfig.thinkingConfig.includeThoughts", true)
|
||||||
}
|
}
|
||||||
|
case "adaptive":
|
||||||
|
// Keep adaptive as a high level sentinel; ApplyThinking resolves it
|
||||||
|
// to model-specific max capability.
|
||||||
|
out, _ = sjson.Set(out, "request.generationConfig.thinkingConfig.thinkingLevel", "high")
|
||||||
|
out, _ = sjson.Set(out, "request.generationConfig.thinkingConfig.includeThoughts", true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if v := gjson.GetBytes(rawJSON, "temperature"); v.Exists() && v.Type == gjson.Number {
|
if v := gjson.GetBytes(rawJSON, "temperature"); v.Exists() && v.Type == gjson.Number {
|
||||||
|
|||||||
@@ -154,12 +154,18 @@ func ConvertClaudeRequestToGemini(modelName string, inputRawJSON []byte, _ bool)
|
|||||||
// Map Anthropic thinking -> Gemini thinkingBudget/include_thoughts when enabled
|
// Map Anthropic thinking -> Gemini thinkingBudget/include_thoughts when enabled
|
||||||
// Translator only does format conversion, ApplyThinking handles model capability validation.
|
// 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() {
|
||||||
if t.Get("type").String() == "enabled" {
|
switch t.Get("type").String() {
|
||||||
|
case "enabled":
|
||||||
if b := t.Get("budget_tokens"); b.Exists() && b.Type == gjson.Number {
|
if b := t.Get("budget_tokens"); b.Exists() && b.Type == gjson.Number {
|
||||||
budget := int(b.Int())
|
budget := int(b.Int())
|
||||||
out, _ = sjson.Set(out, "generationConfig.thinkingConfig.thinkingBudget", budget)
|
out, _ = sjson.Set(out, "generationConfig.thinkingConfig.thinkingBudget", budget)
|
||||||
out, _ = sjson.Set(out, "generationConfig.thinkingConfig.includeThoughts", true)
|
out, _ = sjson.Set(out, "generationConfig.thinkingConfig.includeThoughts", true)
|
||||||
}
|
}
|
||||||
|
case "adaptive":
|
||||||
|
// Keep adaptive as a high level sentinel; ApplyThinking resolves it
|
||||||
|
// to model-specific max capability.
|
||||||
|
out, _ = sjson.Set(out, "generationConfig.thinkingConfig.thinkingLevel", "high")
|
||||||
|
out, _ = sjson.Set(out, "generationConfig.thinkingConfig.includeThoughts", true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if v := gjson.GetBytes(rawJSON, "temperature"); v.Exists() && v.Type == gjson.Number {
|
if v := gjson.GetBytes(rawJSON, "temperature"); v.Exists() && v.Type == gjson.Number {
|
||||||
|
|||||||
@@ -75,6 +75,10 @@ func ConvertClaudeRequestToOpenAI(modelName string, inputRawJSON []byte, stream
|
|||||||
out, _ = sjson.Set(out, "reasoning_effort", effort)
|
out, _ = sjson.Set(out, "reasoning_effort", effort)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
case "adaptive":
|
||||||
|
// Claude adaptive means "enable with max capacity"; keep it as highest level
|
||||||
|
// and let ApplyThinking normalize per target model capability.
|
||||||
|
out, _ = sjson.Set(out, "reasoning_effort", string(thinking.LevelXHigh))
|
||||||
case "disabled":
|
case "disabled":
|
||||||
if effort, ok := thinking.ConvertBudgetToLevel(0); ok && effort != "" {
|
if effort, ok := thinking.ConvertBudgetToLevel(0); ok && effort != "" {
|
||||||
out, _ = sjson.Set(out, "reasoning_effort", effort)
|
out, _ = sjson.Set(out, "reasoning_effort", effort)
|
||||||
|
|||||||
@@ -2590,6 +2590,135 @@ func TestThinkingE2EMatrix_Body(t *testing.T) {
|
|||||||
runThinkingTests(t, cases)
|
runThinkingTests(t, cases)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestThinkingE2EClaudeAdaptive_Body tests Claude thinking.type=adaptive extended body-only cases.
|
||||||
|
// These cases validate that adaptive means "thinking enabled without explicit budget", and
|
||||||
|
// cross-protocol conversion should resolve to target-model maximum thinking capability.
|
||||||
|
func TestThinkingE2EClaudeAdaptive_Body(t *testing.T) {
|
||||||
|
reg := registry.GetGlobalRegistry()
|
||||||
|
uid := fmt.Sprintf("thinking-e2e-claude-adaptive-%d", time.Now().UnixNano())
|
||||||
|
|
||||||
|
reg.RegisterClient(uid, "test", getTestModels())
|
||||||
|
defer reg.UnregisterClient(uid)
|
||||||
|
|
||||||
|
cases := []thinkingTestCase{
|
||||||
|
// A1: Claude adaptive to OpenAI level model -> highest supported level
|
||||||
|
{
|
||||||
|
name: "A1",
|
||||||
|
from: "claude",
|
||||||
|
to: "openai",
|
||||||
|
model: "level-model",
|
||||||
|
inputJSON: `{"model":"level-model","messages":[{"role":"user","content":"hi"}],"thinking":{"type":"adaptive"}}`,
|
||||||
|
expectField: "reasoning_effort",
|
||||||
|
expectValue: "high",
|
||||||
|
expectErr: false,
|
||||||
|
},
|
||||||
|
// A2: Claude adaptive to Gemini level subset model -> highest supported level
|
||||||
|
{
|
||||||
|
name: "A2",
|
||||||
|
from: "claude",
|
||||||
|
to: "gemini",
|
||||||
|
model: "level-subset-model",
|
||||||
|
inputJSON: `{"model":"level-subset-model","messages":[{"role":"user","content":"hi"}],"thinking":{"type":"adaptive"}}`,
|
||||||
|
expectField: "generationConfig.thinkingConfig.thinkingLevel",
|
||||||
|
expectValue: "high",
|
||||||
|
includeThoughts: "true",
|
||||||
|
expectErr: false,
|
||||||
|
},
|
||||||
|
// A3: Claude adaptive to Gemini budget model -> max budget
|
||||||
|
{
|
||||||
|
name: "A3",
|
||||||
|
from: "claude",
|
||||||
|
to: "gemini",
|
||||||
|
model: "gemini-budget-model",
|
||||||
|
inputJSON: `{"model":"gemini-budget-model","messages":[{"role":"user","content":"hi"}],"thinking":{"type":"adaptive"}}`,
|
||||||
|
expectField: "generationConfig.thinkingConfig.thinkingBudget",
|
||||||
|
expectValue: "20000",
|
||||||
|
includeThoughts: "true",
|
||||||
|
expectErr: false,
|
||||||
|
},
|
||||||
|
// A4: Claude adaptive to Gemini mixed model -> highest supported level
|
||||||
|
{
|
||||||
|
name: "A4",
|
||||||
|
from: "claude",
|
||||||
|
to: "gemini",
|
||||||
|
model: "gemini-mixed-model",
|
||||||
|
inputJSON: `{"model":"gemini-mixed-model","messages":[{"role":"user","content":"hi"}],"thinking":{"type":"adaptive"}}`,
|
||||||
|
expectField: "generationConfig.thinkingConfig.thinkingLevel",
|
||||||
|
expectValue: "high",
|
||||||
|
includeThoughts: "true",
|
||||||
|
expectErr: false,
|
||||||
|
},
|
||||||
|
// A5: Claude adaptive passthrough for same protocol
|
||||||
|
{
|
||||||
|
name: "A5",
|
||||||
|
from: "claude",
|
||||||
|
to: "claude",
|
||||||
|
model: "claude-budget-model",
|
||||||
|
inputJSON: `{"model":"claude-budget-model","messages":[{"role":"user","content":"hi"}],"thinking":{"type":"adaptive"}}`,
|
||||||
|
expectField: "thinking.type",
|
||||||
|
expectValue: "adaptive",
|
||||||
|
expectErr: false,
|
||||||
|
},
|
||||||
|
// A6: Claude adaptive to Antigravity budget model -> max budget
|
||||||
|
{
|
||||||
|
name: "A6",
|
||||||
|
from: "claude",
|
||||||
|
to: "antigravity",
|
||||||
|
model: "antigravity-budget-model",
|
||||||
|
inputJSON: `{"model":"antigravity-budget-model","messages":[{"role":"user","content":"hi"}],"thinking":{"type":"adaptive"}}`,
|
||||||
|
expectField: "request.generationConfig.thinkingConfig.thinkingBudget",
|
||||||
|
expectValue: "20000",
|
||||||
|
includeThoughts: "true",
|
||||||
|
expectErr: false,
|
||||||
|
},
|
||||||
|
// A7: Claude adaptive to iFlow GLM -> enabled boolean
|
||||||
|
{
|
||||||
|
name: "A7",
|
||||||
|
from: "claude",
|
||||||
|
to: "iflow",
|
||||||
|
model: "glm-test",
|
||||||
|
inputJSON: `{"model":"glm-test","messages":[{"role":"user","content":"hi"}],"thinking":{"type":"adaptive"}}`,
|
||||||
|
expectField: "chat_template_kwargs.enable_thinking",
|
||||||
|
expectValue: "true",
|
||||||
|
expectErr: false,
|
||||||
|
},
|
||||||
|
// A8: Claude adaptive to iFlow MiniMax -> enabled boolean
|
||||||
|
{
|
||||||
|
name: "A8",
|
||||||
|
from: "claude",
|
||||||
|
to: "iflow",
|
||||||
|
model: "minimax-test",
|
||||||
|
inputJSON: `{"model":"minimax-test","messages":[{"role":"user","content":"hi"}],"thinking":{"type":"adaptive"}}`,
|
||||||
|
expectField: "reasoning_split",
|
||||||
|
expectValue: "true",
|
||||||
|
expectErr: false,
|
||||||
|
},
|
||||||
|
// A9: Claude adaptive to Codex level model -> highest supported level
|
||||||
|
{
|
||||||
|
name: "A9",
|
||||||
|
from: "claude",
|
||||||
|
to: "codex",
|
||||||
|
model: "level-model",
|
||||||
|
inputJSON: `{"model":"level-model","messages":[{"role":"user","content":"hi"}],"thinking":{"type":"adaptive"}}`,
|
||||||
|
expectField: "reasoning.effort",
|
||||||
|
expectValue: "high",
|
||||||
|
expectErr: false,
|
||||||
|
},
|
||||||
|
// A10: Claude adaptive on non-thinking model should still be stripped
|
||||||
|
{
|
||||||
|
name: "A10",
|
||||||
|
from: "claude",
|
||||||
|
to: "openai",
|
||||||
|
model: "no-thinking-model",
|
||||||
|
inputJSON: `{"model":"no-thinking-model","messages":[{"role":"user","content":"hi"}],"thinking":{"type":"adaptive"}}`,
|
||||||
|
expectField: "",
|
||||||
|
expectErr: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
runThinkingTests(t, cases)
|
||||||
|
}
|
||||||
|
|
||||||
// getTestModels returns the shared model definitions for E2E tests.
|
// getTestModels returns the shared model definitions for E2E tests.
|
||||||
func getTestModels() []*registry.ModelInfo {
|
func getTestModels() []*registry.ModelInfo {
|
||||||
return []*registry.ModelInfo{
|
return []*registry.ModelInfo{
|
||||||
|
|||||||
Reference in New Issue
Block a user