History

Merge branch 'router-for-me:main' into master

Ravens2121 committed 11 months ago

Commit f302be52 parents cd4e84a 4360ed813 files · +579 −647Browse files

Unified · Split

13 files
  1. internal/api/modules/amp/fallback_handlers.go +2 −2
  2. internal/registry/model_definitions.go +1 −341
  3. internal/runtime/executor/claude_executor.go +106 −45
  4. internal/runtime/executor/codex_executor.go +12 −84
  5. internal/runtime/executor/gemini_executor.go +8 −2
  6. internal/runtime/executor/gemini_vertex_executor.go +29 −11
  7. internal/runtime/executor/iflow_executor.go +8 −0
  8. internal/runtime/executor/openai_compat_executor.go +8 −0
  9. internal/runtime/executor/payload_helpers.go +48 −5
  10. internal/runtime/executor/qwen_executor.go +9 −0
  11. internal/util/gemini_thinking.go +0 −150
  12. internal/util/thinking_suffix.go +327 −0
  13. sdk/api/handlers/handlers.go +21 −7
internal/api/modules/amp/fallback_handlers.go+2−2
@@ -133,8 +133,8 @@ func (fh *FallbackHandler) WrapHandler(handler gin.HandlerFunc) gin.HandlerFunc · 132 unmodified lines
133 return133 return
134 }134 }
135 135
136 // Normalize model (handles Gemini thinking suffixes)136 // Normalize model (handles dynamic thinking suffixes)
137 normalizedModel, _ := util.NormalizeGeminiThinkingModel(modelName)137 normalizedModel, _ := util.NormalizeThinkingModel(modelName)
138 138
139 // Track resolved model for logging (may change if mapping is applied)139 // Track resolved model for logging (may change if mapping is applied)
140 resolvedModel := normalizedModel140 resolvedModel := normalizedModel
internal/registry/model_definitions.go+1−341
@@ -26,60 +26,6 @@ func GetClaudeModels() []*ModelInfo { · 25 unmodified lines
26 DisplayName: "Claude 4.5 Sonnet",26 DisplayName: "Claude 4.5 Sonnet",
27 ContextLength: 200000,27 ContextLength: 200000,
28 MaxCompletionTokens: 64000,28 MaxCompletionTokens: 64000,
29 },
30 {
31 ID: "claude-sonnet-4-5-thinking",
32 Object: "model",
33 Created: 1759104000, // 2025-09-29
34 OwnedBy: "anthropic",
35 Type: "claude",
36 DisplayName: "Claude 4.5 Sonnet Thinking",
37 ContextLength: 200000,
38 MaxCompletionTokens: 64000,
39 Thinking: &ThinkingSupport{Min: 1024, Max: 100000, ZeroAllowed: false, DynamicAllowed: true},
40 },
41 {
42 ID: "claude-opus-4-5-thinking",
43 Object: "model",
44 Created: 1761955200, // 2025-11-01
45 OwnedBy: "anthropic",
46 Type: "claude",
47 DisplayName: "Claude 4.5 Opus Thinking",
48 ContextLength: 200000,
49 MaxCompletionTokens: 64000,
50 Thinking: &ThinkingSupport{Min: 1024, Max: 100000, ZeroAllowed: false, DynamicAllowed: true},
51 },
52 {
53 ID: "claude-opus-4-5-thinking-low",
54 Object: "model",
55 Created: 1761955200, // 2025-11-01
56 OwnedBy: "anthropic",
57 Type: "claude",
58 DisplayName: "Claude 4.5 Opus Thinking Low",
59 ContextLength: 200000,
60 MaxCompletionTokens: 64000,
61 Thinking: &ThinkingSupport{Min: 1024, Max: 100000, ZeroAllowed: false, DynamicAllowed: true},
62 },
63 {
64 ID: "claude-opus-4-5-thinking-medium",
65 Object: "model",
66 Created: 1761955200, // 2025-11-01
67 OwnedBy: "anthropic",
68 Type: "claude",
69 DisplayName: "Claude 4.5 Opus Thinking Medium",
70 ContextLength: 200000,
71 MaxCompletionTokens: 64000,
72 Thinking: &ThinkingSupport{Min: 1024, Max: 100000, ZeroAllowed: false, DynamicAllowed: true},
73 },
74 {
75 ID: "claude-opus-4-5-thinking-high",
76 Object: "model",
77 Created: 1761955200, // 2025-11-01
78 OwnedBy: "anthropic",
79 Type: "claude",
80 DisplayName: "Claude 4.5 Opus Thinking High",
81 ContextLength: 200000,
82 MaxCompletionTokens: 64000,
83 Thinking: &ThinkingSupport{Min: 1024, Max: 100000, ZeroAllowed: false, DynamicAllowed: true},29 Thinking: &ThinkingSupport{Min: 1024, Max: 100000, ZeroAllowed: false, DynamicAllowed: true},
84 },30 },
85 {31 {
@@ -92,6 +38,7 @@ func GetClaudeModels() []*ModelInfo { · 6 unmodified lines
92 Description: "Premium model combining maximum intelligence with practical performance",38 Description: "Premium model combining maximum intelligence with practical performance",
93 ContextLength: 200000,39 ContextLength: 200000,
94 MaxCompletionTokens: 64000,40 MaxCompletionTokens: 64000,
41 Thinking: &ThinkingSupport{Min: 1024, Max: 100000, ZeroAllowed: false, DynamicAllowed: true},
95 },42 },
96 {43 {
97 ID: "claude-opus-4-1-20250805",44 ID: "claude-opus-4-1-20250805",
@@ -530,58 +477,6 @@ func GetOpenAIModels() []*ModelInfo { · 432 unmodified lines
530 MaxCompletionTokens: 128000,477 MaxCompletionTokens: 128000,
531 SupportedParameters: []string{"tools"},478 SupportedParameters: []string{"tools"},
532 },479 },
533 {
534 ID: "gpt-5-minimal",
535 Object: "model",
536 Created: 1754524800,
537 OwnedBy: "openai",
538 Type: "openai",
539 Version: "gpt-5-2025-08-07",
540 DisplayName: "GPT 5 Minimal",
541 Description: "Stable version of GPT 5, The best model for coding and agentic tasks across domains.",
542 ContextLength: 400000,
543 MaxCompletionTokens: 128000,
544 SupportedParameters: []string{"tools"},
545 },
546 {
547 ID: "gpt-5-low",
548 Object: "model",
549 Created: 1754524800,
550 OwnedBy: "openai",
551 Type: "openai",
552 Version: "gpt-5-2025-08-07",
553 DisplayName: "GPT 5 Low",
554 Description: "Stable version of GPT 5, The best model for coding and agentic tasks across domains.",
555 ContextLength: 400000,
556 MaxCompletionTokens: 128000,
557 SupportedParameters: []string{"tools"},
558 },
559 {
560 ID: "gpt-5-medium",
561 Object: "model",
562 Created: 1754524800,
563 OwnedBy: "openai",
564 Type: "openai",
565 Version: "gpt-5-2025-08-07",
566 DisplayName: "GPT 5 Medium",
567 Description: "Stable version of GPT 5, The best model for coding and agentic tasks across domains.",
568 ContextLength: 400000,
569 MaxCompletionTokens: 128000,
570 SupportedParameters: []string{"tools"},
571 },
572 {
573 ID: "gpt-5-high",
574 Object: "model",
575 Created: 1754524800,
576 OwnedBy: "openai",
577 Type: "openai",
578 Version: "gpt-5-2025-08-07",
579 DisplayName: "GPT 5 High",
580 Description: "Stable version of GPT 5, The best model for coding and agentic tasks across domains.",
581 ContextLength: 400000,
582 MaxCompletionTokens: 128000,
583 SupportedParameters: []string{"tools"},
584 },
585 {480 {
586 ID: "gpt-5-codex",481 ID: "gpt-5-codex",
587 Object: "model",482 Object: "model",
@@ -595,45 +490,6 @@ func GetOpenAIModels() []*ModelInfo { · 7 unmodified lines
595 MaxCompletionTokens: 128000,490 MaxCompletionTokens: 128000,
596 SupportedParameters: []string{"tools"},491 SupportedParameters: []string{"tools"},
597 },492 },
598 {
599 ID: "gpt-5-codex-low",
600 Object: "model",
601 Created: 1757894400,
602 OwnedBy: "openai",
603 Type: "openai",
604 Version: "gpt-5-2025-09-15",
605 DisplayName: "GPT 5 Codex Low",
606 Description: "Stable version of GPT 5 Codex, The best model for coding and agentic tasks across domains.",
607 ContextLength: 400000,
608 MaxCompletionTokens: 128000,
609 SupportedParameters: []string{"tools"},
610 },
611 {
612 ID: "gpt-5-codex-medium",
613 Object: "model",
614 Created: 1757894400,
615 OwnedBy: "openai",
616 Type: "openai",
617 Version: "gpt-5-2025-09-15",
618 DisplayName: "GPT 5 Codex Medium",
619 Description: "Stable version of GPT 5 Codex, The best model for coding and agentic tasks across domains.",
620 ContextLength: 400000,
621 MaxCompletionTokens: 128000,
622 SupportedParameters: []string{"tools"},
623 },
624 {
625 ID: "gpt-5-codex-high",
626 Object: "model",
627 Created: 1757894400,
628 OwnedBy: "openai",
629 Type: "openai",
630 Version: "gpt-5-2025-09-15",
631 DisplayName: "GPT 5 Codex High",
632 Description: "Stable version of GPT 5 Codex, The best model for coding and agentic tasks across domains.",
633 ContextLength: 400000,
634 MaxCompletionTokens: 128000,
635 SupportedParameters: []string{"tools"},
636 },
637 {493 {
638 ID: "gpt-5-codex-mini",494 ID: "gpt-5-codex-mini",
639 Object: "model",495 Object: "model",
@@ -647,32 +503,6 @@ func GetOpenAIModels() []*ModelInfo { · 7 unmodified lines
647 MaxCompletionTokens: 128000,503 MaxCompletionTokens: 128000,
648 SupportedParameters: []string{"tools"},504 SupportedParameters: []string{"tools"},
649 },505 },
650 {
651 ID: "gpt-5-codex-mini-medium",
652 Object: "model",
653 Created: 1762473600,
654 OwnedBy: "openai",
655 Type: "openai",
656 Version: "gpt-5-2025-11-07",
657 DisplayName: "GPT 5 Codex Mini Medium",
658 Description: "Stable version of GPT 5 Codex Mini: cheaper, faster, but less capable version of GPT 5 Codex.",
659 ContextLength: 400000,
660 MaxCompletionTokens: 128000,
661 SupportedParameters: []string{"tools"},
662 },
663 {
664 ID: "gpt-5-codex-mini-high",
665 Object: "model",
666 Created: 1762473600,
667 OwnedBy: "openai",
668 Type: "openai",
669 Version: "gpt-5-2025-11-07",
670 DisplayName: "GPT 5 Codex Mini High",
671 Description: "Stable version of GPT 5 Codex Mini: cheaper, faster, but less capable version of GPT 5 Codex.",
672 ContextLength: 400000,
673 MaxCompletionTokens: 128000,
674 SupportedParameters: []string{"tools"},
675 },
676 {506 {
677 ID: "gpt-5.1",507 ID: "gpt-5.1",
678 Object: "model",508 Object: "model",
@@ -686,58 +516,6 @@ func GetOpenAIModels() []*ModelInfo { · 7 unmodified lines
686 MaxCompletionTokens: 128000,516 MaxCompletionTokens: 128000,
687 SupportedParameters: []string{"tools"},517 SupportedParameters: []string{"tools"},
688 },518 },
689 {
690 ID: "gpt-5.1-none",
691 Object: "model",
692 Created: 1762905600,
693 OwnedBy: "openai",
694 Type: "openai",
695 Version: "gpt-5.1-2025-11-12",
696 DisplayName: "GPT 5.1 Nothink",
697 Description: "Stable version of GPT 5.1, The best model for coding and agentic tasks across domains.",
698 ContextLength: 400000,
699 MaxCompletionTokens: 128000,
700 SupportedParameters: []string{"tools"},
701 },
702 {
703 ID: "gpt-5.1-low",
704 Object: "model",
705 Created: 1762905600,
706 OwnedBy: "openai",
707 Type: "openai",
708 Version: "gpt-5.1-2025-11-12",
709 DisplayName: "GPT 5 Low",
710 Description: "Stable version of GPT 5, The best model for coding and agentic tasks across domains.",
711 ContextLength: 400000,
712 MaxCompletionTokens: 128000,
713 SupportedParameters: []string{"tools"},
714 },
715 {
716 ID: "gpt-5.1-medium",
717 Object: "model",
718 Created: 1762905600,
719 OwnedBy: "openai",
720 Type: "openai",
721 Version: "gpt-5.1-2025-11-12",
722 DisplayName: "GPT 5.1 Medium",
723 Description: "Stable version of GPT 5.1, The best model for coding and agentic tasks across domains.",
724 ContextLength: 400000,
725 MaxCompletionTokens: 128000,
726 SupportedParameters: []string{"tools"},
727 },
728 {
729 ID: "gpt-5.1-high",
730 Object: "model",
731 Created: 1762905600,
732 OwnedBy: "openai",
733 Type: "openai",
734 Version: "gpt-5.1-2025-11-12",
735 DisplayName: "GPT 5.1 High",
736 Description: "Stable version of GPT 5.1, The best model for coding and agentic tasks across domains.",
737 ContextLength: 400000,
738 MaxCompletionTokens: 128000,
739 SupportedParameters: []string{"tools"},
740 },
741 {519 {
742 ID: "gpt-5.1-codex",520 ID: "gpt-5.1-codex",
743 Object: "model",521 Object: "model",
@@ -751,45 +529,6 @@ func GetOpenAIModels() []*ModelInfo { · 7 unmodified lines
751 MaxCompletionTokens: 128000,529 MaxCompletionTokens: 128000,
752 SupportedParameters: []string{"tools"},530 SupportedParameters: []string{"tools"},
753 },531 },
754 {
755 ID: "gpt-5.1-codex-low",
756 Object: "model",
757 Created: 1762905600,
758 OwnedBy: "openai",
759 Type: "openai",
760 Version: "gpt-5.1-2025-11-12",
761 DisplayName: "GPT 5.1 Codex Low",
762 Description: "Stable version of GPT 5.1 Codex, The best model for coding and agentic tasks across domains.",
763 ContextLength: 400000,
764 MaxCompletionTokens: 128000,
765 SupportedParameters: []string{"tools"},
766 },
767 {
768 ID: "gpt-5.1-codex-medium",
769 Object: "model",
770 Created: 1762905600,
771 OwnedBy: "openai",
772 Type: "openai",
773 Version: "gpt-5.1-2025-11-12",
774 DisplayName: "GPT 5.1 Codex Medium",
775 Description: "Stable version of GPT 5.1 Codex, The best model for coding and agentic tasks across domains.",
776 ContextLength: 400000,
777 MaxCompletionTokens: 128000,
778 SupportedParameters: []string{"tools"},
779 },
780 {
781 ID: "gpt-5.1-codex-high",
782 Object: "model",
783 Created: 1762905600,
784 OwnedBy: "openai",
785 Type: "openai",
786 Version: "gpt-5.1-2025-11-12",
787 DisplayName: "GPT 5.1 Codex High",
788 Description: "Stable version of GPT 5.1 Codex, The best model for coding and agentic tasks across domains.",
789 ContextLength: 400000,
790 MaxCompletionTokens: 128000,
791 SupportedParameters: []string{"tools"},
792 },
793 {532 {
794 ID: "gpt-5.1-codex-mini",533 ID: "gpt-5.1-codex-mini",
795 Object: "model",534 Object: "model",
@@ -803,33 +542,6 @@ func GetOpenAIModels() []*ModelInfo { · 7 unmodified lines
803 MaxCompletionTokens: 128000,542 MaxCompletionTokens: 128000,
804 SupportedParameters: []string{"tools"},543 SupportedParameters: []string{"tools"},
805 },544 },
806 {
807 ID: "gpt-5.1-codex-mini-medium",
808 Object: "model",
809 Created: 1762905600,
810 OwnedBy: "openai",
811 Type: "openai",
812 Version: "gpt-5.1-2025-11-12",
813 DisplayName: "GPT 5.1 Codex Mini Medium",
814 Description: "Stable version of GPT 5.1 Codex Mini: cheaper, faster, but less capable version of GPT 5.1 Codex.",
815 ContextLength: 400000,
816 MaxCompletionTokens: 128000,
817 SupportedParameters: []string{"tools"},
818 },
819 {
820 ID: "gpt-5.1-codex-mini-high",
821 Object: "model",
822 Created: 1762905600,
823 OwnedBy: "openai",
824 Type: "openai",
825 Version: "gpt-5.1-2025-11-12",
826 DisplayName: "GPT 5.1 Codex Mini High",
827 Description: "Stable version of GPT 5.1 Codex Mini: cheaper, faster, but less capable version of GPT 5.1 Codex.",
828 ContextLength: 400000,
829 MaxCompletionTokens: 128000,
830 SupportedParameters: []string{"tools"},
831 },
832
833 {545 {
834 ID: "gpt-5.1-codex-max",546 ID: "gpt-5.1-codex-max",
835 Object: "model",547 Object: "model",
@@ -843,58 +555,6 @@ func GetOpenAIModels() []*ModelInfo { · 7 unmodified lines
843 MaxCompletionTokens: 128000,555 MaxCompletionTokens: 128000,
844 SupportedParameters: []string{"tools"},556 SupportedParameters: []string{"tools"},
845 },557 },
846 {
847 ID: "gpt-5.1-codex-max-low",
848 Object: "model",
849 Created: 1763424000,
850 OwnedBy: "openai",
851 Type: "openai",
852 Version: "gpt-5.1-max",
853 DisplayName: "GPT 5.1 Codex Max Low",
854 Description: "Stable version of GPT 5.1 Codex Max Low",
855 ContextLength: 400000,
856 MaxCompletionTokens: 128000,
857 SupportedParameters: []string{"tools"},
858 },
859 {
860 ID: "gpt-5.1-codex-max-medium",
861 Object: "model",
862 Created: 1763424000,
863 OwnedBy: "openai",
864 Type: "openai",
865 Version: "gpt-5.1-max",
866 DisplayName: "GPT 5.1 Codex Max Medium",
867 Description: "Stable version of GPT 5.1 Codex Max Medium",
868 ContextLength: 400000,
869 MaxCompletionTokens: 128000,
870 SupportedParameters: []string{"tools"},
871 },
872 {
873 ID: "gpt-5.1-codex-max-high",
874 Object: "model",
875 Created: 1763424000,
876 OwnedBy: "openai",
877 Type: "openai",
878 Version: "gpt-5.1-max",
879 DisplayName: "GPT 5.1 Codex Max High",
880 Description: "Stable version of GPT 5.1 Codex Max High",
881 ContextLength: 400000,
882 MaxCompletionTokens: 128000,
883 SupportedParameters: []string{"tools"},
884 },
885 {
886 ID: "gpt-5.1-codex-max-xhigh",
887 Object: "model",
888 Created: 1763424000,
889 OwnedBy: "openai",
890 Type: "openai",
891 Version: "gpt-5.1-max",
892 DisplayName: "GPT 5.1 Codex Max XHigh",
893 Description: "Stable version of GPT 5.1 Codex Max XHigh",
894 ContextLength: 400000,
895 MaxCompletionTokens: 128000,
896 SupportedParameters: []string{"tools"},
897 },
898 }558 }
899}559}
900 560
internal/runtime/executor/claude_executor.go+106−45
@@ -54,15 +54,22 @@ func (e *ClaudeExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, r · 53 unmodified lines
54 // Use streaming translation to preserve function calling, except for claude.54 // Use streaming translation to preserve function calling, except for claude.
55 stream := from != to55 stream := from != to
56 body := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), stream)56 body := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), stream)
57 modelForUpstream := req.Model57 upstreamModel := util.ResolveOriginalModel(req.Model, req.Metadata)
58 if modelOverride := e.resolveUpstreamModel(req.Model, auth); modelOverride != "" {58 if upstreamModel == "" {
59 body, _ = sjson.SetBytes(body, "model", modelOverride)59 upstreamModel = req.Model
60 modelForUpstream = modelOverride60 }
61 if modelOverride := e.resolveUpstreamModel(upstreamModel, auth); modelOverride != "" {
62 upstreamModel = modelOverride
63 } else if !strings.EqualFold(upstreamModel, req.Model) {
64 if modelOverride := e.resolveUpstreamModel(req.Model, auth); modelOverride != "" {
65 upstreamModel = modelOverride
66 }
61 }67 }
62 // Inject thinking config based on model suffix for thinking variants68 body, _ = sjson.SetBytes(body, "model", upstreamModel)
63 body = e.injectThinkingConfig(req.Model, body)69 // Inject thinking config based on model metadata for thinking variants
70 body = e.injectThinkingConfig(req.Model, req.Metadata, body)
64 71
65 if !strings.HasPrefix(modelForUpstream, "claude-3-5-haiku") {72 if !strings.HasPrefix(upstreamModel, "claude-3-5-haiku") {
66 body = checkSystemInstructions(body)73 body = checkSystemInstructions(body)
67 }74 }
68 body = applyPayloadConfig(e.cfg, req.Model, body)75 body = applyPayloadConfig(e.cfg, req.Model, body)
@@ -161,11 +168,20 @@ func (e *ClaudeExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.A · 92 unmodified lines
161 from := opts.SourceFormat168 from := opts.SourceFormat
162 to := sdktranslator.FromString("claude")169 to := sdktranslator.FromString("claude")
163 body := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), true)170 body := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), true)
164 if modelOverride := e.resolveUpstreamModel(req.Model, auth); modelOverride != "" {171 upstreamModel := util.ResolveOriginalModel(req.Model, req.Metadata)
165 body, _ = sjson.SetBytes(body, "model", modelOverride)172 if upstreamModel == "" {
173 upstreamModel = req.Model
174 }
175 if modelOverride := e.resolveUpstreamModel(upstreamModel, auth); modelOverride != "" {
176 upstreamModel = modelOverride
177 } else if !strings.EqualFold(upstreamModel, req.Model) {
178 if modelOverride := e.resolveUpstreamModel(req.Model, auth); modelOverride != "" {
179 upstreamModel = modelOverride
180 }
166 }181 }
167 // Inject thinking config based on model suffix for thinking variants182 body, _ = sjson.SetBytes(body, "model", upstreamModel)
168 body = e.injectThinkingConfig(req.Model, body)183 // Inject thinking config based on model metadata for thinking variants
184 body = e.injectThinkingConfig(req.Model, req.Metadata, body)
169 body = checkSystemInstructions(body)185 body = checkSystemInstructions(body)
170 body = applyPayloadConfig(e.cfg, req.Model, body)186 body = applyPayloadConfig(e.cfg, req.Model, body)
171 187
@@ -295,13 +311,20 @@ func (e *ClaudeExecutor) CountTokens(ctx context.Context, auth *cliproxyauth.Aut · 123 unmodified lines
295 // Use streaming translation to preserve function calling, except for claude.311 // Use streaming translation to preserve function calling, except for claude.
296 stream := from != to312 stream := from != to
297 body := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), stream)313 body := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), stream)
298 modelForUpstream := req.Model314 upstreamModel := util.ResolveOriginalModel(req.Model, req.Metadata)
299 if modelOverride := e.resolveUpstreamModel(req.Model, auth); modelOverride != "" {315 if upstreamModel == "" {
300 body, _ = sjson.SetBytes(body, "model", modelOverride)316 upstreamModel = req.Model
301 modelForUpstream = modelOverride317 }
318 if modelOverride := e.resolveUpstreamModel(upstreamModel, auth); modelOverride != "" {
319 upstreamModel = modelOverride
320 } else if !strings.EqualFold(upstreamModel, req.Model) {
321 if modelOverride := e.resolveUpstreamModel(req.Model, auth); modelOverride != "" {
322 upstreamModel = modelOverride
323 }
302 }324 }
325 body, _ = sjson.SetBytes(body, "model", upstreamModel)
303 326
304 if !strings.HasPrefix(modelForUpstream, "claude-3-5-haiku") {327 if !strings.HasPrefix(upstreamModel, "claude-3-5-haiku") {
305 body = checkSystemInstructions(body)328 body = checkSystemInstructions(body)
306 }329 }
307 330
@@ -427,25 +450,15 @@ func extractAndRemoveBetas(body []byte) ([]string, []byte) { · 119 unmodified lines
427 return betas, body450 return betas, body
428}451}
429 452
430// injectThinkingConfig adds thinking configuration based on model name suffix453// injectThinkingConfig adds thinking configuration based on metadata or legacy suffixes.
431func (e *ClaudeExecutor) injectThinkingConfig(modelName string, body []byte) []byte {454func (e *ClaudeExecutor) injectThinkingConfig(modelName string, metadata map[string]any, body []byte) []byte {
432 // Only inject if thinking config is not already present455 // Only inject if thinking config is not already present
433 if gjson.GetBytes(body, "thinking").Exists() {456 if gjson.GetBytes(body, "thinking").Exists() {
434 return body457 return body
435 }458 }
436 459
437 var budgetTokens int460 budgetTokens, ok := resolveClaudeThinkingBudget(modelName, metadata)
438 switch {461 if !ok || budgetTokens <= 0 {
439 case strings.HasSuffix(modelName, "-thinking-low"):
440 budgetTokens = 1024
441 case strings.HasSuffix(modelName, "-thinking-medium"):
442 budgetTokens = 8192
443 case strings.HasSuffix(modelName, "-thinking-high"):
444 budgetTokens = 24576
445 case strings.HasSuffix(modelName, "-thinking"):
446 // Default thinking without suffix uses medium budget
447 budgetTokens = 8192
448 default:
449 return body462 return body
450 }463 }
451 464
@@ -454,6 +467,44 @@ func (e *ClaudeExecutor) injectThinkingConfig(modelName string, body []byte) []b · 2 unmodified lines
454 return body467 return body
455}468}
456 469
470func resolveClaudeThinkingBudget(modelName string, metadata map[string]any) (int, bool) {
471 budget, include, effort, matched := util.ThinkingFromMetadata(metadata)
472 if matched {
473 if include != nil && !*include {
474 return 0, false
475 }
476 if budget != nil {
477 normalized := util.NormalizeThinkingBudget(modelName, *budget)
478 if normalized > 0 {
479 return normalized, true
480 }
481 return 0, false
482 }
483 if effort != nil {
484 if derived, ok := util.ThinkingEffortToBudget(modelName, *effort); ok && derived > 0 {
485 return derived, true
486 }
487 }
488 }
489 return claudeBudgetFromSuffix(modelName)
490}
491
492func claudeBudgetFromSuffix(modelName string) (int, bool) {
493 lower := strings.ToLower(strings.TrimSpace(modelName))
494 switch {
495 case strings.HasSuffix(lower, "-thinking-low"):
496 return 1024, true
497 case strings.HasSuffix(lower, "-thinking-medium"):
498 return 8192, true
499 case strings.HasSuffix(lower, "-thinking-high"):
500 return 24576, true
501 case strings.HasSuffix(lower, "-thinking"):
502 return 8192, true
503 default:
504 return 0, false
505 }
506}
507
457// ensureMaxTokensForThinking ensures max_tokens > thinking.budget_tokens when thinking is enabled.508// ensureMaxTokensForThinking ensures max_tokens > thinking.budget_tokens when thinking is enabled.
458// Anthropic API requires this constraint; violating it returns a 400 error.509// Anthropic API requires this constraint; violating it returns a 400 error.
459// This function should be called after all thinking configuration is finalized.510// This function should be called after all thinking configuration is finalized.
@@ -491,35 +542,45 @@ func ensureMaxTokensForThinking(modelName string, body []byte) []byte { · 31 unmodified lines
491}542}
492 543
493func (e *ClaudeExecutor) resolveUpstreamModel(alias string, auth *cliproxyauth.Auth) string {544func (e *ClaudeExecutor) resolveUpstreamModel(alias string, auth *cliproxyauth.Auth) string {
494 if alias == "" {545 trimmed := strings.TrimSpace(alias)
546 if trimmed == "" {
495 return ""547 return ""
496 }548 }
497 // Hardcoded mappings for thinking models to actual Claude model names549
498 switch alias {
499 case "claude-opus-4-5-thinking", "claude-opus-4-5-thinking-low", "claude-opus-4-5-thinking-medium", "claude-opus-4-5-thinking-high":
500 return "claude-opus-4-5-20251101"
501 case "claude-sonnet-4-5-thinking":
502 return "claude-sonnet-4-5-20250929"
503 }
504 entry := e.resolveClaudeConfig(auth)550 entry := e.resolveClaudeConfig(auth)
505 if entry == nil {551 if entry == nil {
506 return ""552 return ""
507 }553 }
554
555 normalizedModel, metadata := util.NormalizeThinkingModel(trimmed)
556
557 // Candidate names to match against configured aliases/names.
558 candidates := []string{strings.TrimSpace(normalizedModel)}
559 if !strings.EqualFold(normalizedModel, trimmed) {
560 candidates = append(candidates, trimmed)
561 }
562 if original := util.ResolveOriginalModel(normalizedModel, metadata); original != "" && !strings.EqualFold(original, normalizedModel) {
563 candidates = append(candidates, original)
564 }
565
508 for i := range entry.Models {566 for i := range entry.Models {
509 model := entry.Models[i]567 model := entry.Models[i]
510 name := strings.TrimSpace(model.Name)568 name := strings.TrimSpace(model.Name)
511 modelAlias := strings.TrimSpace(model.Alias)569 modelAlias := strings.TrimSpace(model.Alias)
512 if modelAlias != "" {570
513 if strings.EqualFold(modelAlias, alias) {571 for _, candidate := range candidates {
572 if candidate == "" {
573 continue
574 }
575 if modelAlias != "" && strings.EqualFold(modelAlias, candidate) {
514 if name != "" {576 if name != "" {
515 return name577 return name
516 }578 }
517 return alias579 return candidate
580 }
581 if name != "" && strings.EqualFold(name, candidate) {
582 return name
518 }583 }
519 continue
520 }
521 if name != "" && strings.EqualFold(name, alias) {
522 return name
523 }584 }
524 }585 }
525 return ""586 return ""
internal/runtime/executor/codex_executor.go+12−84
@@ -49,14 +49,14 @@ func (e *CodexExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, re · 48 unmodified lines
49 reporter := newUsageReporter(ctx, e.Identifier(), req.Model, auth)49 reporter := newUsageReporter(ctx, e.Identifier(), req.Model, auth)
50 defer reporter.trackFailure(ctx, &err)50 defer reporter.trackFailure(ctx, &err)
51 51
52 upstreamModel := util.ResolveOriginalModel(req.Model, req.Metadata)
53
52 from := opts.SourceFormat54 from := opts.SourceFormat
53 to := sdktranslator.FromString("codex")55 to := sdktranslator.FromString("codex")
54 body := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), false)56 body := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), false)
55 57 body = applyReasoningEffortMetadata(body, req.Metadata, req.Model)
56 body = e.setReasoningEffortByAlias(req.Model, body)
57
58 body = applyPayloadConfig(e.cfg, req.Model, body)58 body = applyPayloadConfig(e.cfg, req.Model, body)
59 59 body, _ = sjson.SetBytes(body, "model", upstreamModel)
60 body, _ = sjson.SetBytes(body, "stream", true)60 body, _ = sjson.SetBytes(body, "stream", true)
61 body, _ = sjson.DeleteBytes(body, "previous_response_id")61 body, _ = sjson.DeleteBytes(body, "previous_response_id")
62 62
@@ -142,13 +142,16 @@ func (e *CodexExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Au · 79 unmodified lines
142 reporter := newUsageReporter(ctx, e.Identifier(), req.Model, auth)142 reporter := newUsageReporter(ctx, e.Identifier(), req.Model, auth)
143 defer reporter.trackFailure(ctx, &err)143 defer reporter.trackFailure(ctx, &err)
144 144
145 upstreamModel := util.ResolveOriginalModel(req.Model, req.Metadata)
146
145 from := opts.SourceFormat147 from := opts.SourceFormat
146 to := sdktranslator.FromString("codex")148 to := sdktranslator.FromString("codex")
147 body := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), true)149 body := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), true)
148 150
149 body = e.setReasoningEffortByAlias(req.Model, body)151 body = applyReasoningEffortMetadata(body, req.Metadata, req.Model)
150 body = applyPayloadConfig(e.cfg, req.Model, body)152 body = applyPayloadConfig(e.cfg, req.Model, body)
151 body, _ = sjson.DeleteBytes(body, "previous_response_id")153 body, _ = sjson.DeleteBytes(body, "previous_response_id")
154 body, _ = sjson.SetBytes(body, "model", upstreamModel)
152 155
153 url := strings.TrimSuffix(baseURL, "/") + "/responses"156 url := strings.TrimSuffix(baseURL, "/") + "/responses"
154 httpReq, err := e.cacheHelper(ctx, from, url, req, body)157 httpReq, err := e.cacheHelper(ctx, from, url, req, body)
@@ -235,14 +238,16 @@ func (e *CodexExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Au · 80 unmodified lines
235}238}
236 239
237func (e *CodexExecutor) CountTokens(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options) (cliproxyexecutor.Response, error) {240func (e *CodexExecutor) CountTokens(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options) (cliproxyexecutor.Response, error) {
241 upstreamModel := util.ResolveOriginalModel(req.Model, req.Metadata)
242
238 from := opts.SourceFormat243 from := opts.SourceFormat
239 to := sdktranslator.FromString("codex")244 to := sdktranslator.FromString("codex")
240 body := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), false)245 body := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), false)
241 246
242 modelForCounting := req.Model247 modelForCounting := req.Model
243 248
244 body = e.setReasoningEffortByAlias(req.Model, body)249 body = applyReasoningEffortMetadata(body, req.Metadata, req.Model)
245 250 body, _ = sjson.SetBytes(body, "model", upstreamModel)
246 body, _ = sjson.DeleteBytes(body, "previous_response_id")251 body, _ = sjson.DeleteBytes(body, "previous_response_id")
247 body, _ = sjson.SetBytes(body, "stream", false)252 body, _ = sjson.SetBytes(body, "stream", false)
248 253
@@ -261,83 +266,6 @@ func (e *CodexExecutor) CountTokens(ctx context.Context, auth *cliproxyauth.Auth · 12 unmodified lines
261 return cliproxyexecutor.Response{Payload: []byte(translated)}, nil266 return cliproxyexecutor.Response{Payload: []byte(translated)}, nil
262}267}
263 268
264func (e *CodexExecutor) setReasoningEffortByAlias(modelName string, payload []byte) []byte {
265 if util.InArray([]string{"gpt-5", "gpt-5-minimal", "gpt-5-low", "gpt-5-medium", "gpt-5-high"}, modelName) {
266 payload, _ = sjson.SetBytes(payload, "model", "gpt-5")
267 switch modelName {
268 case "gpt-5-minimal":
269 payload, _ = sjson.SetBytes(payload, "reasoning.effort", "minimal")
270 case "gpt-5-low":
271 payload, _ = sjson.SetBytes(payload, "reasoning.effort", "low")
272 case "gpt-5-medium":
273 payload, _ = sjson.SetBytes(payload, "reasoning.effort", "medium")
274 case "gpt-5-high":
275 payload, _ = sjson.SetBytes(payload, "reasoning.effort", "high")
276 }
277 } else if util.InArray([]string{"gpt-5-codex", "gpt-5-codex-low", "gpt-5-codex-medium", "gpt-5-codex-high"}, modelName) {
278 payload, _ = sjson.SetBytes(payload, "model", "gpt-5-codex")
279 switch modelName {
280 case "gpt-5-codex-low":
281 payload, _ = sjson.SetBytes(payload, "reasoning.effort", "low")
282 case "gpt-5-codex-medium":
283 payload, _ = sjson.SetBytes(payload, "reasoning.effort", "medium")
284 case "gpt-5-codex-high":
285 payload, _ = sjson.SetBytes(payload, "reasoning.effort", "high")
286 }
287 } else if util.InArray([]string{"gpt-5-codex-mini", "gpt-5-codex-mini-medium", "gpt-5-codex-mini-high"}, modelName) {
288 payload, _ = sjson.SetBytes(payload, "model", "gpt-5-codex-mini")
289 switch modelName {
290 case "gpt-5-codex-mini-medium":
291 payload, _ = sjson.SetBytes(payload, "reasoning.effort", "medium")
292 case "gpt-5-codex-mini-high":
293 payload, _ = sjson.SetBytes(payload, "reasoning.effort", "high")
294 }
295 } else if util.InArray([]string{"gpt-5.1", "gpt-5.1-none", "gpt-5.1-low", "gpt-5.1-medium", "gpt-5.1-high"}, modelName) {
296 payload, _ = sjson.SetBytes(payload, "model", "gpt-5.1")
297 switch modelName {
298 case "gpt-5.1-none":
299 payload, _ = sjson.SetBytes(payload, "reasoning.effort", "none")
300 case "gpt-5.1-low":
301 payload, _ = sjson.SetBytes(payload, "reasoning.effort", "low")
302 case "gpt-5.1-medium":
303 payload, _ = sjson.SetBytes(payload, "reasoning.effort", "medium")
304 case "gpt-5.1-high":
305 payload, _ = sjson.SetBytes(payload, "reasoning.effort", "high")
306 }
307 } else if util.InArray([]string{"gpt-5.1-codex", "gpt-5.1-codex-low", "gpt-5.1-codex-medium", "gpt-5.1-codex-high"}, modelName) {
308 payload, _ = sjson.SetBytes(payload, "model", "gpt-5.1-codex")
309 switch modelName {
310 case "gpt-5.1-codex-low":
311 payload, _ = sjson.SetBytes(payload, "reasoning.effort", "low")
312 case "gpt-5.1-codex-medium":
313 payload, _ = sjson.SetBytes(payload, "reasoning.effort", "medium")
314 case "gpt-5.1-codex-high":
315 payload, _ = sjson.SetBytes(payload, "reasoning.effort", "high")
316 }
317 } else if util.InArray([]string{"gpt-5.1-codex-mini", "gpt-5.1-codex-mini-medium", "gpt-5.1-codex-mini-high"}, modelName) {
318 payload, _ = sjson.SetBytes(payload, "model", "gpt-5.1-codex-mini")
319 switch modelName {
320 case "gpt-5.1-codex-mini-medium":
321 payload, _ = sjson.SetBytes(payload, "reasoning.effort", "medium")
322 case "gpt-5.1-codex-mini-high":
323 payload, _ = sjson.SetBytes(payload, "reasoning.effort", "high")
324 }
325 } else if util.InArray([]string{"gpt-5.1-codex-max", "gpt-5.1-codex-max-low", "gpt-5.1-codex-max-medium", "gpt-5.1-codex-max-high", "gpt-5.1-codex-max-xhigh"}, modelName) {
326 payload, _ = sjson.SetBytes(payload, "model", "gpt-5.1-codex-max")
327 switch modelName {
328 case "gpt-5.1-codex-max-low":
329 payload, _ = sjson.SetBytes(payload, "reasoning.effort", "low")
330 case "gpt-5.1-codex-max-medium":
331 payload, _ = sjson.SetBytes(payload, "reasoning.effort", "medium")
332 case "gpt-5.1-codex-max-high":
333 payload, _ = sjson.SetBytes(payload, "reasoning.effort", "high")
334 case "gpt-5.1-codex-max-xhigh":
335 payload, _ = sjson.SetBytes(payload, "reasoning.effort", "xhigh")
336 }
337 }
338 return payload
339}
340
341func tokenizerForCodexModel(model string) (tokenizer.Codec, error) {269func tokenizerForCodexModel(model string) (tokenizer.Codec, error) {
342 sanitized := strings.ToLower(strings.TrimSpace(model))270 sanitized := strings.ToLower(strings.TrimSpace(model))
343 switch {271 switch {
internal/runtime/executor/gemini_executor.go+8−2
@@ -75,6 +75,8 @@ func (e *GeminiExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, r · 74 unmodified lines
75 reporter := newUsageReporter(ctx, e.Identifier(), req.Model, auth)75 reporter := newUsageReporter(ctx, e.Identifier(), req.Model, auth)
76 defer reporter.trackFailure(ctx, &err)76 defer reporter.trackFailure(ctx, &err)
77 77
78 upstreamModel := util.ResolveOriginalModel(req.Model, req.Metadata)
79
78 // Official Gemini API via API key or OAuth bearer80 // Official Gemini API via API key or OAuth bearer
79 from := opts.SourceFormat81 from := opts.SourceFormat
80 to := sdktranslator.FromString("gemini")82 to := sdktranslator.FromString("gemini")
@@ -85,6 +87,7 @@ func (e *GeminiExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, r · 4 unmodified lines
85 body = util.StripThinkingConfigIfUnsupported(req.Model, body)87 body = util.StripThinkingConfigIfUnsupported(req.Model, body)
86 body = fixGeminiImageAspectRatio(req.Model, body)88 body = fixGeminiImageAspectRatio(req.Model, body)
87 body = applyPayloadConfig(e.cfg, req.Model, body)89 body = applyPayloadConfig(e.cfg, req.Model, body)
90 body, _ = sjson.SetBytes(body, "model", upstreamModel)
88 91
89 action := "generateContent"92 action := "generateContent"
90 if req.Metadata != nil {93 if req.Metadata != nil {
@@ -93,7 +96,7 @@ func (e *GeminiExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, r · 2 unmodified lines
93 }96 }
94 }97 }
95 baseURL := resolveGeminiBaseURL(auth)98 baseURL := resolveGeminiBaseURL(auth)
96 url := fmt.Sprintf("%s/%s/models/%s:%s", baseURL, glAPIVersion, req.Model, action)99 url := fmt.Sprintf("%s/%s/models/%s:%s", baseURL, glAPIVersion, upstreamModel, action)
97 if opts.Alt != "" && action != "countTokens" {100 if opts.Alt != "" && action != "countTokens" {
98 url = url + fmt.Sprintf("?$alt=%s", opts.Alt)101 url = url + fmt.Sprintf("?$alt=%s", opts.Alt)
99 }102 }
@@ -167,6 +170,8 @@ func (e *GeminiExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.A · 67 unmodified lines
167 reporter := newUsageReporter(ctx, e.Identifier(), req.Model, auth)170 reporter := newUsageReporter(ctx, e.Identifier(), req.Model, auth)
168 defer reporter.trackFailure(ctx, &err)171 defer reporter.trackFailure(ctx, &err)
169 172
173 upstreamModel := util.ResolveOriginalModel(req.Model, req.Metadata)
174
170 from := opts.SourceFormat175 from := opts.SourceFormat
171 to := sdktranslator.FromString("gemini")176 to := sdktranslator.FromString("gemini")
172 body := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), true)177 body := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), true)
@@ -176,9 +181,10 @@ func (e *GeminiExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.A · 3 unmodified lines
176 body = util.StripThinkingConfigIfUnsupported(req.Model, body)181 body = util.StripThinkingConfigIfUnsupported(req.Model, body)
177 body = fixGeminiImageAspectRatio(req.Model, body)182 body = fixGeminiImageAspectRatio(req.Model, body)
178 body = applyPayloadConfig(e.cfg, req.Model, body)183 body = applyPayloadConfig(e.cfg, req.Model, body)
184 body, _ = sjson.SetBytes(body, "model", upstreamModel)
179 185
180 baseURL := resolveGeminiBaseURL(auth)186 baseURL := resolveGeminiBaseURL(auth)
181 url := fmt.Sprintf("%s/%s/models/%s:%s", baseURL, glAPIVersion, req.Model, "streamGenerateContent")187 url := fmt.Sprintf("%s/%s/models/%s:%s", baseURL, glAPIVersion, upstreamModel, "streamGenerateContent")
182 if opts.Alt == "" {188 if opts.Alt == "" {
183 url = url + "?alt=sse"189 url = url + "?alt=sse"
184 } else {190 } else {
internal/runtime/executor/gemini_vertex_executor.go+29−11
@@ -105,10 +105,12 @@ func (e *GeminiVertexExecutor) CountTokens(ctx context.Context, auth *cliproxyau · 104 unmodified lines
105 105
106// countTokensWithServiceAccount handles token counting using service account credentials.106// countTokensWithServiceAccount handles token counting using service account credentials.
107func (e *GeminiVertexExecutor) countTokensWithServiceAccount(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options, projectID, location string, saJSON []byte) (cliproxyexecutor.Response, error) {107func (e *GeminiVertexExecutor) countTokensWithServiceAccount(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options, projectID, location string, saJSON []byte) (cliproxyexecutor.Response, error) {
108 upstreamModel := util.ResolveOriginalModel(req.Model, req.Metadata)
109
108 from := opts.SourceFormat110 from := opts.SourceFormat
109 to := sdktranslator.FromString("gemini")111 to := sdktranslator.FromString("gemini")
110 translatedReq := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), false)112 translatedReq := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), false)
111 if budgetOverride, includeOverride, ok := util.GeminiThinkingFromMetadata(req.Metadata); ok && util.ModelSupportsThinking(req.Model) {113 if budgetOverride, includeOverride, ok := util.ResolveThinkingConfigFromMetadata(req.Model, req.Metadata); ok && util.ModelSupportsThinking(req.Model) {
112 if budgetOverride != nil {114 if budgetOverride != nil {
113 norm := util.NormalizeThinkingBudget(req.Model, *budgetOverride)115 norm := util.NormalizeThinkingBudget(req.Model, *budgetOverride)
114 budgetOverride = &norm116 budgetOverride = &norm
@@ -117,13 +119,14 @@ func (e *GeminiVertexExecutor) countTokensWithServiceAccount(ctx context.Context · 2 unmodified lines
117 }119 }
118 translatedReq = util.StripThinkingConfigIfUnsupported(req.Model, translatedReq)120 translatedReq = util.StripThinkingConfigIfUnsupported(req.Model, translatedReq)
119 translatedReq = fixGeminiImageAspectRatio(req.Model, translatedReq)121 translatedReq = fixGeminiImageAspectRatio(req.Model, translatedReq)
122 translatedReq, _ = sjson.SetBytes(translatedReq, "model", upstreamModel)
120 respCtx := context.WithValue(ctx, "alt", opts.Alt)123 respCtx := context.WithValue(ctx, "alt", opts.Alt)
121 translatedReq, _ = sjson.DeleteBytes(translatedReq, "tools")124 translatedReq, _ = sjson.DeleteBytes(translatedReq, "tools")
122 translatedReq, _ = sjson.DeleteBytes(translatedReq, "generationConfig")125 translatedReq, _ = sjson.DeleteBytes(translatedReq, "generationConfig")
123 translatedReq, _ = sjson.DeleteBytes(translatedReq, "safetySettings")126 translatedReq, _ = sjson.DeleteBytes(translatedReq, "safetySettings")
124 127
125 baseURL := vertexBaseURL(location)128 baseURL := vertexBaseURL(location)
126 url := fmt.Sprintf("%s/%s/projects/%s/locations/%s/publishers/google/models/%s:%s", baseURL, vertexAPIVersion, projectID, location, req.Model, "countTokens")129 url := fmt.Sprintf("%s/%s/projects/%s/locations/%s/publishers/google/models/%s:%s", baseURL, vertexAPIVersion, projectID, location, upstreamModel, "countTokens")
127 130
128 httpReq, errNewReq := http.NewRequestWithContext(respCtx, http.MethodPost, url, bytes.NewReader(translatedReq))131 httpReq, errNewReq := http.NewRequestWithContext(respCtx, http.MethodPost, url, bytes.NewReader(translatedReq))
129 if errNewReq != nil {132 if errNewReq != nil {
@@ -191,10 +194,12 @@ func (e *GeminiVertexExecutor) countTokensWithServiceAccount(ctx context.Context · 61 unmodified lines
191 194
192// countTokensWithAPIKey handles token counting using API key credentials.195// countTokensWithAPIKey handles token counting using API key credentials.
193func (e *GeminiVertexExecutor) countTokensWithAPIKey(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options, apiKey, baseURL string) (cliproxyexecutor.Response, error) {196func (e *GeminiVertexExecutor) countTokensWithAPIKey(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options, apiKey, baseURL string) (cliproxyexecutor.Response, error) {
197 upstreamModel := util.ResolveOriginalModel(req.Model, req.Metadata)
198
194 from := opts.SourceFormat199 from := opts.SourceFormat
195 to := sdktranslator.FromString("gemini")200 to := sdktranslator.FromString("gemini")
196 translatedReq := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), false)201 translatedReq := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), false)
197 if budgetOverride, includeOverride, ok := util.GeminiThinkingFromMetadata(req.Metadata); ok && util.ModelSupportsThinking(req.Model) {202 if budgetOverride, includeOverride, ok := util.ResolveThinkingConfigFromMetadata(req.Model, req.Metadata); ok && util.ModelSupportsThinking(req.Model) {
198 if budgetOverride != nil {203 if budgetOverride != nil {
199 norm := util.NormalizeThinkingBudget(req.Model, *budgetOverride)204 norm := util.NormalizeThinkingBudget(req.Model, *budgetOverride)
200 budgetOverride = &norm205 budgetOverride = &norm
@@ -203,6 +208,7 @@ func (e *GeminiVertexExecutor) countTokensWithAPIKey(ctx context.Context, auth * · 2 unmodified lines
203 }208 }
204 translatedReq = util.StripThinkingConfigIfUnsupported(req.Model, translatedReq)209 translatedReq = util.StripThinkingConfigIfUnsupported(req.Model, translatedReq)
205 translatedReq = fixGeminiImageAspectRatio(req.Model, translatedReq)210 translatedReq = fixGeminiImageAspectRatio(req.Model, translatedReq)
211 translatedReq, _ = sjson.SetBytes(translatedReq, "model", upstreamModel)
206 respCtx := context.WithValue(ctx, "alt", opts.Alt)212 respCtx := context.WithValue(ctx, "alt", opts.Alt)
207 translatedReq, _ = sjson.DeleteBytes(translatedReq, "tools")213 translatedReq, _ = sjson.DeleteBytes(translatedReq, "tools")
208 translatedReq, _ = sjson.DeleteBytes(translatedReq, "generationConfig")214 translatedReq, _ = sjson.DeleteBytes(translatedReq, "generationConfig")
@@ -286,10 +292,12 @@ func (e *GeminiVertexExecutor) executeWithServiceAccount(ctx context.Context, au · 77 unmodified lines
286 reporter := newUsageReporter(ctx, e.Identifier(), req.Model, auth)292 reporter := newUsageReporter(ctx, e.Identifier(), req.Model, auth)
287 defer reporter.trackFailure(ctx, &err)293 defer reporter.trackFailure(ctx, &err)
288 294
295 upstreamModel := util.ResolveOriginalModel(req.Model, req.Metadata)
296
289 from := opts.SourceFormat297 from := opts.SourceFormat
290 to := sdktranslator.FromString("gemini")298 to := sdktranslator.FromString("gemini")
291 body := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), false)299 body := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), false)
292 if budgetOverride, includeOverride, ok := util.GeminiThinkingFromMetadata(req.Metadata); ok && util.ModelSupportsThinking(req.Model) {300 if budgetOverride, includeOverride, ok := util.ResolveThinkingConfigFromMetadata(req.Model, req.Metadata); ok && util.ModelSupportsThinking(req.Model) {
293 if budgetOverride != nil {301 if budgetOverride != nil {
294 norm := util.NormalizeThinkingBudget(req.Model, *budgetOverride)302 norm := util.NormalizeThinkingBudget(req.Model, *budgetOverride)
295 budgetOverride = &norm303 budgetOverride = &norm
@@ -301,6 +309,7 @@ func (e *GeminiVertexExecutor) executeWithServiceAccount(ctx context.Context, au · 5 unmodified lines
301 body = util.StripThinkingConfigIfUnsupported(req.Model, body)309 body = util.StripThinkingConfigIfUnsupported(req.Model, body)
302 body = fixGeminiImageAspectRatio(req.Model, body)310 body = fixGeminiImageAspectRatio(req.Model, body)
303 body = applyPayloadConfig(e.cfg, req.Model, body)311 body = applyPayloadConfig(e.cfg, req.Model, body)
312 body, _ = sjson.SetBytes(body, "model", upstreamModel)
304 313
305 action := "generateContent"314 action := "generateContent"
306 if req.Metadata != nil {315 if req.Metadata != nil {
@@ -309,7 +318,7 @@ func (e *GeminiVertexExecutor) executeWithServiceAccount(ctx context.Context, au · 2 unmodified lines
309 }318 }
310 }319 }
311 baseURL := vertexBaseURL(location)320 baseURL := vertexBaseURL(location)
312 url := fmt.Sprintf("%s/%s/projects/%s/locations/%s/publishers/google/models/%s:%s", baseURL, vertexAPIVersion, projectID, location, req.Model, action)321 url := fmt.Sprintf("%s/%s/projects/%s/locations/%s/publishers/google/models/%s:%s", baseURL, vertexAPIVersion, projectID, location, upstreamModel, action)
313 if opts.Alt != "" && action != "countTokens" {322 if opts.Alt != "" && action != "countTokens" {
314 url = url + fmt.Sprintf("?$alt=%s", opts.Alt)323 url = url + fmt.Sprintf("?$alt=%s", opts.Alt)
315 }324 }
@@ -383,10 +392,12 @@ func (e *GeminiVertexExecutor) executeWithAPIKey(ctx context.Context, auth *clip · 67 unmodified lines
383 reporter := newUsageReporter(ctx, e.Identifier(), req.Model, auth)392 reporter := newUsageReporter(ctx, e.Identifier(), req.Model, auth)
384 defer reporter.trackFailure(ctx, &err)393 defer reporter.trackFailure(ctx, &err)
385 394
395 upstreamModel := util.ResolveOriginalModel(req.Model, req.Metadata)
396
386 from := opts.SourceFormat397 from := opts.SourceFormat
387 to := sdktranslator.FromString("gemini")398 to := sdktranslator.FromString("gemini")
388 body := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), false)399 body := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), false)
389 if budgetOverride, includeOverride, ok := util.GeminiThinkingFromMetadata(req.Metadata); ok && util.ModelSupportsThinking(req.Model) {400 if budgetOverride, includeOverride, ok := util.ResolveThinkingConfigFromMetadata(req.Model, req.Metadata); ok && util.ModelSupportsThinking(req.Model) {
390 if budgetOverride != nil {401 if budgetOverride != nil {
391 norm := util.NormalizeThinkingBudget(req.Model, *budgetOverride)402 norm := util.NormalizeThinkingBudget(req.Model, *budgetOverride)
392 budgetOverride = &norm403 budgetOverride = &norm
@@ -398,6 +409,7 @@ func (e *GeminiVertexExecutor) executeWithAPIKey(ctx context.Context, auth *clip · 5 unmodified lines
398 body = util.StripThinkingConfigIfUnsupported(req.Model, body)409 body = util.StripThinkingConfigIfUnsupported(req.Model, body)
399 body = fixGeminiImageAspectRatio(req.Model, body)410 body = fixGeminiImageAspectRatio(req.Model, body)
400 body = applyPayloadConfig(e.cfg, req.Model, body)411 body = applyPayloadConfig(e.cfg, req.Model, body)
412 body, _ = sjson.SetBytes(body, "model", upstreamModel)
401 413
402 action := "generateContent"414 action := "generateContent"
403 if req.Metadata != nil {415 if req.Metadata != nil {
@@ -410,7 +422,7 @@ func (e *GeminiVertexExecutor) executeWithAPIKey(ctx context.Context, auth *clip · 6 unmodified lines
410 if baseURL == "" {422 if baseURL == "" {
411 baseURL = "https://generativelanguage.googleapis.com"423 baseURL = "https://generativelanguage.googleapis.com"
412 }424 }
413 url := fmt.Sprintf("%s/%s/publishers/google/models/%s:%s", baseURL, vertexAPIVersion, req.Model, action)425 url := fmt.Sprintf("%s/%s/publishers/google/models/%s:%s", baseURL, vertexAPIVersion, upstreamModel, action)
414 if opts.Alt != "" && action != "countTokens" {426 if opts.Alt != "" && action != "countTokens" {
415 url = url + fmt.Sprintf("?$alt=%s", opts.Alt)427 url = url + fmt.Sprintf("?$alt=%s", opts.Alt)
416 }428 }
@@ -481,10 +493,12 @@ func (e *GeminiVertexExecutor) executeStreamWithServiceAccount(ctx context.Conte · 64 unmodified lines
481 reporter := newUsageReporter(ctx, e.Identifier(), req.Model, auth)493 reporter := newUsageReporter(ctx, e.Identifier(), req.Model, auth)
482 defer reporter.trackFailure(ctx, &err)494 defer reporter.trackFailure(ctx, &err)
483 495
496 upstreamModel := util.ResolveOriginalModel(req.Model, req.Metadata)
497
484 from := opts.SourceFormat498 from := opts.SourceFormat
485 to := sdktranslator.FromString("gemini")499 to := sdktranslator.FromString("gemini")
486 body := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), true)500 body := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), true)
487 if budgetOverride, includeOverride, ok := util.GeminiThinkingFromMetadata(req.Metadata); ok && util.ModelSupportsThinking(req.Model) {501 if budgetOverride, includeOverride, ok := util.ResolveThinkingConfigFromMetadata(req.Model, req.Metadata); ok && util.ModelSupportsThinking(req.Model) {
488 if budgetOverride != nil {502 if budgetOverride != nil {
489 norm := util.NormalizeThinkingBudget(req.Model, *budgetOverride)503 norm := util.NormalizeThinkingBudget(req.Model, *budgetOverride)
490 budgetOverride = &norm504 budgetOverride = &norm
@@ -496,9 +510,10 @@ func (e *GeminiVertexExecutor) executeStreamWithServiceAccount(ctx context.Conte · 5 unmodified lines
496 body = util.StripThinkingConfigIfUnsupported(req.Model, body)510 body = util.StripThinkingConfigIfUnsupported(req.Model, body)
497 body = fixGeminiImageAspectRatio(req.Model, body)511 body = fixGeminiImageAspectRatio(req.Model, body)
498 body = applyPayloadConfig(e.cfg, req.Model, body)512 body = applyPayloadConfig(e.cfg, req.Model, body)
513 body, _ = sjson.SetBytes(body, "model", upstreamModel)
499 514
500 baseURL := vertexBaseURL(location)515 baseURL := vertexBaseURL(location)
501 url := fmt.Sprintf("%s/%s/projects/%s/locations/%s/publishers/google/models/%s:%s", baseURL, vertexAPIVersion, projectID, location, req.Model, "streamGenerateContent")516 url := fmt.Sprintf("%s/%s/projects/%s/locations/%s/publishers/google/models/%s:%s", baseURL, vertexAPIVersion, projectID, location, upstreamModel, "streamGenerateContent")
502 if opts.Alt == "" {517 if opts.Alt == "" {
503 url = url + "?alt=sse"518 url = url + "?alt=sse"
504 } else {519 } else {
@@ -595,10 +610,12 @@ func (e *GeminiVertexExecutor) executeStreamWithAPIKey(ctx context.Context, auth · 90 unmodified lines
595 reporter := newUsageReporter(ctx, e.Identifier(), req.Model, auth)610 reporter := newUsageReporter(ctx, e.Identifier(), req.Model, auth)
596 defer reporter.trackFailure(ctx, &err)611 defer reporter.trackFailure(ctx, &err)
597 612
613 upstreamModel := util.ResolveOriginalModel(req.Model, req.Metadata)
614
598 from := opts.SourceFormat615 from := opts.SourceFormat
599 to := sdktranslator.FromString("gemini")616 to := sdktranslator.FromString("gemini")
600 body := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), true)617 body := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), true)
601 if budgetOverride, includeOverride, ok := util.GeminiThinkingFromMetadata(req.Metadata); ok && util.ModelSupportsThinking(req.Model) {618 if budgetOverride, includeOverride, ok := util.ResolveThinkingConfigFromMetadata(req.Model, req.Metadata); ok && util.ModelSupportsThinking(req.Model) {
602 if budgetOverride != nil {619 if budgetOverride != nil {
603 norm := util.NormalizeThinkingBudget(req.Model, *budgetOverride)620 norm := util.NormalizeThinkingBudget(req.Model, *budgetOverride)
604 budgetOverride = &norm621 budgetOverride = &norm
@@ -610,12 +627,13 @@ func (e *GeminiVertexExecutor) executeStreamWithAPIKey(ctx context.Context, auth · 5 unmodified lines
610 body = util.StripThinkingConfigIfUnsupported(req.Model, body)627 body = util.StripThinkingConfigIfUnsupported(req.Model, body)
611 body = fixGeminiImageAspectRatio(req.Model, body)628 body = fixGeminiImageAspectRatio(req.Model, body)
612 body = applyPayloadConfig(e.cfg, req.Model, body)629 body = applyPayloadConfig(e.cfg, req.Model, body)
630 body, _ = sjson.SetBytes(body, "model", upstreamModel)
613 631
614 // For API key auth, use simpler URL format without project/location632 // For API key auth, use simpler URL format without project/location
615 if baseURL == "" {633 if baseURL == "" {
616 baseURL = "https://generativelanguage.googleapis.com"634 baseURL = "https://generativelanguage.googleapis.com"
617 }635 }
618 url := fmt.Sprintf("%s/%s/publishers/google/models/%s:%s", baseURL, vertexAPIVersion, req.Model, "streamGenerateContent")636 url := fmt.Sprintf("%s/%s/publishers/google/models/%s:%s", baseURL, vertexAPIVersion, upstreamModel, "streamGenerateContent")
619 if opts.Alt == "" {637 if opts.Alt == "" {
620 url = url + "?alt=sse"638 url = url + "?alt=sse"
621 } else {639 } else {
internal/runtime/executor/iflow_executor.go+8−0
@@ -57,6 +57,10 @@ func (e *IFlowExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, re · 56 unmodified lines
57 from := opts.SourceFormat57 from := opts.SourceFormat
58 to := sdktranslator.FromString("openai")58 to := sdktranslator.FromString("openai")
59 body := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), false)59 body := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), false)
60 body = applyReasoningEffortMetadataChatCompletions(body, req.Metadata, req.Model)
61 if upstreamModel := util.ResolveOriginalModel(req.Model, req.Metadata); upstreamModel != "" {
62 body, _ = sjson.SetBytes(body, "model", upstreamModel)
63 }
60 body = applyPayloadConfig(e.cfg, req.Model, body)64 body = applyPayloadConfig(e.cfg, req.Model, body)
61 65
62 endpoint := strings.TrimSuffix(baseURL, "/") + iflowDefaultEndpoint66 endpoint := strings.TrimSuffix(baseURL, "/") + iflowDefaultEndpoint
@@ -139,6 +143,10 @@ func (e *IFlowExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Au · 76 unmodified lines
139 to := sdktranslator.FromString("openai")143 to := sdktranslator.FromString("openai")
140 body := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), true)144 body := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), true)
141 145
146 body = applyReasoningEffortMetadataChatCompletions(body, req.Metadata, req.Model)
147 if upstreamModel := util.ResolveOriginalModel(req.Model, req.Metadata); upstreamModel != "" {
148 body, _ = sjson.SetBytes(body, "model", upstreamModel)
149 }
142 // Ensure tools array exists to avoid provider quirks similar to Qwen's behaviour.150 // Ensure tools array exists to avoid provider quirks similar to Qwen's behaviour.
143 toolsResult := gjson.GetBytes(body, "tools")151 toolsResult := gjson.GetBytes(body, "tools")
144 if toolsResult.Exists() && toolsResult.IsArray() && len(toolsResult.Array()) == 0 {152 if toolsResult.Exists() && toolsResult.IsArray() && len(toolsResult.Array()) == 0 {
internal/runtime/executor/openai_compat_executor.go+8−0
@@ -58,6 +58,10 @@ func (e *OpenAICompatExecutor) Execute(ctx context.Context, auth *cliproxyauth.A · 57 unmodified lines
58 translated = e.overrideModel(translated, modelOverride)58 translated = e.overrideModel(translated, modelOverride)
59 }59 }
60 translated = applyPayloadConfigWithRoot(e.cfg, req.Model, to.String(), "", translated)60 translated = applyPayloadConfigWithRoot(e.cfg, req.Model, to.String(), "", translated)
61 translated = applyReasoningEffortMetadataChatCompletions(translated, req.Metadata, req.Model)
62 if upstreamModel := util.ResolveOriginalModel(req.Model, req.Metadata); upstreamModel != "" {
63 translated, _ = sjson.SetBytes(translated, "model", upstreamModel)
64 }
61 65
62 url := strings.TrimSuffix(baseURL, "/") + "/chat/completions"66 url := strings.TrimSuffix(baseURL, "/") + "/chat/completions"
63 httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(translated))67 httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(translated))
@@ -143,6 +147,10 @@ func (e *OpenAICompatExecutor) ExecuteStream(ctx context.Context, auth *cliproxy · 79 unmodified lines
143 translated = e.overrideModel(translated, modelOverride)147 translated = e.overrideModel(translated, modelOverride)
144 }148 }
145 translated = applyPayloadConfigWithRoot(e.cfg, req.Model, to.String(), "", translated)149 translated = applyPayloadConfigWithRoot(e.cfg, req.Model, to.String(), "", translated)
150 translated = applyReasoningEffortMetadataChatCompletions(translated, req.Metadata, req.Model)
151 if upstreamModel := util.ResolveOriginalModel(req.Model, req.Metadata); upstreamModel != "" {
152 translated, _ = sjson.SetBytes(translated, "model", upstreamModel)
153 }
146 154
147 url := strings.TrimSuffix(baseURL, "/") + "/chat/completions"155 url := strings.TrimSuffix(baseURL, "/") + "/chat/completions"
148 httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(translated))156 httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(translated))
internal/runtime/executor/payload_helpers.go+48−5
@@ -12,8 +12,8 @@ import ( · 11 unmodified lines
12// applyThinkingMetadata applies thinking config from model suffix metadata (e.g., -reasoning, -thinking-N)12// applyThinkingMetadata applies thinking config from model suffix metadata (e.g., -reasoning, -thinking-N)
13// for standard Gemini format payloads. It normalizes the budget when the model supports thinking.13// for standard Gemini format payloads. It normalizes the budget when the model supports thinking.
14func applyThinkingMetadata(payload []byte, metadata map[string]any, model string) []byte {14func applyThinkingMetadata(payload []byte, metadata map[string]any, model string) []byte {
15 budgetOverride, includeOverride, ok := util.GeminiThinkingFromMetadata(metadata)15 budgetOverride, includeOverride, ok := util.ResolveThinkingConfigFromMetadata(model, metadata)
16 if !ok {16 if !ok || (budgetOverride == nil && includeOverride == nil) {
17 return payload17 return payload
18 }18 }
19 if !util.ModelSupportsThinking(model) {19 if !util.ModelSupportsThinking(model) {
@@ -29,17 +29,60 @@ func applyThinkingMetadata(payload []byte, metadata map[string]any, model string · 9 unmodified lines
29// applyThinkingMetadataCLI applies thinking config from model suffix metadata (e.g., -reasoning, -thinking-N)29// applyThinkingMetadataCLI applies thinking config from model suffix metadata (e.g., -reasoning, -thinking-N)
30// for Gemini CLI format payloads (nested under "request"). It normalizes the budget when the model supports thinking.30// for Gemini CLI format payloads (nested under "request"). It normalizes the budget when the model supports thinking.
31func applyThinkingMetadataCLI(payload []byte, metadata map[string]any, model string) []byte {31func applyThinkingMetadataCLI(payload []byte, metadata map[string]any, model string) []byte {
32 budgetOverride, includeOverride, ok := util.GeminiThinkingFromMetadata(metadata)32 budgetOverride, includeOverride, ok := util.ResolveThinkingConfigFromMetadata(model, metadata)
33 if !ok {33 if !ok || (budgetOverride == nil && includeOverride == nil) {
34 return payload34 return payload
35 }35 }
36 if budgetOverride != nil && util.ModelSupportsThinking(model) {36 if !util.ModelSupportsThinking(model) {
37 return payload
38 }
39 if budgetOverride != nil {
37 norm := util.NormalizeThinkingBudget(model, *budgetOverride)40 norm := util.NormalizeThinkingBudget(model, *budgetOverride)
38 budgetOverride = &norm41 budgetOverride = &norm
39 }42 }
40 return util.ApplyGeminiCLIThinkingConfig(payload, budgetOverride, includeOverride)43 return util.ApplyGeminiCLIThinkingConfig(payload, budgetOverride, includeOverride)
41}44}
42 45
46// applyReasoningEffortMetadata applies reasoning effort overrides (reasoning.effort) when present in metadata.
47// It avoids overwriting an existing reasoning.effort field and only applies to models that support thinking.
48func applyReasoningEffortMetadata(payload []byte, metadata map[string]any, model string) []byte {
49 if len(metadata) == 0 {
50 return payload
51 }
52 if !util.ModelSupportsThinking(model) {
53 return payload
54 }
55 if gjson.GetBytes(payload, "reasoning.effort").Exists() {
56 return payload
57 }
58 if effort, ok := util.ReasoningEffortFromMetadata(metadata); ok && effort != "" {
59 if updated, err := sjson.SetBytes(payload, "reasoning.effort", effort); err == nil {
60 return updated
61 }
62 }
63 return payload
64}
65
66// applyReasoningEffortMetadataChatCompletions applies reasoning_effort (OpenAI chat completions field)
67// when present in metadata. It avoids overwriting an existing reasoning_effort field.
68func applyReasoningEffortMetadataChatCompletions(payload []byte, metadata map[string]any, model string) []byte {
69 if len(metadata) == 0 {
70 return payload
71 }
72 if !util.ModelSupportsThinking(model) {
73 return payload
74 }
75 if gjson.GetBytes(payload, "reasoning_effort").Exists() {
76 return payload
77 }
78 if effort, ok := util.ReasoningEffortFromMetadata(metadata); ok && effort != "" {
79 if updated, err := sjson.SetBytes(payload, "reasoning_effort", effort); err == nil {
80 return updated
81 }
82 }
83 return payload
84}
85
43// applyPayloadConfig applies payload default and override rules from configuration86// applyPayloadConfig applies payload default and override rules from configuration
44// to the given JSON payload for the specified model.87// to the given JSON payload for the specified model.
45// Defaults only fill missing fields, while overrides always overwrite existing values.88// Defaults only fill missing fields, while overrides always overwrite existing values.
internal/runtime/executor/qwen_executor.go+9−0
@@ -12,6 +12,7 @@ import ( · 11 unmodified lines
12 12
13 qwenauth "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/qwen"13 qwenauth "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/qwen"
14 "github.com/router-for-me/CLIProxyAPI/v6/internal/config"14 "github.com/router-for-me/CLIProxyAPI/v6/internal/config"
15 "github.com/router-for-me/CLIProxyAPI/v6/internal/util"
15 cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"16 cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
16 cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor"17 cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor"
17 sdktranslator "github.com/router-for-me/CLIProxyAPI/v6/sdk/translator"18 sdktranslator "github.com/router-for-me/CLIProxyAPI/v6/sdk/translator"
@@ -50,6 +51,10 @@ func (e *QwenExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, req · 32 unmodified lines
50 from := opts.SourceFormat51 from := opts.SourceFormat
51 to := sdktranslator.FromString("openai")52 to := sdktranslator.FromString("openai")
52 body := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), false)53 body := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), false)
54 body = applyReasoningEffortMetadataChatCompletions(body, req.Metadata, req.Model)
55 if upstreamModel := util.ResolveOriginalModel(req.Model, req.Metadata); upstreamModel != "" {
56 body, _ = sjson.SetBytes(body, "model", upstreamModel)
57 }
53 body = applyPayloadConfig(e.cfg, req.Model, body)58 body = applyPayloadConfig(e.cfg, req.Model, body)
54 59
55 url := strings.TrimSuffix(baseURL, "/") + "/chat/completions"60 url := strings.TrimSuffix(baseURL, "/") + "/chat/completions"
@@ -121,6 +126,10 @@ func (e *QwenExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Aut · 65 unmodified lines
121 to := sdktranslator.FromString("openai")126 to := sdktranslator.FromString("openai")
122 body := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), true)127 body := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), true)
123 128
129 body = applyReasoningEffortMetadataChatCompletions(body, req.Metadata, req.Model)
130 if upstreamModel := util.ResolveOriginalModel(req.Model, req.Metadata); upstreamModel != "" {
131 body, _ = sjson.SetBytes(body, "model", upstreamModel)
132 }
124 toolsResult := gjson.GetBytes(body, "tools")133 toolsResult := gjson.GetBytes(body, "tools")
125 // I'm addressing the Qwen3 "poisoning" issue, which is caused by the model needing a tool to be defined. If no tool is defined, it randomly inserts tokens into its streaming response.134 // I'm addressing the Qwen3 "poisoning" issue, which is caused by the model needing a tool to be defined. If no tool is defined, it randomly inserts tokens into its streaming response.
126 // This will have no real consequences. It's just to scare Qwen3.135 // This will have no real consequences. It's just to scare Qwen3.
internal/util/gemini_thinking.go+0−150
@@ -1,8 +1,6 @@
1package util1package util
2 2
3import (3import (
4 "encoding/json"
5 "strconv"
6 "strings"4 "strings"
7 5
8 "github.com/tidwall/gjson"6 "github.com/tidwall/gjson"
@@ -15,80 +13,6 @@ const ( · 6 unmodified lines
15 GeminiOriginalModelMetadataKey = "gemini_original_model"13 GeminiOriginalModelMetadataKey = "gemini_original_model"
16)14)
17 15
18func ParseGeminiThinkingSuffix(model string) (string, *int, *bool, bool) {
19 if model == "" {
20 return model, nil, nil, false
21 }
22 lower := strings.ToLower(model)
23 if !strings.HasPrefix(lower, "gemini-") {
24 return model, nil, nil, false
25 }
26
27 if strings.HasSuffix(lower, "-nothinking") {
28 base := model[:len(model)-len("-nothinking")]
29 budgetValue := 0
30 if strings.HasPrefix(lower, "gemini-2.5-pro") {
31 budgetValue = 128
32 }
33 include := false
34 return base, &budgetValue, &include, true
35 }
36
37 // Handle "-reasoning" suffix: enables thinking with dynamic budget (-1)
38 // Maps: gemini-2.5-flash-reasoning -> gemini-2.5-flash with thinkingBudget=-1
39 if strings.HasSuffix(lower, "-reasoning") {
40 base := model[:len(model)-len("-reasoning")]
41 budgetValue := -1 // Dynamic budget
42 include := true
43 return base, &budgetValue, &include, true
44 }
45
46 idx := strings.LastIndex(lower, "-thinking-")
47 if idx == -1 {
48 return model, nil, nil, false
49 }
50
51 digits := model[idx+len("-thinking-"):]
52 if digits == "" {
53 return model, nil, nil, false
54 }
55 end := len(digits)
56 for i := 0; i < len(digits); i++ {
57 if digits[i] < '0' || digits[i] > '9' {
58 end = i
59 break
60 }
61 }
62 if end == 0 {
63 return model, nil, nil, false
64 }
65 valueStr := digits[:end]
66 value, err := strconv.Atoi(valueStr)
67 if err != nil {
68 return model, nil, nil, false
69 }
70 base := model[:idx]
71 budgetValue := value
72 return base, &budgetValue, nil, true
73}
74
75func NormalizeGeminiThinkingModel(modelName string) (string, map[string]any) {
76 baseModel, budget, include, matched := ParseGeminiThinkingSuffix(modelName)
77 if !matched {
78 return baseModel, nil
79 }
80 metadata := map[string]any{
81 GeminiOriginalModelMetadataKey: modelName,
82 }
83 if budget != nil {
84 metadata[GeminiThinkingBudgetMetadataKey] = *budget
85 }
86 if include != nil {
87 metadata[GeminiIncludeThoughtsMetadataKey] = *include
88 }
89 return baseModel, metadata
90}
91
92func ApplyGeminiThinkingConfig(body []byte, budget *int, includeThoughts *bool) []byte {16func ApplyGeminiThinkingConfig(body []byte, budget *int, includeThoughts *bool) []byte {
93 if budget == nil && includeThoughts == nil {17 if budget == nil && includeThoughts == nil {
94 return body18 return body
@@ -133,80 +57,6 @@ func ApplyGeminiCLIThinkingConfig(body []byte, budget *int, includeThoughts *boo · 38 unmodified lines
133 return updated57 return updated
134}58}
135 59
136func GeminiThinkingFromMetadata(metadata map[string]any) (*int, *bool, bool) {
137 if len(metadata) == 0 {
138 return nil, nil, false
139 }
140 var (
141 budgetPtr *int
142 includePtr *bool
143 matched bool
144 )
145 if rawBudget, ok := metadata[GeminiThinkingBudgetMetadataKey]; ok {
146 switch v := rawBudget.(type) {
147 case int:
148 budget := v
149 budgetPtr = &budget
150 matched = true
151 case int32:
152 budget := int(v)
153 budgetPtr = &budget
154 matched = true
155 case int64:
156 budget := int(v)
157 budgetPtr = &budget
158 matched = true
159 case float64:
160 budget := int(v)
161 budgetPtr = &budget
162 matched = true
163 case json.Number:
164 if val, err := v.Int64(); err == nil {
165 budget := int(val)
166 budgetPtr = &budget
167 matched = true
168 }
169 }
170 }
171 if rawInclude, ok := metadata[GeminiIncludeThoughtsMetadataKey]; ok {
172 switch v := rawInclude.(type) {
173 case bool:
174 include := v
175 includePtr = &include
176 matched = true
177 case string:
178 if parsed, err := strconv.ParseBool(v); err == nil {
179 include := parsed
180 includePtr = &include
181 matched = true
182 }
183 case json.Number:
184 if val, err := v.Int64(); err == nil {
185 include := val != 0
186 includePtr = &include
187 matched = true
188 }
189 case int:
190 include := v != 0
191 includePtr = &include
192 matched = true
193 case int32:
194 include := v != 0
195 includePtr = &include
196 matched = true
197 case int64:
198 include := v != 0
199 includePtr = &include
200 matched = true
201 case float64:
202 include := v != 0
203 includePtr = &include
204 matched = true
205 }
206 }
207 return budgetPtr, includePtr, matched
208}
209
210// modelsWithDefaultThinking lists models that should have thinking enabled by default60// modelsWithDefaultThinking lists models that should have thinking enabled by default
211// when no explicit thinkingConfig is provided.61// when no explicit thinkingConfig is provided.
212var modelsWithDefaultThinking = map[string]bool{62var modelsWithDefaultThinking = map[string]bool{
internal/util/thinking_suffix.go+327−0
@@ -0,0 +1,327 @@
1package util
2
3import (
4 "encoding/json"
5 "strconv"
6 "strings"
7)
8
9const (
10 ThinkingBudgetMetadataKey = "thinking_budget"
11 ThinkingIncludeThoughtsMetadataKey = "thinking_include_thoughts"
12 ReasoningEffortMetadataKey = "reasoning_effort"
13 ThinkingOriginalModelMetadataKey = "thinking_original_model"
14)
15
16// NormalizeThinkingModel parses dynamic thinking suffixes on model names and returns
17// the normalized base model with extracted metadata. Supported patterns:
18// - "-thinking-<number>" extracts a numeric budget
19// - "-thinking-<level>" extracts a reasoning effort level (minimal/low/medium/high/xhigh/auto/none)
20// - "-thinking" maps to a default reasoning effort of "medium"
21// - "-reasoning" maps to dynamic budget (-1) and include_thoughts=true
22// - "-nothinking" maps to budget=0 and include_thoughts=false
23func NormalizeThinkingModel(modelName string) (string, map[string]any) {
24 if modelName == "" {
25 return modelName, nil
26 }
27
28 lower := strings.ToLower(modelName)
29 baseModel := modelName
30
31 var (
32 budgetOverride *int
33 includeThoughts *bool
34 reasoningEffort *string
35 matched bool
36 )
37
38 switch {
39 case strings.HasSuffix(lower, "-nothinking"):
40 baseModel = modelName[:len(modelName)-len("-nothinking")]
41 budget := 0
42 include := false
43 budgetOverride = &budget
44 includeThoughts = &include
45 matched = true
46 case strings.HasSuffix(lower, "-reasoning"):
47 baseModel = modelName[:len(modelName)-len("-reasoning")]
48 budget := -1
49 include := true
50 budgetOverride = &budget
51 includeThoughts = &include
52 matched = true
53 default:
54 if idx := strings.LastIndex(lower, "-thinking-"); idx != -1 {
55 value := modelName[idx+len("-thinking-"):]
56 if value != "" {
57 if parsed, ok := parseIntPrefix(value); ok {
58 baseModel = modelName[:idx]
59 budgetOverride = &parsed
60 matched = true
61 } else if effort, okEffort := normalizeReasoningEffort(value); okEffort {
62 baseModel = modelName[:idx]
63 reasoningEffort = &effort
64 matched = true
65 }
66 }
67 } else if strings.HasSuffix(lower, "-thinking") {
68 baseModel = modelName[:len(modelName)-len("-thinking")]
69 effort := "medium"
70 reasoningEffort = &effort
71 matched = true
72 }
73 }
74
75 if !matched {
76 return baseModel, nil
77 }
78
79 metadata := map[string]any{
80 ThinkingOriginalModelMetadataKey: modelName,
81 }
82 if budgetOverride != nil {
83 metadata[ThinkingBudgetMetadataKey] = *budgetOverride
84 }
85 if includeThoughts != nil {
86 metadata[ThinkingIncludeThoughtsMetadataKey] = *includeThoughts
87 }
88 if reasoningEffort != nil {
89 metadata[ReasoningEffortMetadataKey] = *reasoningEffort
90 }
91 return baseModel, metadata
92}
93
94// ThinkingFromMetadata extracts thinking overrides from metadata produced by NormalizeThinkingModel.
95// It accepts both the new generic keys and legacy Gemini-specific keys.
96func ThinkingFromMetadata(metadata map[string]any) (*int, *bool, *string, bool) {
97 if len(metadata) == 0 {
98 return nil, nil, nil, false
99 }
100
101 var (
102 budgetPtr *int
103 includePtr *bool
104 effortPtr *string
105 matched bool
106 )
107
108 readBudget := func(key string) {
109 if budgetPtr != nil {
110 return
111 }
112 if raw, ok := metadata[key]; ok {
113 if v, okNumber := parseNumberToInt(raw); okNumber {
114 budget := v
115 budgetPtr = &budget
116 matched = true
117 }
118 }
119 }
120
121 readInclude := func(key string) {
122 if includePtr != nil {
123 return
124 }
125 if raw, ok := metadata[key]; ok {
126 switch v := raw.(type) {
127 case bool:
128 val := v
129 includePtr = &val
130 matched = true
131 case *bool:
132 if v != nil {
133 val := *v
134 includePtr = &val
135 matched = true
136 }
137 }
138 }
139 }
140
141 readEffort := func(key string) {
142 if effortPtr != nil {
143 return
144 }
145 if raw, ok := metadata[key]; ok {
146 if val, okStr := raw.(string); okStr && strings.TrimSpace(val) != "" {
147 normalized := strings.ToLower(strings.TrimSpace(val))
148 effortPtr = &normalized
149 matched = true
150 }
151 }
152 }
153
154 readBudget(ThinkingBudgetMetadataKey)
155 readBudget(GeminiThinkingBudgetMetadataKey)
156 readInclude(ThinkingIncludeThoughtsMetadataKey)
157 readInclude(GeminiIncludeThoughtsMetadataKey)
158 readEffort(ReasoningEffortMetadataKey)
159 readEffort("reasoning.effort")
160
161 return budgetPtr, includePtr, effortPtr, matched
162}
163
164// ResolveThinkingConfigFromMetadata derives thinking budget/include overrides,
165// converting reasoning effort strings into budgets when possible.
166func ResolveThinkingConfigFromMetadata(model string, metadata map[string]any) (*int, *bool, bool) {
167 budget, include, effort, matched := ThinkingFromMetadata(metadata)
168 if !matched {
169 return nil, nil, false
170 }
171
172 if budget == nil && effort != nil {
173 if derived, ok := ThinkingEffortToBudget(model, *effort); ok {
174 budget = &derived
175 }
176 }
177 return budget, include, budget != nil || include != nil || effort != nil
178}
179
180// ReasoningEffortFromMetadata resolves a reasoning effort string from metadata,
181// inferring "auto" and "none" when budgets request dynamic or disabled thinking.
182func ReasoningEffortFromMetadata(metadata map[string]any) (string, bool) {
183 budget, include, effort, matched := ThinkingFromMetadata(metadata)
184 if !matched {
185 return "", false
186 }
187 if effort != nil && *effort != "" {
188 return *effort, true
189 }
190 if budget != nil {
191 switch *budget {
192 case -1:
193 return "auto", true
194 case 0:
195 return "none", true
196 }
197 }
198 if include != nil && !*include {
199 return "none", true
200 }
201 return "", true
202}
203
204// ThinkingEffortToBudget maps reasoning effort levels to approximate budgets,
205// clamping the result to the model's supported range.
206func ThinkingEffortToBudget(model, effort string) (int, bool) {
207 if effort == "" {
208 return 0, false
209 }
210 switch strings.ToLower(effort) {
211 case "none":
212 return 0, true
213 case "auto":
214 return NormalizeThinkingBudget(model, -1), true
215 case "minimal":
216 return NormalizeThinkingBudget(model, 512), true
217 case "low":
218 return NormalizeThinkingBudget(model, 1024), true
219 case "medium":
220 return NormalizeThinkingBudget(model, 8192), true
221 case "high":
222 return NormalizeThinkingBudget(model, 24576), true
223 case "xhigh":
224 return NormalizeThinkingBudget(model, 32768), true
225 default:
226 return 0, false
227 }
228}
229
230// ResolveOriginalModel returns the original model name stored in metadata (if present),
231// otherwise falls back to the provided model.
232func ResolveOriginalModel(model string, metadata map[string]any) string {
233 normalize := func(name string) string {
234 if name == "" {
235 return ""
236 }
237 if base, _ := NormalizeThinkingModel(name); base != "" {
238 return base
239 }
240 return strings.TrimSpace(name)
241 }
242
243 if metadata != nil {
244 if v, ok := metadata[ThinkingOriginalModelMetadataKey]; ok {
245 if s, okStr := v.(string); okStr && strings.TrimSpace(s) != "" {
246 if base := normalize(s); base != "" {
247 return base
248 }
249 }
250 }
251 if v, ok := metadata[GeminiOriginalModelMetadataKey]; ok {
252 if s, okStr := v.(string); okStr && strings.TrimSpace(s) != "" {
253 if base := normalize(s); base != "" {
254 return base
255 }
256 }
257 }
258 }
259 // Fallback: try to re-normalize the model name when metadata was dropped.
260 if base := normalize(model); base != "" {
261 return base
262 }
263 return model
264}
265
266func parseIntPrefix(value string) (int, bool) {
267 if value == "" {
268 return 0, false
269 }
270 digits := strings.TrimLeft(value, "-")
271 if digits == "" {
272 return 0, false
273 }
274 end := len(digits)
275 for i := 0; i < len(digits); i++ {
276 if digits[i] < '0' || digits[i] > '9' {
277 end = i
278 break
279 }
280 }
281 if end == 0 {
282 return 0, false
283 }
284 val, err := strconv.Atoi(digits[:end])
285 if err != nil {
286 return 0, false
287 }
288 return val, true
289}
290
291func parseNumberToInt(raw any) (int, bool) {
292 switch v := raw.(type) {
293 case int:
294 return v, true
295 case int32:
296 return int(v), true
297 case int64:
298 return int(v), true
299 case float64:
300 return int(v), true
301 case json.Number:
302 if val, err := v.Int64(); err == nil {
303 return int(val), true
304 }
305 case string:
306 if strings.TrimSpace(v) == "" {
307 return 0, false
308 }
309 if parsed, err := strconv.Atoi(strings.TrimSpace(v)); err == nil {
310 return parsed, true
311 }
312 }
313 return 0, false
314}
315
316func normalizeReasoningEffort(value string) (string, bool) {
317 if value == "" {
318 return "", false
319 }
320 effort := strings.ToLower(strings.TrimSpace(value))
321 switch effort {
322 case "minimal", "low", "medium", "high", "xhigh", "auto", "none":
323 return effort, true
324 default:
325 return "", false
326 }
327}
sdk/api/handlers/handlers.go+21−7
@@ -343,18 +343,32 @@ func (h *BaseAPIHandler) getRequestDetails(modelName string) (providers []string · 342 unmodified lines
343 343
344 providerName, extractedModelName, isDynamic := h.parseDynamicModel(resolvedModelName)344 providerName, extractedModelName, isDynamic := h.parseDynamicModel(resolvedModelName)
345 345
346 // First, normalize the model name to handle suffixes like "-thinking-128"346 targetModelName := resolvedModelName
347 // This needs to happen before determining the provider for non-dynamic models.347 if isDynamic {
348 normalizedModel, metadata = normalizeModelMetadata(resolvedModelName)348 targetModelName = extractedModelName
349 }
350
351 // Normalize the model name to handle dynamic thinking suffixes before determining the provider.
352 normalizedModel, metadata = normalizeModelMetadata(targetModelName)
349 353
350 if isDynamic {354 if isDynamic {
351 providers = []string{providerName}355 providers = []string{providerName}
352 // For dynamic models, the extractedModelName is already normalized by parseDynamicModel
353 // so we use it as the final normalizedModel.
354 normalizedModel = extractedModelName
355 } else {356 } else {
356 // For non-dynamic models, use the normalizedModel to get the provider name.357 // For non-dynamic models, use the normalizedModel to get the provider name.
357 providers = util.GetProviderName(normalizedModel)358 providers = util.GetProviderName(normalizedModel)
359 if len(providers) == 0 && metadata != nil {
360 if originalRaw, ok := metadata[util.ThinkingOriginalModelMetadataKey]; ok {
361 if originalModel, okStr := originalRaw.(string); okStr {
362 originalModel = strings.TrimSpace(originalModel)
363 if originalModel != "" && !strings.EqualFold(originalModel, normalizedModel) {
364 if altProviders := util.GetProviderName(originalModel); len(altProviders) > 0 {
365 providers = altProviders
366 normalizedModel = originalModel
367 }
368 }
369 }
370 }
371 }
358 }372 }
359 373
360 if len(providers) == 0 {374 if len(providers) == 0 {
@@ -402,7 +416,7 @@ func cloneBytes(src []byte) []byte { · 41 unmodified lines
402}416}
403 417
404func normalizeModelMetadata(modelName string) (string, map[string]any) {418func normalizeModelMetadata(modelName string) (string, map[string]any) {
405 return util.NormalizeGeminiThinkingModel(modelName)419 return util.NormalizeThinkingModel(modelName)
406}420}
407 421
408func cloneMetadata(src map[string]any) map[string]any {422func cloneMetadata(src map[string]any) map[string]any {