feat(translator): support Claude thinking type adaptive
Unified · Split
| @@ -866,7 +866,7 @@ func GetAntigravityModelConfig() map[string]*AntigravityModelConfig { · 865 unmodified lines | ||
| 866 | 866 | "gemini-3-flash": {Thinking: &ThinkingSupport{Min: 128, Max: 32768, ZeroAllowed: false, DynamicAllowed: true, Levels: []string{"minimal", "low", "medium", "high"}}}, |
| 867 | 867 | "claude-sonnet-4-5-thinking": {Thinking: &ThinkingSupport{Min: 1024, Max: 128000, ZeroAllowed: true, DynamicAllowed: true}, MaxCompletionTokens: 64000}, |
| 868 | 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 | 870 | "claude-sonnet-4-5": {MaxCompletionTokens: 64000}, |
| 871 | 871 | "gpt-oss-120b-medium": {}, |
| 872 | 872 | "tab_flash_lite_preview": {}, |
| @@ -344,7 +344,8 @@ func ConvertClaudeRequestToAntigravity(modelName string, inputRawJSON []byte, _ · 343 unmodified lines | ||
| 344 | 344 | // Inject interleaved thinking hint when both tools and thinking are active |
| 345 | 345 | hasTools := toolDeclCount > 0 |
| 346 | 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 | 349 | isClaudeThinking := util.IsClaudeThinkingModel(modelName) |
| 349 | 350 | |
| 350 | 351 | if hasTools && hasThinking && isClaudeThinking { |
| @@ -377,12 +378,18 @@ func ConvertClaudeRequestToAntigravity(modelName string, inputRawJSON []byte, _ · 26 unmodified lines | ||
| 377 | 378 | |
| 378 | 379 | // Map Anthropic thinking -> Gemini thinkingBudget/include_thoughts when type==enabled |
| 379 | 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 | 383 | if b := t.Get("budget_tokens"); b.Exists() && b.Type == gjson.Number { |
| 382 | 384 | budget := int(b.Int()) |
| 383 | 385 | out, _ = sjson.Set(out, "request.generationConfig.thinkingConfig.thinkingBudget", budget) |
| 384 | 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 | 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 | 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 | 229 | case "disabled": |
| 226 | 230 | if effort, ok := thinking.ConvertBudgetToLevel(0); ok && effort != "" { |
| 227 | 231 | reasoningEffort = effort |
| @@ -173,12 +173,18 @@ func ConvertClaudeRequestToCLI(modelName string, inputRawJSON []byte, _ bool) [] · 172 unmodified lines | ||
| 173 | 173 | |
| 174 | 174 | // Map Anthropic thinking -> Gemini thinkingBudget/include_thoughts when type==enabled |
| 175 | 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 | 178 | if b := t.Get("budget_tokens"); b.Exists() && b.Type == gjson.Number { |
| 178 | 179 | budget := int(b.Int()) |
| 179 | 180 | out, _ = sjson.Set(out, "request.generationConfig.thinkingConfig.thinkingBudget", budget) |
| 180 | 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 | 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 | 154 | // Map Anthropic thinking -> Gemini thinkingBudget/include_thoughts when enabled |
| 155 | 155 | // Translator only does format conversion, ApplyThinking handles model capability validation. |
| 156 | 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 | 159 | if b := t.Get("budget_tokens"); b.Exists() && b.Type == gjson.Number { |
| 159 | 160 | budget := int(b.Int()) |
| 160 | 161 | out, _ = sjson.Set(out, "generationConfig.thinkingConfig.thinkingBudget", budget) |
| 161 | 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 | 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 | 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 | 82 | case "disabled": |
| 79 | 83 | if effort, ok := thinking.ConvertBudgetToLevel(0); ok && effort != "" { |
| 80 | 84 | out, _ = sjson.Set(out, "reasoning_effort", effort) |
| @@ -2590,6 +2590,135 @@ func TestThinkingE2EMatrix_Body(t *testing.T) { · 2589 unmodified lines | ||
| 2590 | 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 | 2722 | // getTestModels returns the shared model definitions for E2E tests. |
| 2594 | 2723 | func getTestModels() []*registry.ModelInfo { |
| 2595 | 2724 | return []*registry.ModelInfo{ |