History

Merge pull request #782 from router-for-me/modelmappings

refactor(config): rename model-name-mappings to oauth-model-mappings

Luis Pater committed 10 months agov6.6.69

Commit 9fa2a7e2 parents f6ab6d9 d443c867 files · +112 −129Browse files

Unified · Split

7 files
  1. config.example.yaml +33 −29
  2. internal/config/config.go +29 −28
  3. internal/watcher/config_reload.go +1 −1
  4. sdk/cliproxy/auth/conductor.go +3 −3
  5. sdk/cliproxy/auth/model_name_mappings.go +33 −24
  6. sdk/cliproxy/builder.go +1 −1
  7. sdk/cliproxy/service.go +12 −43
config.example.yaml+33−29
@@ -156,9 +156,9 @@ ws-auth: false · 155 unmodified lines
156156 # headers:
157157 # X-Custom-Header: "custom-value"
158158 # models: # optional: map aliases to upstream model names
159−# - name: "gemini-2.0-flash" # upstream model name
159+# - name: "gemini-2.5-flash" # upstream model name
160160 # alias: "vertex-flash" # client-visible alias
161−# - name: "gemini-1.5-pro"
161+# - name: "gemini-2.5-pro"
162162 # alias: "vertex-pro"
163163
164164 # Amp Integration
@@ -188,38 +188,42 @@ ws-auth: false · 23 unmodified lines
188188 # # Useful when Amp CLI requests models you don't have access to (e.g., Claude Opus 4.5)
189189 # # but you have a similar model available (e.g., Claude Sonnet 4).
190190 # model-mappings:
191−# - from: "claude-opus-4.5" # Model requested by Amp CLI
192−# to: "claude-sonnet-4" # Route to this available model instead
193−# - from: "gpt-5"
194−# to: "gemini-2.5-pro"
195−# - from: "claude-3-opus-20240229"
196−# to: "claude-3-5-sonnet-20241022"
191+# - from: "claude-opus-4-5-20251101" # Model requested by Amp CLI
192+# to: "gemini-claude-opus-4-5-thinking" # Route to this available model instead
193+# - from: "claude-sonnet-4-5-20250929"
194+# to: "gemini-claude-sonnet-4-5-thinking"
195+# - from: "claude-haiku-4-5-20251001"
196+# to: "gemini-2.5-flash"
197197
198−# Global model name mappings (per channel)
198+# Global OAuth model name mappings (per channel)
199199 # These mappings rename model IDs for both model listing and request routing.
200−# NOTE: Mappings do not apply to codex-api-key, claude-api-key, openai-compatibility, vertex-api-key, or ampcode.
201−# model-name-mappings:
202−# gemini:
203−# - from: "gemini-2.5-pro" # original model name under this channel
204−# to: "gpt-5" # client-visible alias
205−# apikey-gemini:
206−# - from: "gemini-2.5-pro"
207−# to: "gpt-5"
208−# claude:
209−# - from: "claude-sonnet-4"
210−# to: "gpt-4o"
200+# Supported channels: gemini-cli, vertex, aistudio, antigravity, claude, codex, qwen, iflow.
201+# NOTE: Mappings do not apply to gemini-api-key, codex-api-key, claude-api-key, openai-compatibility, vertex-api-key, or ampcode.
202+# oauth-model-mappings:
203+# gemini-cli:
204+# - name: "gemini-2.5-pro" # original model name under this channel
205+# alias: "g2.5p" # client-visible alias
211206 # vertex:
212−# - from: "gemini-2.5-pro"
213−# to: "gpt-5"
207+# - name: "gemini-2.5-pro"
208+# alias: "g2.5p"
209+# aistudio:
210+# - name: "gemini-2.5-pro"
211+# alias: "g2.5p"
212+# antigravity:
213+# - name: "gemini-3-pro-preview"
214+# alias: "g3p"
215+# claude:
216+# - name: "claude-sonnet-4-5-20250929"
217+# alias: "cs4.5"
218+# codex:
219+# - name: "gpt-5"
220+# alias: "g5"
214221 # qwen:
215−# - from: "qwen3-coder-plus"
216−# to: "gpt-4o-mini"
222+# - name: "qwen3-coder-plus"
223+# alias: "qwen-plus"
217224 # iflow:
218−# - from: "glm-4.7"
219−# to: "gpt-5.1-mini"
220−# antigravity:
221−# - from: "gemini-3-pro-preview"
222−# to: "gpt-5"
225+# - name: "glm-4.7"
226+# alias: "glm-god"
223227
224228 # OAuth provider excluded models
225229 # oauth-excluded-models:
internal/config/config.go+29−28
@@ -91,12 +91,13 @@ type Config struct { · 90 unmodified lines
9191 // OAuthExcludedModels defines per-provider global model exclusions applied to OAuth/file-backed auth entries.
9292 OAuthExcludedModels map[string][]string `yaml:"oauth-excluded-models,omitempty" json:"oauth-excluded-models,omitempty"`
9393
94− // ModelNameMappings defines global per-channel model name mappings.
95− // These mappings affect both model listing and model routing for supported channels.
94+ // OAuthModelMappings defines global model name mappings for OAuth/file-backed auth channels.
95+ // These mappings affect both model listing and model routing for supported channels:
96+ // gemini-cli, vertex, aistudio, antigravity, claude, codex, qwen, iflow.
9697 //
9798 // NOTE: This does not apply to existing per-credential model alias features under:
98− // codex-api-key, claude-api-key, openai-compatibility, vertex-api-key, and ampcode.
99− ModelNameMappings map[string][]ModelNameMapping `yaml:"model-name-mappings,omitempty" json:"model-name-mappings,omitempty"`
99+ // gemini-api-key, codex-api-key, claude-api-key, openai-compatibility, vertex-api-key, and ampcode.
100+ OAuthModelMappings map[string][]ModelNameMapping `yaml:"oauth-model-mappings,omitempty" json:"oauth-model-mappings,omitempty"`
100101
101102 // Payload defines default and override rules for provider payload parameters.
102103 Payload PayloadConfig `yaml:"payload" json:"payload"`
@@ -145,10 +146,10 @@ type RoutingConfig struct { · 42 unmodified lines
145146 }
146147
147148 // ModelNameMapping defines a model ID rename mapping for a specific channel.
148−// It maps the original model name (From) to the client-visible alias (To).
149+// It maps the original model name (Name) to the client-visible alias (Alias).
149150 type ModelNameMapping struct {
150− From string `yaml:"from" json:"from"`
151− To string `yaml:"to" json:"to"`
151+ Name string `yaml:"name" json:"name"`
152+ Alias string `yaml:"alias" json:"alias"`
152153 }
153154
154155 // AmpModelMapping defines a model name mapping for Amp CLI requests.
@@ -475,8 +476,8 @@ func LoadConfigOptional(configFile string, optional bool) (*Config, error) { · 320 unmodified lines
475476 // Normalize OAuth provider model exclusion map.
476477 cfg.OAuthExcludedModels = NormalizeOAuthExcludedModels(cfg.OAuthExcludedModels)
477478
478− // Normalize global model name mappings.
479− cfg.SanitizeModelNameMappings()
479+ // Normalize global OAuth model name mappings.
480+ cfg.SanitizeOAuthModelMappings()
480481
481482 if cfg.legacyMigrationPending {
482483 fmt.Println("Detected legacy configuration keys, attempting to persist the normalized config...")
@@ -494,48 +495,48 @@ func LoadConfigOptional(configFile string, optional bool) (*Config, error) { · 11 unmodified lines
494495 return &cfg, nil
495496 }
496497
497−// SanitizeModelNameMappings normalizes and deduplicates global model name mappings.
498+// SanitizeOAuthModelMappings normalizes and deduplicates global OAuth model name mappings.
498499 // It trims whitespace, normalizes channel keys to lower-case, drops empty entries,
499500 // and ensures (From, To) pairs are unique within each channel.
500−func (cfg *Config) SanitizeModelNameMappings() {
501− if cfg == nil || len(cfg.ModelNameMappings) == 0 {
501+func (cfg *Config) SanitizeOAuthModelMappings() {
502+ if cfg == nil || len(cfg.OAuthModelMappings) == 0 {
502503 return
503504 }
504− out := make(map[string][]ModelNameMapping, len(cfg.ModelNameMappings))
505− for rawChannel, mappings := range cfg.ModelNameMappings {
505+ out := make(map[string][]ModelNameMapping, len(cfg.OAuthModelMappings))
506+ for rawChannel, mappings := range cfg.OAuthModelMappings {
506507 channel := strings.ToLower(strings.TrimSpace(rawChannel))
507508 if channel == "" || len(mappings) == 0 {
508509 continue
509510 }
510− seenFrom := make(map[string]struct{}, len(mappings))
511− seenTo := make(map[string]struct{}, len(mappings))
511+ seenName := make(map[string]struct{}, len(mappings))
512+ seenAlias := make(map[string]struct{}, len(mappings))
512513 clean := make([]ModelNameMapping, 0, len(mappings))
513514 for _, mapping := range mappings {
514− from := strings.TrimSpace(mapping.From)
515− to := strings.TrimSpace(mapping.To)
516− if from == "" || to == "" {
515+ name := strings.TrimSpace(mapping.Name)
516+ alias := strings.TrimSpace(mapping.Alias)
517+ if name == "" || alias == "" {
517518 continue
518519 }
519− if strings.EqualFold(from, to) {
520+ if strings.EqualFold(name, alias) {
520521 continue
521522 }
522− fromKey := strings.ToLower(from)
523− toKey := strings.ToLower(to)
524− if _, ok := seenFrom[fromKey]; ok {
523+ nameKey := strings.ToLower(name)
524+ aliasKey := strings.ToLower(alias)
525+ if _, ok := seenName[nameKey]; ok {
525526 continue
526527 }
527− if _, ok := seenTo[toKey]; ok {
528+ if _, ok := seenAlias[aliasKey]; ok {
528529 continue
529530 }
530− seenFrom[fromKey] = struct{}{}
531− seenTo[toKey] = struct{}{}
532− clean = append(clean, ModelNameMapping{From: from, To: to})
531+ seenName[nameKey] = struct{}{}
532+ seenAlias[aliasKey] = struct{}{}
533+ clean = append(clean, ModelNameMapping{Name: name, Alias: alias})
533534 }
534535 if len(clean) > 0 {
535536 out[channel] = clean
536537 }
537538 }
538− cfg.ModelNameMappings = out
539+ cfg.OAuthModelMappings = out
539540 }
540541
541542 // SanitizeOpenAICompatibility removes OpenAI-compatibility provider entries that are
internal/watcher/config_reload.go+1−1
@@ -127,7 +127,7 @@ func (w *Watcher) reloadConfig() bool { · 126 unmodified lines
127127 }
128128
129129 authDirChanged := oldConfig == nil || oldConfig.AuthDir != newConfig.AuthDir
130− forceAuthRefresh := oldConfig != nil && (oldConfig.ForceModelPrefix != newConfig.ForceModelPrefix || !reflect.DeepEqual(oldConfig.ModelNameMappings, newConfig.ModelNameMappings))
130+ forceAuthRefresh := oldConfig != nil && (oldConfig.ForceModelPrefix != newConfig.ForceModelPrefix || !reflect.DeepEqual(oldConfig.OAuthModelMappings, newConfig.OAuthModelMappings))
131131
132132 log.Infof("config successfully reloaded, triggering client reload")
133133 w.reloadClients(authDirChanged, affectedOAuthProviders, forceAuthRefresh)
sdk/cliproxy/auth/conductor.go+3−3
@@ -413,7 +413,7 @@ func (m *Manager) executeWithProvider(ctx context.Context, provider string, req · 412 unmodified lines
413413 }
414414 execReq := req
415415 execReq.Model, execReq.Metadata = rewriteModelForAuth(routeModel, req.Metadata, auth)
416− execReq.Metadata = m.applyGlobalModelNameMappingMetadata(auth, execReq.Model, execReq.Metadata)
416+ execReq.Metadata = m.applyOAuthModelMappingMetadata(auth, execReq.Model, execReq.Metadata)
417417 resp, errExec := executor.Execute(execCtx, auth, execReq, opts)
418418 result := Result{AuthID: auth.ID, Provider: provider, Model: routeModel, Success: errExec == nil}
419419 if errExec != nil {
@@ -475,7 +475,7 @@ func (m *Manager) executeCountWithProvider(ctx context.Context, provider string, · 55 unmodified lines
475475 }
476476 execReq := req
477477 execReq.Model, execReq.Metadata = rewriteModelForAuth(routeModel, req.Metadata, auth)
478− execReq.Metadata = m.applyGlobalModelNameMappingMetadata(auth, execReq.Model, execReq.Metadata)
478+ execReq.Metadata = m.applyOAuthModelMappingMetadata(auth, execReq.Model, execReq.Metadata)
479479 resp, errExec := executor.CountTokens(execCtx, auth, execReq, opts)
480480 result := Result{AuthID: auth.ID, Provider: provider, Model: routeModel, Success: errExec == nil}
481481 if errExec != nil {
@@ -537,7 +537,7 @@ func (m *Manager) executeStreamWithProvider(ctx context.Context, provider string · 55 unmodified lines
537537 }
538538 execReq := req
539539 execReq.Model, execReq.Metadata = rewriteModelForAuth(routeModel, req.Metadata, auth)
540− execReq.Metadata = m.applyGlobalModelNameMappingMetadata(auth, execReq.Model, execReq.Metadata)
540+ execReq.Metadata = m.applyOAuthModelMappingMetadata(auth, execReq.Model, execReq.Metadata)
541541 chunks, errStream := executor.ExecuteStream(execCtx, auth, execReq, opts)
542542 if errStream != nil {
543543 rerr := &Error{Message: errStream.Error()}
sdk/cliproxy/auth/model_name_mappings.go+33−24
@@ -26,19 +26,19 @@ func compileModelNameMappingTable(mappings map[string][]internalconfig.ModelName · 25 unmodified lines
2626 }
2727 rev := make(map[string]string, len(entries))
2828 for _, entry := range entries {
29− from := strings.TrimSpace(entry.From)
30− to := strings.TrimSpace(entry.To)
31− if from == "" || to == "" {
29+ name := strings.TrimSpace(entry.Name)
30+ alias := strings.TrimSpace(entry.Alias)
31+ if name == "" || alias == "" {
3232 continue
3333 }
34− if strings.EqualFold(from, to) {
34+ if strings.EqualFold(name, alias) {
3535 continue
3636 }
37− aliasKey := strings.ToLower(to)
37+ aliasKey := strings.ToLower(alias)
3838 if _, exists := rev[aliasKey]; exists {
3939 continue
4040 }
41− rev[aliasKey] = from
41+ rev[aliasKey] = name
4242 }
4343 if len(rev) > 0 {
4444 out.reverse[channel] = rev
@@ -50,10 +50,10 @@ func compileModelNameMappingTable(mappings map[string][]internalconfig.ModelName · 5 unmodified lines
5050 return out
5151 }
5252
53−// SetGlobalModelNameMappings updates the global model name mapping table used during execution.
53+// SetOAuthModelMappings updates the OAuth model name mapping table used during execution.
5454 // The mapping is applied per-auth channel to resolve the upstream model name while keeping the
5555 // client-visible model name unchanged for translation/response formatting.
56−func (m *Manager) SetGlobalModelNameMappings(mappings map[string][]internalconfig.ModelNameMapping) {
56+func (m *Manager) SetOAuthModelMappings(mappings map[string][]internalconfig.ModelNameMapping) {
5757 if m == nil {
5858 return
5959 }
@@ -65,8 +65,8 @@ func (m *Manager) SetGlobalModelNameMappings(mappings map[string][]internalconfi · 5 unmodified lines
6565 m.modelNameMappings.Store(table)
6666 }
6767
68−func (m *Manager) applyGlobalModelNameMappingMetadata(auth *Auth, requestedModel string, metadata map[string]any) map[string]any {
69− original := m.resolveGlobalUpstreamModelForAuth(auth, requestedModel)
68+func (m *Manager) applyOAuthModelMappingMetadata(auth *Auth, requestedModel string, metadata map[string]any) map[string]any {
69+ original := m.resolveOAuthUpstreamModel(auth, requestedModel)
7070 if original == "" {
7171 return metadata
7272 }
@@ -88,11 +88,11 @@ func (m *Manager) applyGlobalModelNameMappingMetadata(auth *Auth, requestedModel · 15 unmodified lines
8888 return out
8989 }
9090
91−func (m *Manager) resolveGlobalUpstreamModelForAuth(auth *Auth, requestedModel string) string {
91+func (m *Manager) resolveOAuthUpstreamModel(auth *Auth, requestedModel string) string {
9292 if m == nil || auth == nil {
9393 return ""
9494 }
95− channel := globalModelMappingChannelForAuth(auth)
95+ channel := modelMappingChannel(auth)
9696 if channel == "" {
9797 return ""
9898 }
@@ -116,7 +116,10 @@ func (m *Manager) resolveGlobalUpstreamModelForAuth(auth *Auth, requestedModel s · 17 unmodified lines
116116 return original
117117 }
118118
119−func globalModelMappingChannelForAuth(auth *Auth) string {
119+// modelMappingChannel extracts the OAuth model mapping channel from an Auth object.
120+// It determines the provider and auth kind from the Auth's attributes and delegates
121+// to OAuthModelMappingChannel for the actual channel resolution.
122+func modelMappingChannel(auth *Auth) string {
120123 if auth == nil {
121124 return ""
122125 }
@@ -130,32 +133,38 @@ func globalModelMappingChannelForAuth(auth *Auth) string { · 7 unmodified lines
130133 authKind = "apikey"
131134 }
132135 }
133− return globalModelMappingChannel(provider, authKind)
136+ return OAuthModelMappingChannel(provider, authKind)
134137 }
135138
136−func globalModelMappingChannel(provider, authKind string) string {
139+// OAuthModelMappingChannel returns the OAuth model mapping channel name for a given provider
140+// and auth kind. Returns empty string if the provider/authKind combination doesn't support
141+// OAuth model mappings (e.g., API key authentication).
142+//
143+// Supported channels: gemini-cli, vertex, aistudio, antigravity, claude, codex, qwen, iflow.
144+func OAuthModelMappingChannel(provider, authKind string) string {
145+ provider = strings.ToLower(strings.TrimSpace(provider))
146+ authKind = strings.ToLower(strings.TrimSpace(authKind))
137147 switch provider {
138148 case "gemini":
139− if authKind == "apikey" {
140− return "apikey-gemini"
141− }
142− return "gemini"
143− case "codex":
149+ // gemini provider uses gemini-api-key config, not oauth-model-mappings.
150+ // OAuth-based gemini auth is converted to "gemini-cli" by the synthesizer.
151+ return ""
152+ case "vertex":
144153 if authKind == "apikey" {
145154 return ""
146155 }
147− return "codex"
156+ return "vertex"
148157 case "claude":
149158 if authKind == "apikey" {
150159 return ""
151160 }
152161 return "claude"
153− case "vertex":
162+ case "codex":
154163 if authKind == "apikey" {
155164 return ""
156165 }
157− return "vertex"
158− case "antigravity", "qwen", "iflow":
166+ return "codex"
167+ case "gemini-cli", "aistudio", "antigravity", "qwen", "iflow":
159168 return provider
160169 default:
161170 return ""
sdk/cliproxy/builder.go+1−1
@@ -215,7 +215,7 @@ func (b *Builder) Build() (*Service, error) { · 214 unmodified lines
215215 }
216216 // Attach a default RoundTripper provider so providers can opt-in per-auth transports.
217217 coreManager.SetRoundTripperProvider(newDefaultRoundTripperProvider())
218− coreManager.SetGlobalModelNameMappings(b.cfg.ModelNameMappings)
218+ coreManager.SetOAuthModelMappings(b.cfg.OAuthModelMappings)
219219
220220 service := &Service{
221221 cfg: b.cfg,
sdk/cliproxy/service.go+12−43
@@ -553,7 +553,7 @@ func (s *Service) Run(ctx context.Context) error { · 552 unmodified lines
553553 s.cfg = newCfg
554554 s.cfgMu.Unlock()
555555 if s.coreManager != nil {
556− s.coreManager.SetGlobalModelNameMappings(newCfg.ModelNameMappings)
556+ s.coreManager.SetOAuthModelMappings(newCfg.OAuthModelMappings)
557557 }
558558 s.rebindExecutors()
559559 }
@@ -844,7 +844,7 @@ func (s *Service) registerModelsForAuth(a *coreauth.Auth) { · 284 unmodified lines
844844 }
845845 }
846846 }
847− models = applyGlobalModelNameMappings(s.cfg, provider, authKind, models)
847+ models = applyOAuthModelMappings(s.cfg, provider, authKind, models)
848848 if len(models) > 0 {
849849 key := provider
850850 if key == "" {
@@ -1154,37 +1154,6 @@ func buildVertexCompatConfigModels(entry *config.VertexCompatKey) []*ModelInfo { · 303 unmodified lines
11541154 return out
11551155 }
11561156
1157−func globalModelMappingChannel(provider, authKind string) string {
1158− provider = strings.ToLower(strings.TrimSpace(provider))
1159− authKind = strings.ToLower(strings.TrimSpace(authKind))
1160− switch provider {
1161− case "gemini":
1162− if authKind == "apikey" {
1163− return "apikey-gemini"
1164− }
1165− return "gemini"
1166− case "codex":
1167− if authKind == "apikey" {
1168− return ""
1169− }
1170− return "codex"
1171− case "claude":
1172− if authKind == "apikey" {
1173− return ""
1174− }
1175− return "claude"
1176− case "vertex":
1177− if authKind == "apikey" {
1178− return ""
1179− }
1180− return "vertex"
1181− case "antigravity", "qwen", "iflow":
1182− return provider
1183− default:
1184− return ""
1185− }
1186−}
1187−
11881157 func rewriteModelInfoName(name, oldID, newID string) string {
11891158 trimmed := strings.TrimSpace(name)
11901159 if trimmed == "" {
@@ -1208,33 +1177,33 @@ func rewriteModelInfoName(name, oldID, newID string) string { · 17 unmodified lines
12081177 return name
12091178 }
12101179
1211−func applyGlobalModelNameMappings(cfg *config.Config, provider, authKind string, models []*ModelInfo) []*ModelInfo {
1180+func applyOAuthModelMappings(cfg *config.Config, provider, authKind string, models []*ModelInfo) []*ModelInfo {
12121181 if cfg == nil || len(models) == 0 {
12131182 return models
12141183 }
1215− channel := globalModelMappingChannel(provider, authKind)
1216− if channel == "" || len(cfg.ModelNameMappings) == 0 {
1184+ channel := coreauth.OAuthModelMappingChannel(provider, authKind)
1185+ if channel == "" || len(cfg.OAuthModelMappings) == 0 {
12171186 return models
12181187 }
1219− mappings := cfg.ModelNameMappings[channel]
1188+ mappings := cfg.OAuthModelMappings[channel]
12201189 if len(mappings) == 0 {
12211190 return models
12221191 }
12231192 forward := make(map[string]string, len(mappings))
12241193 for i := range mappings {
1225− from := strings.TrimSpace(mappings[i].From)
1226− to := strings.TrimSpace(mappings[i].To)
1227− if from == "" || to == "" {
1194+ name := strings.TrimSpace(mappings[i].Name)
1195+ alias := strings.TrimSpace(mappings[i].Alias)
1196+ if name == "" || alias == "" {
12281197 continue
12291198 }
1230− if strings.EqualFold(from, to) {
1199+ if strings.EqualFold(name, alias) {
12311200 continue
12321201 }
1233− key := strings.ToLower(from)
1202+ key := strings.ToLower(name)
12341203 if _, exists := forward[key]; exists {
12351204 continue
12361205 }
1237− forward[key] = to
1206+ forward[key] = alias
12381207 }
12391208 if len(forward) == 0 {
12401209 return models