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
133133 return
134134 }
135135
136− // Normalize model (handles Gemini thinking suffixes)
137− normalizedModel, _ := util.NormalizeGeminiThinkingModel(modelName)
136+ // Normalize model (handles dynamic thinking suffixes)
137+ normalizedModel, _ := util.NormalizeThinkingModel(modelName)
138138
139139 // Track resolved model for logging (may change if mapping is applied)
140140 resolvedModel := normalizedModel
internal/registry/model_definitions.go+1−341
@@ -26,60 +26,6 @@ func GetClaudeModels() []*ModelInfo { · 25 unmodified lines
2626 DisplayName: "Claude 4.5 Sonnet",
2727 ContextLength: 200000,
2828 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,
8329 Thinking: &ThinkingSupport{Min: 1024, Max: 100000, ZeroAllowed: false, DynamicAllowed: true},
8430 },
8531 {
@@ -92,6 +38,7 @@ func GetClaudeModels() []*ModelInfo { · 6 unmodified lines
9238 Description: "Premium model combining maximum intelligence with practical performance",
9339 ContextLength: 200000,
9440 MaxCompletionTokens: 64000,
41+ Thinking: &ThinkingSupport{Min: 1024, Max: 100000, ZeroAllowed: false, DynamicAllowed: true},
9542 },
9643 {
9744 ID: "claude-opus-4-1-20250805",
@@ -530,58 +477,6 @@ func GetOpenAIModels() []*ModelInfo { · 432 unmodified lines
530477 MaxCompletionTokens: 128000,
531478 SupportedParameters: []string{"tools"},
532479 },
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− },
585480 {
586481 ID: "gpt-5-codex",
587482 Object: "model",
@@ -595,45 +490,6 @@ func GetOpenAIModels() []*ModelInfo { · 7 unmodified lines
595490 MaxCompletionTokens: 128000,
596491 SupportedParameters: []string{"tools"},
597492 },
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− },
637493 {
638494 ID: "gpt-5-codex-mini",
639495 Object: "model",
@@ -647,32 +503,6 @@ func GetOpenAIModels() []*ModelInfo { · 7 unmodified lines
647503 MaxCompletionTokens: 128000,
648504 SupportedParameters: []string{"tools"},
649505 },
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− },
676506 {
677507 ID: "gpt-5.1",
678508 Object: "model",
@@ -686,58 +516,6 @@ func GetOpenAIModels() []*ModelInfo { · 7 unmodified lines
686516 MaxCompletionTokens: 128000,
687517 SupportedParameters: []string{"tools"},
688518 },
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− },
741519 {
742520 ID: "gpt-5.1-codex",
743521 Object: "model",
@@ -751,45 +529,6 @@ func GetOpenAIModels() []*ModelInfo { · 7 unmodified lines
751529 MaxCompletionTokens: 128000,
752530 SupportedParameters: []string{"tools"},
753531 },
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− },
793532 {
794533 ID: "gpt-5.1-codex-mini",
795534 Object: "model",
@@ -803,33 +542,6 @@ func GetOpenAIModels() []*ModelInfo { · 7 unmodified lines
803542 MaxCompletionTokens: 128000,
804543 SupportedParameters: []string{"tools"},
805544 },
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−
833545 {
834546 ID: "gpt-5.1-codex-max",
835547 Object: "model",
@@ -843,58 +555,6 @@ func GetOpenAIModels() []*ModelInfo { · 7 unmodified lines
843555 MaxCompletionTokens: 128000,
844556 SupportedParameters: []string{"tools"},
845557 },
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− },
898558 }
899559 }
900560
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
5454 // Use streaming translation to preserve function calling, except for claude.
5555 stream := from != to
5656 body := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), stream)
57− modelForUpstream := req.Model
58− if modelOverride := e.resolveUpstreamModel(req.Model, auth); modelOverride != "" {
59− body, _ = sjson.SetBytes(body, "model", modelOverride)
60− modelForUpstream = modelOverride
57+ upstreamModel := util.ResolveOriginalModel(req.Model, req.Metadata)
58+ if upstreamModel == "" {
59+ upstreamModel = req.Model
60+ }
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+ }
6167 }
62− // Inject thinking config based on model suffix for thinking variants
63− body = e.injectThinkingConfig(req.Model, body)
68+ body, _ = sjson.SetBytes(body, "model", upstreamModel)
69+ // Inject thinking config based on model metadata for thinking variants
70+ body = e.injectThinkingConfig(req.Model, req.Metadata, body)
6471
65− if !strings.HasPrefix(modelForUpstream, "claude-3-5-haiku") {
72+ if !strings.HasPrefix(upstreamModel, "claude-3-5-haiku") {
6673 body = checkSystemInstructions(body)
6774 }
6875 body = applyPayloadConfig(e.cfg, req.Model, body)
@@ -161,11 +168,20 @@ func (e *ClaudeExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.A · 92 unmodified lines
161168 from := opts.SourceFormat
162169 to := sdktranslator.FromString("claude")
163170 body := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), true)
164− if modelOverride := e.resolveUpstreamModel(req.Model, auth); modelOverride != "" {
165− body, _ = sjson.SetBytes(body, "model", modelOverride)
171+ upstreamModel := util.ResolveOriginalModel(req.Model, req.Metadata)
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+ }
166181 }
167− // Inject thinking config based on model suffix for thinking variants
168− body = e.injectThinkingConfig(req.Model, body)
182+ body, _ = sjson.SetBytes(body, "model", upstreamModel)
183+ // Inject thinking config based on model metadata for thinking variants
184+ body = e.injectThinkingConfig(req.Model, req.Metadata, body)
169185 body = checkSystemInstructions(body)
170186 body = applyPayloadConfig(e.cfg, req.Model, body)
171187
@@ -295,13 +311,20 @@ func (e *ClaudeExecutor) CountTokens(ctx context.Context, auth *cliproxyauth.Aut · 123 unmodified lines
295311 // Use streaming translation to preserve function calling, except for claude.
296312 stream := from != to
297313 body := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), stream)
298− modelForUpstream := req.Model
299− if modelOverride := e.resolveUpstreamModel(req.Model, auth); modelOverride != "" {
300− body, _ = sjson.SetBytes(body, "model", modelOverride)
301− modelForUpstream = modelOverride
314+ upstreamModel := util.ResolveOriginalModel(req.Model, req.Metadata)
315+ if upstreamModel == "" {
316+ upstreamModel = req.Model
317+ }
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+ }
302324 }
325+ body, _ = sjson.SetBytes(body, "model", upstreamModel)
303326
304− if !strings.HasPrefix(modelForUpstream, "claude-3-5-haiku") {
327+ if !strings.HasPrefix(upstreamModel, "claude-3-5-haiku") {
305328 body = checkSystemInstructions(body)
306329 }
307330
@@ -427,25 +450,15 @@ func extractAndRemoveBetas(body []byte) ([]string, []byte) { · 119 unmodified lines
427450 return betas, body
428451 }
429452
430−// injectThinkingConfig adds thinking configuration based on model name suffix
431−func (e *ClaudeExecutor) injectThinkingConfig(modelName string, body []byte) []byte {
453+// injectThinkingConfig adds thinking configuration based on metadata or legacy suffixes.
454+func (e *ClaudeExecutor) injectThinkingConfig(modelName string, metadata map[string]any, body []byte) []byte {
432455 // Only inject if thinking config is not already present
433456 if gjson.GetBytes(body, "thinking").Exists() {
434457 return body
435458 }
436459
437− var budgetTokens int
438− switch {
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:
460+ budgetTokens, ok := resolveClaudeThinkingBudget(modelName, metadata)
461+ if !ok || budgetTokens <= 0 {
449462 return body
450463 }
451464
@@ -454,6 +467,44 @@ func (e *ClaudeExecutor) injectThinkingConfig(modelName string, body []byte) []b · 2 unmodified lines
454467 return body
455468 }
456469
470+func 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+
492+func 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+
457508 // ensureMaxTokensForThinking ensures max_tokens > thinking.budget_tokens when thinking is enabled.
458509 // Anthropic API requires this constraint; violating it returns a 400 error.
459510 // 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
491542 }
492543
493544 func (e *ClaudeExecutor) resolveUpstreamModel(alias string, auth *cliproxyauth.Auth) string {
494− if alias == "" {
545+ trimmed := strings.TrimSpace(alias)
546+ if trimmed == "" {
495547 return ""
496548 }
497− // Hardcoded mappings for thinking models to actual Claude model names
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− }
549+
504550 entry := e.resolveClaudeConfig(auth)
505551 if entry == nil {
506552 return ""
507553 }
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+
508566 for i := range entry.Models {
509567 model := entry.Models[i]
510568 name := strings.TrimSpace(model.Name)
511569 modelAlias := strings.TrimSpace(model.Alias)
512− if modelAlias != "" {
513− if strings.EqualFold(modelAlias, alias) {
570+
571+ for _, candidate := range candidates {
572+ if candidate == "" {
573+ continue
574+ }
575+ if modelAlias != "" && strings.EqualFold(modelAlias, candidate) {
514576 if name != "" {
515577 return name
516578 }
517− return alias
579+ return candidate
580+ }
581+ if name != "" && strings.EqualFold(name, candidate) {
582+ return name
518583 }
519− continue
520− }
521− if name != "" && strings.EqualFold(name, alias) {
522− return name
523584 }
524585 }
525586 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
4949 reporter := newUsageReporter(ctx, e.Identifier(), req.Model, auth)
5050 defer reporter.trackFailure(ctx, &err)
5151
52+ upstreamModel := util.ResolveOriginalModel(req.Model, req.Metadata)
53+
5254 from := opts.SourceFormat
5355 to := sdktranslator.FromString("codex")
5456 body := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), false)
55−
56− body = e.setReasoningEffortByAlias(req.Model, body)
57−
57+ body = applyReasoningEffortMetadata(body, req.Metadata, req.Model)
5858 body = applyPayloadConfig(e.cfg, req.Model, body)
59−
59+ body, _ = sjson.SetBytes(body, "model", upstreamModel)
6060 body, _ = sjson.SetBytes(body, "stream", true)
6161 body, _ = sjson.DeleteBytes(body, "previous_response_id")
6262
@@ -142,13 +142,16 @@ func (e *CodexExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Au · 79 unmodified lines
142142 reporter := newUsageReporter(ctx, e.Identifier(), req.Model, auth)
143143 defer reporter.trackFailure(ctx, &err)
144144
145+ upstreamModel := util.ResolveOriginalModel(req.Model, req.Metadata)
146+
145147 from := opts.SourceFormat
146148 to := sdktranslator.FromString("codex")
147149 body := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), true)
148150
149− body = e.setReasoningEffortByAlias(req.Model, body)
151+ body = applyReasoningEffortMetadata(body, req.Metadata, req.Model)
150152 body = applyPayloadConfig(e.cfg, req.Model, body)
151153 body, _ = sjson.DeleteBytes(body, "previous_response_id")
154+ body, _ = sjson.SetBytes(body, "model", upstreamModel)
152155
153156 url := strings.TrimSuffix(baseURL, "/") + "/responses"
154157 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
235238 }
236239
237240 func (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+
238243 from := opts.SourceFormat
239244 to := sdktranslator.FromString("codex")
240245 body := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), false)
241246
242247 modelForCounting := req.Model
243248
244− body = e.setReasoningEffortByAlias(req.Model, body)
245−
249+ body = applyReasoningEffortMetadata(body, req.Metadata, req.Model)
250+ body, _ = sjson.SetBytes(body, "model", upstreamModel)
246251 body, _ = sjson.DeleteBytes(body, "previous_response_id")
247252 body, _ = sjson.SetBytes(body, "stream", false)
248253
@@ -261,83 +266,6 @@ func (e *CodexExecutor) CountTokens(ctx context.Context, auth *cliproxyauth.Auth · 12 unmodified lines
261266 return cliproxyexecutor.Response{Payload: []byte(translated)}, nil
262267 }
263268
264−func (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−
341269 func tokenizerForCodexModel(model string) (tokenizer.Codec, error) {
342270 sanitized := strings.ToLower(strings.TrimSpace(model))
343271 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
7575 reporter := newUsageReporter(ctx, e.Identifier(), req.Model, auth)
7676 defer reporter.trackFailure(ctx, &err)
7777
78+ upstreamModel := util.ResolveOriginalModel(req.Model, req.Metadata)
79+
7880 // Official Gemini API via API key or OAuth bearer
7981 from := opts.SourceFormat
8082 to := sdktranslator.FromString("gemini")
@@ -85,6 +87,7 @@ func (e *GeminiExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, r · 4 unmodified lines
8587 body = util.StripThinkingConfigIfUnsupported(req.Model, body)
8688 body = fixGeminiImageAspectRatio(req.Model, body)
8789 body = applyPayloadConfig(e.cfg, req.Model, body)
90+ body, _ = sjson.SetBytes(body, "model", upstreamModel)
8891
8992 action := "generateContent"
9093 if req.Metadata != nil {
@@ -93,7 +96,7 @@ func (e *GeminiExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, r · 2 unmodified lines
9396 }
9497 }
9598 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)
97100 if opts.Alt != "" && action != "countTokens" {
98101 url = url + fmt.Sprintf("?$alt=%s", opts.Alt)
99102 }
@@ -167,6 +170,8 @@ func (e *GeminiExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.A · 67 unmodified lines
167170 reporter := newUsageReporter(ctx, e.Identifier(), req.Model, auth)
168171 defer reporter.trackFailure(ctx, &err)
169172
173+ upstreamModel := util.ResolveOriginalModel(req.Model, req.Metadata)
174+
170175 from := opts.SourceFormat
171176 to := sdktranslator.FromString("gemini")
172177 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
176181 body = util.StripThinkingConfigIfUnsupported(req.Model, body)
177182 body = fixGeminiImageAspectRatio(req.Model, body)
178183 body = applyPayloadConfig(e.cfg, req.Model, body)
184+ body, _ = sjson.SetBytes(body, "model", upstreamModel)
179185
180186 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")
182188 if opts.Alt == "" {
183189 url = url + "?alt=sse"
184190 } 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
105105
106106 // countTokensWithServiceAccount handles token counting using service account credentials.
107107 func (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+
108110 from := opts.SourceFormat
109111 to := sdktranslator.FromString("gemini")
110112 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) {
112114 if budgetOverride != nil {
113115 norm := util.NormalizeThinkingBudget(req.Model, *budgetOverride)
114116 budgetOverride = &norm
@@ -117,13 +119,14 @@ func (e *GeminiVertexExecutor) countTokensWithServiceAccount(ctx context.Context · 2 unmodified lines
117119 }
118120 translatedReq = util.StripThinkingConfigIfUnsupported(req.Model, translatedReq)
119121 translatedReq = fixGeminiImageAspectRatio(req.Model, translatedReq)
122+ translatedReq, _ = sjson.SetBytes(translatedReq, "model", upstreamModel)
120123 respCtx := context.WithValue(ctx, "alt", opts.Alt)
121124 translatedReq, _ = sjson.DeleteBytes(translatedReq, "tools")
122125 translatedReq, _ = sjson.DeleteBytes(translatedReq, "generationConfig")
123126 translatedReq, _ = sjson.DeleteBytes(translatedReq, "safetySettings")
124127
125128 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")
127130
128131 httpReq, errNewReq := http.NewRequestWithContext(respCtx, http.MethodPost, url, bytes.NewReader(translatedReq))
129132 if errNewReq != nil {
@@ -191,10 +194,12 @@ func (e *GeminiVertexExecutor) countTokensWithServiceAccount(ctx context.Context · 61 unmodified lines
191194
192195 // countTokensWithAPIKey handles token counting using API key credentials.
193196 func (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+
194199 from := opts.SourceFormat
195200 to := sdktranslator.FromString("gemini")
196201 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) {
198203 if budgetOverride != nil {
199204 norm := util.NormalizeThinkingBudget(req.Model, *budgetOverride)
200205 budgetOverride = &norm
@@ -203,6 +208,7 @@ func (e *GeminiVertexExecutor) countTokensWithAPIKey(ctx context.Context, auth * · 2 unmodified lines
203208 }
204209 translatedReq = util.StripThinkingConfigIfUnsupported(req.Model, translatedReq)
205210 translatedReq = fixGeminiImageAspectRatio(req.Model, translatedReq)
211+ translatedReq, _ = sjson.SetBytes(translatedReq, "model", upstreamModel)
206212 respCtx := context.WithValue(ctx, "alt", opts.Alt)
207213 translatedReq, _ = sjson.DeleteBytes(translatedReq, "tools")
208214 translatedReq, _ = sjson.DeleteBytes(translatedReq, "generationConfig")
@@ -286,10 +292,12 @@ func (e *GeminiVertexExecutor) executeWithServiceAccount(ctx context.Context, au · 77 unmodified lines
286292 reporter := newUsageReporter(ctx, e.Identifier(), req.Model, auth)
287293 defer reporter.trackFailure(ctx, &err)
288294
295+ upstreamModel := util.ResolveOriginalModel(req.Model, req.Metadata)
296+
289297 from := opts.SourceFormat
290298 to := sdktranslator.FromString("gemini")
291299 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) {
293301 if budgetOverride != nil {
294302 norm := util.NormalizeThinkingBudget(req.Model, *budgetOverride)
295303 budgetOverride = &norm
@@ -301,6 +309,7 @@ func (e *GeminiVertexExecutor) executeWithServiceAccount(ctx context.Context, au · 5 unmodified lines
301309 body = util.StripThinkingConfigIfUnsupported(req.Model, body)
302310 body = fixGeminiImageAspectRatio(req.Model, body)
303311 body = applyPayloadConfig(e.cfg, req.Model, body)
312+ body, _ = sjson.SetBytes(body, "model", upstreamModel)
304313
305314 action := "generateContent"
306315 if req.Metadata != nil {
@@ -309,7 +318,7 @@ func (e *GeminiVertexExecutor) executeWithServiceAccount(ctx context.Context, au · 2 unmodified lines
309318 }
310319 }
311320 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)
313322 if opts.Alt != "" && action != "countTokens" {
314323 url = url + fmt.Sprintf("?$alt=%s", opts.Alt)
315324 }
@@ -383,10 +392,12 @@ func (e *GeminiVertexExecutor) executeWithAPIKey(ctx context.Context, auth *clip · 67 unmodified lines
383392 reporter := newUsageReporter(ctx, e.Identifier(), req.Model, auth)
384393 defer reporter.trackFailure(ctx, &err)
385394
395+ upstreamModel := util.ResolveOriginalModel(req.Model, req.Metadata)
396+
386397 from := opts.SourceFormat
387398 to := sdktranslator.FromString("gemini")
388399 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) {
390401 if budgetOverride != nil {
391402 norm := util.NormalizeThinkingBudget(req.Model, *budgetOverride)
392403 budgetOverride = &norm
@@ -398,6 +409,7 @@ func (e *GeminiVertexExecutor) executeWithAPIKey(ctx context.Context, auth *clip · 5 unmodified lines
398409 body = util.StripThinkingConfigIfUnsupported(req.Model, body)
399410 body = fixGeminiImageAspectRatio(req.Model, body)
400411 body = applyPayloadConfig(e.cfg, req.Model, body)
412+ body, _ = sjson.SetBytes(body, "model", upstreamModel)
401413
402414 action := "generateContent"
403415 if req.Metadata != nil {
@@ -410,7 +422,7 @@ func (e *GeminiVertexExecutor) executeWithAPIKey(ctx context.Context, auth *clip · 6 unmodified lines
410422 if baseURL == "" {
411423 baseURL = "https://generativelanguage.googleapis.com"
412424 }
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)
414426 if opts.Alt != "" && action != "countTokens" {
415427 url = url + fmt.Sprintf("?$alt=%s", opts.Alt)
416428 }
@@ -481,10 +493,12 @@ func (e *GeminiVertexExecutor) executeStreamWithServiceAccount(ctx context.Conte · 64 unmodified lines
481493 reporter := newUsageReporter(ctx, e.Identifier(), req.Model, auth)
482494 defer reporter.trackFailure(ctx, &err)
483495
496+ upstreamModel := util.ResolveOriginalModel(req.Model, req.Metadata)
497+
484498 from := opts.SourceFormat
485499 to := sdktranslator.FromString("gemini")
486500 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) {
488502 if budgetOverride != nil {
489503 norm := util.NormalizeThinkingBudget(req.Model, *budgetOverride)
490504 budgetOverride = &norm
@@ -496,9 +510,10 @@ func (e *GeminiVertexExecutor) executeStreamWithServiceAccount(ctx context.Conte · 5 unmodified lines
496510 body = util.StripThinkingConfigIfUnsupported(req.Model, body)
497511 body = fixGeminiImageAspectRatio(req.Model, body)
498512 body = applyPayloadConfig(e.cfg, req.Model, body)
513+ body, _ = sjson.SetBytes(body, "model", upstreamModel)
499514
500515 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")
502517 if opts.Alt == "" {
503518 url = url + "?alt=sse"
504519 } else {
@@ -595,10 +610,12 @@ func (e *GeminiVertexExecutor) executeStreamWithAPIKey(ctx context.Context, auth · 90 unmodified lines
595610 reporter := newUsageReporter(ctx, e.Identifier(), req.Model, auth)
596611 defer reporter.trackFailure(ctx, &err)
597612
613+ upstreamModel := util.ResolveOriginalModel(req.Model, req.Metadata)
614+
598615 from := opts.SourceFormat
599616 to := sdktranslator.FromString("gemini")
600617 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) {
602619 if budgetOverride != nil {
603620 norm := util.NormalizeThinkingBudget(req.Model, *budgetOverride)
604621 budgetOverride = &norm
@@ -610,12 +627,13 @@ func (e *GeminiVertexExecutor) executeStreamWithAPIKey(ctx context.Context, auth · 5 unmodified lines
610627 body = util.StripThinkingConfigIfUnsupported(req.Model, body)
611628 body = fixGeminiImageAspectRatio(req.Model, body)
612629 body = applyPayloadConfig(e.cfg, req.Model, body)
630+ body, _ = sjson.SetBytes(body, "model", upstreamModel)
613631
614632 // For API key auth, use simpler URL format without project/location
615633 if baseURL == "" {
616634 baseURL = "https://generativelanguage.googleapis.com"
617635 }
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")
619637 if opts.Alt == "" {
620638 url = url + "?alt=sse"
621639 } 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
5757 from := opts.SourceFormat
5858 to := sdktranslator.FromString("openai")
5959 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+ }
6064 body = applyPayloadConfig(e.cfg, req.Model, body)
6165
6266 endpoint := strings.TrimSuffix(baseURL, "/") + iflowDefaultEndpoint
@@ -139,6 +143,10 @@ func (e *IFlowExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Au · 76 unmodified lines
139143 to := sdktranslator.FromString("openai")
140144 body := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), true)
141145
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+ }
142150 // Ensure tools array exists to avoid provider quirks similar to Qwen's behaviour.
143151 toolsResult := gjson.GetBytes(body, "tools")
144152 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
5858 translated = e.overrideModel(translated, modelOverride)
5959 }
6060 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+ }
6165
6266 url := strings.TrimSuffix(baseURL, "/") + "/chat/completions"
6367 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
143147 translated = e.overrideModel(translated, modelOverride)
144148 }
145149 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+ }
146154
147155 url := strings.TrimSuffix(baseURL, "/") + "/chat/completions"
148156 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
1212 // applyThinkingMetadata applies thinking config from model suffix metadata (e.g., -reasoning, -thinking-N)
1313 // for standard Gemini format payloads. It normalizes the budget when the model supports thinking.
1414 func applyThinkingMetadata(payload []byte, metadata map[string]any, model string) []byte {
15− budgetOverride, includeOverride, ok := util.GeminiThinkingFromMetadata(metadata)
16− if !ok {
15+ budgetOverride, includeOverride, ok := util.ResolveThinkingConfigFromMetadata(model, metadata)
16+ if !ok || (budgetOverride == nil && includeOverride == nil) {
1717 return payload
1818 }
1919 if !util.ModelSupportsThinking(model) {
@@ -29,17 +29,60 @@ func applyThinkingMetadata(payload []byte, metadata map[string]any, model string · 9 unmodified lines
2929 // applyThinkingMetadataCLI applies thinking config from model suffix metadata (e.g., -reasoning, -thinking-N)
3030 // for Gemini CLI format payloads (nested under "request"). It normalizes the budget when the model supports thinking.
3131 func applyThinkingMetadataCLI(payload []byte, metadata map[string]any, model string) []byte {
32− budgetOverride, includeOverride, ok := util.GeminiThinkingFromMetadata(metadata)
33− if !ok {
32+ budgetOverride, includeOverride, ok := util.ResolveThinkingConfigFromMetadata(model, metadata)
33+ if !ok || (budgetOverride == nil && includeOverride == nil) {
3434 return payload
3535 }
36− if budgetOverride != nil && util.ModelSupportsThinking(model) {
36+ if !util.ModelSupportsThinking(model) {
37+ return payload
38+ }
39+ if budgetOverride != nil {
3740 norm := util.NormalizeThinkingBudget(model, *budgetOverride)
3841 budgetOverride = &norm
3942 }
4043 return util.ApplyGeminiCLIThinkingConfig(payload, budgetOverride, includeOverride)
4144 }
4245
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.
48+func 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.
68+func 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+
4386 // applyPayloadConfig applies payload default and override rules from configuration
4487 // to the given JSON payload for the specified model.
4588 // 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
1212
1313 qwenauth "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/qwen"
1414 "github.com/router-for-me/CLIProxyAPI/v6/internal/config"
15+ "github.com/router-for-me/CLIProxyAPI/v6/internal/util"
1516 cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
1617 cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor"
1718 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
5051 from := opts.SourceFormat
5152 to := sdktranslator.FromString("openai")
5253 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+ }
5358 body = applyPayloadConfig(e.cfg, req.Model, body)
5459
5560 url := strings.TrimSuffix(baseURL, "/") + "/chat/completions"
@@ -121,6 +126,10 @@ func (e *QwenExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Aut · 65 unmodified lines
121126 to := sdktranslator.FromString("openai")
122127 body := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), true)
123128
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+ }
124133 toolsResult := gjson.GetBytes(body, "tools")
125134 // 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.
126135 // This will have no real consequences. It's just to scare Qwen3.
internal/util/gemini_thinking.go+0−150
@@ -1,8 +1,6 @@
11 package util
22
33 import (
4− "encoding/json"
5− "strconv"
64 "strings"
75
86 "github.com/tidwall/gjson"
@@ -15,80 +13,6 @@ const ( · 6 unmodified lines
1513 GeminiOriginalModelMetadataKey = "gemini_original_model"
1614 )
1715
18−func 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−
75−func 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−
9216 func ApplyGeminiThinkingConfig(body []byte, budget *int, includeThoughts *bool) []byte {
9317 if budget == nil && includeThoughts == nil {
9418 return body
@@ -133,80 +57,6 @@ func ApplyGeminiCLIThinkingConfig(body []byte, budget *int, includeThoughts *boo · 38 unmodified lines
13357 return updated
13458 }
13559
136−func 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−
21060 // modelsWithDefaultThinking lists models that should have thinking enabled by default
21161 // when no explicit thinkingConfig is provided.
21262 var modelsWithDefaultThinking = map[string]bool{
internal/util/thinking_suffix.go+327−0
@@ -0,0 +1,327 @@
1+package util
2+
3+import (
4+ "encoding/json"
5+ "strconv"
6+ "strings"
7+)
8+
9+const (
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
23+func 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.
96+func 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.
166+func 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.
182+func 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.
206+func 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.
232+func 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+
266+func 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+
291+func 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+
316+func 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
343343
344344 providerName, extractedModelName, isDynamic := h.parseDynamicModel(resolvedModelName)
345345
346− // First, normalize the model name to handle suffixes like "-thinking-128"
347− // This needs to happen before determining the provider for non-dynamic models.
348− normalizedModel, metadata = normalizeModelMetadata(resolvedModelName)
346+ targetModelName := resolvedModelName
347+ if isDynamic {
348+ targetModelName = extractedModelName
349+ }
350+
351+ // Normalize the model name to handle dynamic thinking suffixes before determining the provider.
352+ normalizedModel, metadata = normalizeModelMetadata(targetModelName)
349353
350354 if isDynamic {
351355 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
355356 } else {
356357 // For non-dynamic models, use the normalizedModel to get the provider name.
357358 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+ }
358372 }
359373
360374 if len(providers) == 0 {
@@ -402,7 +416,7 @@ func cloneBytes(src []byte) []byte { · 41 unmodified lines
402416 }
403417
404418 func normalizeModelMetadata(modelName string) (string, map[string]any) {
405− return util.NormalizeGeminiThinkingModel(modelName)
419+ return util.NormalizeThinkingModel(modelName)
406420 }
407421
408422 func cloneMetadata(src map[string]any) map[string]any {