feat(translator): support Claude thinking type adaptive
Unified · Split
| @@ -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": {}, |
| @@ -344,7 +344,8 @@ func ConvertClaudeRequestToAntigravity(modelName string, inputRawJSON []byte, _ · 343 unmodified lines | |||
| 344 | // Inject interleaved thinking hint when both tools and thinking are active | 344 | // Inject interleaved thinking hint when both tools and thinking are active |
| 345 | hasTools := toolDeclCount > 0 | 345 | 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==enabled | 379 | // 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 { |
| @@ -222,6 +222,10 @@ func ConvertClaudeRequestToCodex(modelName string, inputRawJSON []byte, _ bool) · 221 unmodified lines | |||
| 222 | reasoningEffort = effort | 222 | 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 = effort | 231 | reasoningEffort = effort |
| @@ -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==enabled | 174 | // 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 { |
| @@ -154,12 +154,18 @@ func ConvertClaudeRequestToGemini(modelName string, inputRawJSON []byte, _ bool) · 153 unmodified lines | |||
| 154 | // Map Anthropic thinking -> Gemini thinkingBudget/include_thoughts when enabled | 154 | // 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 { |
| @@ -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) |
| @@ -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. | ||
| 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 | |||
| 2593 | // getTestModels returns the shared model definitions for E2E tests. | 2722 | // getTestModels returns the shared model definitions for E2E tests. |
| 2594 | func getTestModels() []*registry.ModelInfo { | 2723 | func getTestModels() []*registry.ModelInfo { |
| 2595 | return []*registry.ModelInfo{ | 2724 | return []*registry.ModelInfo{ |