History

Merge pull request #1519 from router-for-me/thinking

feat(translator): support Claude thinking type adaptive

Chén Mù committed 9 months agov6.8.10

Commit c6bd91b2 parents 0040d78 349ddca7 files · +161 −5Browse files

Unified · Split

7 files
  1. internal/registry/model_definitions_static_data.go +1 −1
  2. internal/translator/antigravity/claude/antigravity_claude_request.go +9 −2
  3. internal/translator/codex/claude/codex_claude_request.go +4 −0
  4. internal/translator/gemini-cli/claude/gemini-cli_claude_request.go +7 −1
  5. internal/translator/gemini/claude/gemini_claude_request.go +7 −1
  6. internal/translator/openai/claude/openai_claude_request.go +4 −0
  7. test/thinking_conversion_test.go +129 −0
internal/registry/model_definitions_static_data.go+1−1
@@ -866,7 +866,7 @@ func GetAntigravityModelConfig() map[string]*AntigravityModelConfig { · 865 unmodified lines
866866 "gemini-3-flash": {Thinking: &ThinkingSupport{Min: 128, Max: 32768, ZeroAllowed: false, DynamicAllowed: true, Levels: []string{"minimal", "low", "medium", "high"}}},
867867 "claude-sonnet-4-5-thinking": {Thinking: &ThinkingSupport{Min: 1024, Max: 128000, ZeroAllowed: true, DynamicAllowed: true}, MaxCompletionTokens: 64000},
868868 "claude-opus-4-5-thinking": {Thinking: &ThinkingSupport{Min: 1024, Max: 128000, ZeroAllowed: true, DynamicAllowed: true}, MaxCompletionTokens: 64000},
869− "claude-opus-4-6-thinking": {Thinking: &ThinkingSupport{Min: 1024, Max: 128000, ZeroAllowed: true, DynamicAllowed: true}, MaxCompletionTokens: 128000},
869+ "claude-opus-4-6-thinking": {Thinking: &ThinkingSupport{Min: 1024, Max: 128000, ZeroAllowed: true, DynamicAllowed: true}, MaxCompletionTokens: 64000},
870870 "claude-sonnet-4-5": {MaxCompletionTokens: 64000},
871871 "gpt-oss-120b-medium": {},
872872 "tab_flash_lite_preview": {},
internal/translator/antigravity/claude/antigravity_claude_request.go+9−2
@@ -344,7 +344,8 @@ func ConvertClaudeRequestToAntigravity(modelName string, inputRawJSON []byte, _ · 343 unmodified lines
344344 // Inject interleaved thinking hint when both tools and thinking are active
345345 hasTools := toolDeclCount > 0
346346 thinkingResult := gjson.GetBytes(rawJSON, "thinking")
347− hasThinking := thinkingResult.Exists() && thinkingResult.IsObject() && thinkingResult.Get("type").String() == "enabled"
347+ thinkingType := thinkingResult.Get("type").String()
348+ hasThinking := thinkingResult.Exists() && thinkingResult.IsObject() && (thinkingType == "enabled" || thinkingType == "adaptive")
348349 isClaudeThinking := util.IsClaudeThinkingModel(modelName)
349350
350351 if hasTools && hasThinking && isClaudeThinking {
@@ -377,12 +378,18 @@ func ConvertClaudeRequestToAntigravity(modelName string, inputRawJSON []byte, _ · 26 unmodified lines
377378
378379 // Map Anthropic thinking -> Gemini thinkingBudget/include_thoughts when type==enabled
379380 if t := gjson.GetBytes(rawJSON, "thinking"); enableThoughtTranslate && t.Exists() && t.IsObject() {
380− if t.Get("type").String() == "enabled" {
381+ switch t.Get("type").String() {
382+ case "enabled":
381383 if b := t.Get("budget_tokens"); b.Exists() && b.Type == gjson.Number {
382384 budget := int(b.Int())
383385 out, _ = sjson.Set(out, "request.generationConfig.thinkingConfig.thinkingBudget", budget)
384386 out, _ = sjson.Set(out, "request.generationConfig.thinkingConfig.includeThoughts", true)
385387 }
388+ case "adaptive":
389+ // Keep adaptive as a high level sentinel; ApplyThinking resolves it
390+ // to model-specific max capability.
391+ out, _ = sjson.Set(out, "request.generationConfig.thinkingConfig.thinkingLevel", "high")
392+ out, _ = sjson.Set(out, "request.generationConfig.thinkingConfig.includeThoughts", true)
386393 }
387394 }
388395 if v := gjson.GetBytes(rawJSON, "temperature"); v.Exists() && v.Type == gjson.Number {
internal/translator/codex/claude/codex_claude_request.go+4−0
@@ -222,6 +222,10 @@ func ConvertClaudeRequestToCodex(modelName string, inputRawJSON []byte, _ bool) · 221 unmodified lines
222222 reasoningEffort = effort
223223 }
224224 }
225+ case "adaptive":
226+ // Claude adaptive means "enable with max capacity"; keep it as highest level
227+ // and let ApplyThinking normalize per target model capability.
228+ reasoningEffort = string(thinking.LevelXHigh)
225229 case "disabled":
226230 if effort, ok := thinking.ConvertBudgetToLevel(0); ok && effort != "" {
227231 reasoningEffort = effort
internal/translator/gemini-cli/claude/gemini-cli_claude_request.go+7−1
@@ -173,12 +173,18 @@ func ConvertClaudeRequestToCLI(modelName string, inputRawJSON []byte, _ bool) [] · 172 unmodified lines
173173
174174 // Map Anthropic thinking -> Gemini thinkingBudget/include_thoughts when type==enabled
175175 if t := gjson.GetBytes(rawJSON, "thinking"); t.Exists() && t.IsObject() {
176− if t.Get("type").String() == "enabled" {
176+ switch t.Get("type").String() {
177+ case "enabled":
177178 if b := t.Get("budget_tokens"); b.Exists() && b.Type == gjson.Number {
178179 budget := int(b.Int())
179180 out, _ = sjson.Set(out, "request.generationConfig.thinkingConfig.thinkingBudget", budget)
180181 out, _ = sjson.Set(out, "request.generationConfig.thinkingConfig.includeThoughts", true)
181182 }
183+ case "adaptive":
184+ // Keep adaptive as a high level sentinel; ApplyThinking resolves it
185+ // to model-specific max capability.
186+ out, _ = sjson.Set(out, "request.generationConfig.thinkingConfig.thinkingLevel", "high")
187+ out, _ = sjson.Set(out, "request.generationConfig.thinkingConfig.includeThoughts", true)
182188 }
183189 }
184190 if v := gjson.GetBytes(rawJSON, "temperature"); v.Exists() && v.Type == gjson.Number {
internal/translator/gemini/claude/gemini_claude_request.go+7−1
@@ -154,12 +154,18 @@ func ConvertClaudeRequestToGemini(modelName string, inputRawJSON []byte, _ bool) · 153 unmodified lines
154154 // Map Anthropic thinking -> Gemini thinkingBudget/include_thoughts when enabled
155155 // Translator only does format conversion, ApplyThinking handles model capability validation.
156156 if t := gjson.GetBytes(rawJSON, "thinking"); t.Exists() && t.IsObject() {
157− if t.Get("type").String() == "enabled" {
157+ switch t.Get("type").String() {
158+ case "enabled":
158159 if b := t.Get("budget_tokens"); b.Exists() && b.Type == gjson.Number {
159160 budget := int(b.Int())
160161 out, _ = sjson.Set(out, "generationConfig.thinkingConfig.thinkingBudget", budget)
161162 out, _ = sjson.Set(out, "generationConfig.thinkingConfig.includeThoughts", true)
162163 }
164+ case "adaptive":
165+ // Keep adaptive as a high level sentinel; ApplyThinking resolves it
166+ // to model-specific max capability.
167+ out, _ = sjson.Set(out, "generationConfig.thinkingConfig.thinkingLevel", "high")
168+ out, _ = sjson.Set(out, "generationConfig.thinkingConfig.includeThoughts", true)
163169 }
164170 }
165171 if v := gjson.GetBytes(rawJSON, "temperature"); v.Exists() && v.Type == gjson.Number {
internal/translator/openai/claude/openai_claude_request.go+4−0
@@ -75,6 +75,10 @@ func ConvertClaudeRequestToOpenAI(modelName string, inputRawJSON []byte, stream · 74 unmodified lines
7575 out, _ = sjson.Set(out, "reasoning_effort", effort)
7676 }
7777 }
78+ case "adaptive":
79+ // Claude adaptive means "enable with max capacity"; keep it as highest level
80+ // and let ApplyThinking normalize per target model capability.
81+ out, _ = sjson.Set(out, "reasoning_effort", string(thinking.LevelXHigh))
7882 case "disabled":
7983 if effort, ok := thinking.ConvertBudgetToLevel(0); ok && effort != "" {
8084 out, _ = sjson.Set(out, "reasoning_effort", effort)
test/thinking_conversion_test.go+129−0
@@ -2590,6 +2590,135 @@ func TestThinkingE2EMatrix_Body(t *testing.T) { · 2589 unmodified lines
25902590 runThinkingTests(t, cases)
25912591 }
25922592
2593+// TestThinkingE2EClaudeAdaptive_Body tests Claude thinking.type=adaptive extended body-only cases.
2594+// These cases validate that adaptive means "thinking enabled without explicit budget", and
2595+// cross-protocol conversion should resolve to target-model maximum thinking capability.
2596+func TestThinkingE2EClaudeAdaptive_Body(t *testing.T) {
2597+ reg := registry.GetGlobalRegistry()
2598+ uid := fmt.Sprintf("thinking-e2e-claude-adaptive-%d", time.Now().UnixNano())
2599+
2600+ reg.RegisterClient(uid, "test", getTestModels())
2601+ defer reg.UnregisterClient(uid)
2602+
2603+ cases := []thinkingTestCase{
2604+ // A1: Claude adaptive to OpenAI level model -> highest supported level
2605+ {
2606+ name: "A1",
2607+ from: "claude",
2608+ to: "openai",
2609+ model: "level-model",
2610+ inputJSON: `{"model":"level-model","messages":[{"role":"user","content":"hi"}],"thinking":{"type":"adaptive"}}`,
2611+ expectField: "reasoning_effort",
2612+ expectValue: "high",
2613+ expectErr: false,
2614+ },
2615+ // A2: Claude adaptive to Gemini level subset model -> highest supported level
2616+ {
2617+ name: "A2",
2618+ from: "claude",
2619+ to: "gemini",
2620+ model: "level-subset-model",
2621+ inputJSON: `{"model":"level-subset-model","messages":[{"role":"user","content":"hi"}],"thinking":{"type":"adaptive"}}`,
2622+ expectField: "generationConfig.thinkingConfig.thinkingLevel",
2623+ expectValue: "high",
2624+ includeThoughts: "true",
2625+ expectErr: false,
2626+ },
2627+ // A3: Claude adaptive to Gemini budget model -> max budget
2628+ {
2629+ name: "A3",
2630+ from: "claude",
2631+ to: "gemini",
2632+ model: "gemini-budget-model",
2633+ inputJSON: `{"model":"gemini-budget-model","messages":[{"role":"user","content":"hi"}],"thinking":{"type":"adaptive"}}`,
2634+ expectField: "generationConfig.thinkingConfig.thinkingBudget",
2635+ expectValue: "20000",
2636+ includeThoughts: "true",
2637+ expectErr: false,
2638+ },
2639+ // A4: Claude adaptive to Gemini mixed model -> highest supported level
2640+ {
2641+ name: "A4",
2642+ from: "claude",
2643+ to: "gemini",
2644+ model: "gemini-mixed-model",
2645+ inputJSON: `{"model":"gemini-mixed-model","messages":[{"role":"user","content":"hi"}],"thinking":{"type":"adaptive"}}`,
2646+ expectField: "generationConfig.thinkingConfig.thinkingLevel",
2647+ expectValue: "high",
2648+ includeThoughts: "true",
2649+ expectErr: false,
2650+ },
2651+ // A5: Claude adaptive passthrough for same protocol
2652+ {
2653+ name: "A5",
2654+ from: "claude",
2655+ to: "claude",
2656+ model: "claude-budget-model",
2657+ inputJSON: `{"model":"claude-budget-model","messages":[{"role":"user","content":"hi"}],"thinking":{"type":"adaptive"}}`,
2658+ expectField: "thinking.type",
2659+ expectValue: "adaptive",
2660+ expectErr: false,
2661+ },
2662+ // A6: Claude adaptive to Antigravity budget model -> max budget
2663+ {
2664+ name: "A6",
2665+ from: "claude",
2666+ to: "antigravity",
2667+ model: "antigravity-budget-model",
2668+ inputJSON: `{"model":"antigravity-budget-model","messages":[{"role":"user","content":"hi"}],"thinking":{"type":"adaptive"}}`,
2669+ expectField: "request.generationConfig.thinkingConfig.thinkingBudget",
2670+ expectValue: "20000",
2671+ includeThoughts: "true",
2672+ expectErr: false,
2673+ },
2674+ // A7: Claude adaptive to iFlow GLM -> enabled boolean
2675+ {
2676+ name: "A7",
2677+ from: "claude",
2678+ to: "iflow",
2679+ model: "glm-test",
2680+ inputJSON: `{"model":"glm-test","messages":[{"role":"user","content":"hi"}],"thinking":{"type":"adaptive"}}`,
2681+ expectField: "chat_template_kwargs.enable_thinking",
2682+ expectValue: "true",
2683+ expectErr: false,
2684+ },
2685+ // A8: Claude adaptive to iFlow MiniMax -> enabled boolean
2686+ {
2687+ name: "A8",
2688+ from: "claude",
2689+ to: "iflow",
2690+ model: "minimax-test",
2691+ inputJSON: `{"model":"minimax-test","messages":[{"role":"user","content":"hi"}],"thinking":{"type":"adaptive"}}`,
2692+ expectField: "reasoning_split",
2693+ expectValue: "true",
2694+ expectErr: false,
2695+ },
2696+ // A9: Claude adaptive to Codex level model -> highest supported level
2697+ {
2698+ name: "A9",
2699+ from: "claude",
2700+ to: "codex",
2701+ model: "level-model",
2702+ inputJSON: `{"model":"level-model","messages":[{"role":"user","content":"hi"}],"thinking":{"type":"adaptive"}}`,
2703+ expectField: "reasoning.effort",
2704+ expectValue: "high",
2705+ expectErr: false,
2706+ },
2707+ // A10: Claude adaptive on non-thinking model should still be stripped
2708+ {
2709+ name: "A10",
2710+ from: "claude",
2711+ to: "openai",
2712+ model: "no-thinking-model",
2713+ inputJSON: `{"model":"no-thinking-model","messages":[{"role":"user","content":"hi"}],"thinking":{"type":"adaptive"}}`,
2714+ expectField: "",
2715+ expectErr: false,
2716+ },
2717+ }
2718+
2719+ runThinkingTests(t, cases)
2720+}
2721+
25932722 // getTestModels returns the shared model definitions for E2E tests.
25942723 func getTestModels() []*registry.ModelInfo {
25952724 return []*registry.ModelInfo{

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t