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
866 "gemini-3-flash": {Thinking: &ThinkingSupport{Min: 128, Max: 32768, ZeroAllowed: false, DynamicAllowed: true, Levels: []string{"minimal", "low", "medium", "high"}}},866 "gemini-3-flash": {Thinking: &ThinkingSupport{Min: 128, Max: 32768, ZeroAllowed: false, DynamicAllowed: true, Levels: []string{"minimal", "low", "medium", "high"}}},
867 "claude-sonnet-4-5-thinking": {Thinking: &ThinkingSupport{Min: 1024, Max: 128000, ZeroAllowed: true, DynamicAllowed: true}, MaxCompletionTokens: 64000},867 "claude-sonnet-4-5-thinking": {Thinking: &ThinkingSupport{Min: 1024, Max: 128000, ZeroAllowed: true, DynamicAllowed: true}, MaxCompletionTokens: 64000},
868 "claude-opus-4-5-thinking": {Thinking: &ThinkingSupport{Min: 1024, Max: 128000, ZeroAllowed: true, DynamicAllowed: true}, MaxCompletionTokens: 64000},868 "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},
870 "claude-sonnet-4-5": {MaxCompletionTokens: 64000},870 "claude-sonnet-4-5": {MaxCompletionTokens: 64000},
871 "gpt-oss-120b-medium": {},871 "gpt-oss-120b-medium": {},
872 "tab_flash_lite_preview": {},872 "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
344 // Inject interleaved thinking hint when both tools and thinking are active344 // Inject interleaved thinking hint when both tools and thinking are active
345 hasTools := toolDeclCount > 0345 hasTools := toolDeclCount > 0
346 thinkingResult := gjson.GetBytes(rawJSON, "thinking")346 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")
348 isClaudeThinking := util.IsClaudeThinkingModel(modelName)349 isClaudeThinking := util.IsClaudeThinkingModel(modelName)
349 350
350 if hasTools && hasThinking && isClaudeThinking {351 if hasTools && hasThinking && isClaudeThinking {
@@ -377,12 +378,18 @@ func ConvertClaudeRequestToAntigravity(modelName string, inputRawJSON []byte, _ · 26 unmodified lines
377 378
378 // Map Anthropic thinking -> Gemini thinkingBudget/include_thoughts when type==enabled379 // Map Anthropic thinking -> Gemini thinkingBudget/include_thoughts when type==enabled
379 if t := gjson.GetBytes(rawJSON, "thinking"); enableThoughtTranslate && t.Exists() && t.IsObject() {380 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":
381 if b := t.Get("budget_tokens"); b.Exists() && b.Type == gjson.Number {383 if b := t.Get("budget_tokens"); b.Exists() && b.Type == gjson.Number {
382 budget := int(b.Int())384 budget := int(b.Int())
383 out, _ = sjson.Set(out, "request.generationConfig.thinkingConfig.thinkingBudget", budget)385 out, _ = sjson.Set(out, "request.generationConfig.thinkingConfig.thinkingBudget", budget)
384 out, _ = sjson.Set(out, "request.generationConfig.thinkingConfig.includeThoughts", true)386 out, _ = sjson.Set(out, "request.generationConfig.thinkingConfig.includeThoughts", true)
385 }387 }
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)
386 }393 }
387 }394 }
388 if v := gjson.GetBytes(rawJSON, "temperature"); v.Exists() && v.Type == gjson.Number {395 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
222 reasoningEffort = effort222 reasoningEffort = effort
223 }223 }
224 }224 }
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)
225 case "disabled":229 case "disabled":
226 if effort, ok := thinking.ConvertBudgetToLevel(0); ok && effort != "" {230 if effort, ok := thinking.ConvertBudgetToLevel(0); ok && effort != "" {
227 reasoningEffort = effort231 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
173 173
174 // Map Anthropic thinking -> Gemini thinkingBudget/include_thoughts when type==enabled174 // Map Anthropic thinking -> Gemini thinkingBudget/include_thoughts when type==enabled
175 if t := gjson.GetBytes(rawJSON, "thinking"); t.Exists() && t.IsObject() {175 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":
177 if b := t.Get("budget_tokens"); b.Exists() && b.Type == gjson.Number {178 if b := t.Get("budget_tokens"); b.Exists() && b.Type == gjson.Number {
178 budget := int(b.Int())179 budget := int(b.Int())
179 out, _ = sjson.Set(out, "request.generationConfig.thinkingConfig.thinkingBudget", budget)180 out, _ = sjson.Set(out, "request.generationConfig.thinkingConfig.thinkingBudget", budget)
180 out, _ = sjson.Set(out, "request.generationConfig.thinkingConfig.includeThoughts", true)181 out, _ = sjson.Set(out, "request.generationConfig.thinkingConfig.includeThoughts", true)
181 }182 }
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)
182 }188 }
183 }189 }
184 if v := gjson.GetBytes(rawJSON, "temperature"); v.Exists() && v.Type == gjson.Number {190 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
154 // Map Anthropic thinking -> Gemini thinkingBudget/include_thoughts when enabled154 // Map Anthropic thinking -> Gemini thinkingBudget/include_thoughts when enabled
155 // Translator only does format conversion, ApplyThinking handles model capability validation.155 // Translator only does format conversion, ApplyThinking handles model capability validation.
156 if t := gjson.GetBytes(rawJSON, "thinking"); t.Exists() && t.IsObject() {156 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":
158 if b := t.Get("budget_tokens"); b.Exists() && b.Type == gjson.Number {159 if b := t.Get("budget_tokens"); b.Exists() && b.Type == gjson.Number {
159 budget := int(b.Int())160 budget := int(b.Int())
160 out, _ = sjson.Set(out, "generationConfig.thinkingConfig.thinkingBudget", budget)161 out, _ = sjson.Set(out, "generationConfig.thinkingConfig.thinkingBudget", budget)
161 out, _ = sjson.Set(out, "generationConfig.thinkingConfig.includeThoughts", true)162 out, _ = sjson.Set(out, "generationConfig.thinkingConfig.includeThoughts", true)
162 }163 }
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)
163 }169 }
164 }170 }
165 if v := gjson.GetBytes(rawJSON, "temperature"); v.Exists() && v.Type == gjson.Number {171 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
75 out, _ = sjson.Set(out, "reasoning_effort", effort)75 out, _ = sjson.Set(out, "reasoning_effort", effort)
76 }76 }
77 }77 }
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))
78 case "disabled":82 case "disabled":
79 if effort, ok := thinking.ConvertBudgetToLevel(0); ok && effort != "" {83 if effort, ok := thinking.ConvertBudgetToLevel(0); ok && effort != "" {
80 out, _ = sjson.Set(out, "reasoning_effort", effort)84 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
2590 runThinkingTests(t, cases)2590 runThinkingTests(t, cases)
2591}2591}
2592 2592
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.
2596func 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
2593// getTestModels returns the shared model definitions for E2E tests.2722// getTestModels returns the shared model definitions for E2E tests.
2594func getTestModels() []*registry.ModelInfo {2723func getTestModels() []*registry.ModelInfo {
2595 return []*registry.ModelInfo{2724 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