Implemented `Fork` flag in `ModelNameMapping` to allow aliases as additional models while preserving the original model ID. Updated the `applyOAuthModelMappings` logic, added tests for `Fork` behavior, and updated documentation and examples accordingly.
Unified · Split
| @@ -206,6 +206,7 @@ ws-auth: false · 205 unmodified lines | |||
| 206 | # gemini-cli: | 206 | # gemini-cli: |
| 207 | # - name: "gemini-2.5-pro" # original model name under this channel | 207 | # - name: "gemini-2.5-pro" # original model name under this channel |
| 208 | # alias: "g2.5p" # client-visible alias | 208 | # alias: "g2.5p" # client-visible alias |
| 209 | # fork: true # when true, keep original and also add the alias as an extra model (default: false) | ||
| 209 | # vertex: | 210 | # vertex: |
| 210 | # - name: "gemini-2.5-pro" | 211 | # - name: "gemini-2.5-pro" |
| 211 | # alias: "g2.5p" | 212 | # alias: "g2.5p" |
| @@ -145,11 +145,14 @@ type RoutingConfig struct { · 144 unmodified lines | |||
| 145 | Strategy string `yaml:"strategy,omitempty" json:"strategy,omitempty"` | 145 | Strategy string `yaml:"strategy,omitempty" json:"strategy,omitempty"` |
| 146 | } | 146 | } |
| 147 | 147 | ||
| 148 | // ModelNameMapping defines a model ID rename mapping for a specific channel. | 148 | // ModelNameMapping defines a model ID mapping for a specific channel. |
| 149 | // It maps the original model name (Name) to the client-visible alias (Alias). | 149 | // It maps the upstream model name (Name) to the client-visible alias (Alias). |
| 150 | // When Fork is true, the alias is added as an additional model in listings while | ||
| 151 | // keeping the original model ID available. | ||
| 150 | type ModelNameMapping struct { | 152 | type ModelNameMapping struct { |
| 151 | Name string `yaml:"name" json:"name"` | 153 | Name string `yaml:"name" json:"name"` |
| 152 | Alias string `yaml:"alias" json:"alias"` | 154 | Alias string `yaml:"alias" json:"alias"` |
| 155 | Fork bool `yaml:"fork,omitempty" json:"fork,omitempty"` | ||
| 153 | } | 156 | } |
| 154 | 157 | ||
| 155 | // AmpModelMapping defines a model name mapping for Amp CLI requests. | 158 | // AmpModelMapping defines a model name mapping for Amp CLI requests. |
| @@ -551,7 +554,7 @@ func (cfg *Config) SanitizeOAuthModelMappings() { · 395 unmodified lines | |||
| 551 | } | 554 | } |
| 552 | seenName[nameKey] = struct{}{} | 555 | seenName[nameKey] = struct{}{} |
| 553 | seenAlias[aliasKey] = struct{}{} | 556 | seenAlias[aliasKey] = struct{}{} |
| 554 | clean = append(clean, ModelNameMapping{Name: name, Alias: alias}) | 557 | clean = append(clean, ModelNameMapping{Name: name, Alias: alias, Fork: mapping.Fork}) |
| 555 | } | 558 | } |
| 556 | if len(clean) > 0 { | 559 | if len(clean) > 0 { |
| 557 | out[channel] = clean | 560 | out[channel] = clean |
| @@ -0,0 +1,27 @@ | |||
| 1 | package config | ||
| 2 | |||
| 3 | import "testing" | ||
| 4 | |||
| 5 | func TestSanitizeOAuthModelMappings_PreservesForkFlag(t *testing.T) { | ||
| 6 | cfg := &Config{ | ||
| 7 | OAuthModelMappings: map[string][]ModelNameMapping{ | ||
| 8 | " CoDeX ": { | ||
| 9 | {Name: " gpt-5 ", Alias: " g5 ", Fork: true}, | ||
| 10 | {Name: "gpt-6", Alias: "g6"}, | ||
| 11 | }, | ||
| 12 | }, | ||
| 13 | } | ||
| 14 | |||
| 15 | cfg.SanitizeOAuthModelMappings() | ||
| 16 | |||
| 17 | mappings := cfg.OAuthModelMappings["codex"] | ||
| 18 | if len(mappings) != 2 { | ||
| 19 | t.Fatalf("expected 2 sanitized mappings, got %d", len(mappings)) | ||
| 20 | } | ||
| 21 | if mappings[0].Name != "gpt-5" || mappings[0].Alias != "g5" || !mappings[0].Fork { | ||
| 22 | t.Fatalf("expected first mapping to be gpt-5->g5 fork=true, got name=%q alias=%q fork=%v", mappings[0].Name, mappings[0].Alias, mappings[0].Fork) | ||
| 23 | } | ||
| 24 | if mappings[1].Name != "gpt-6" || mappings[1].Alias != "g6" || mappings[1].Fork { | ||
| 25 | t.Fatalf("expected second mapping to be gpt-6->g6 fork=false, got name=%q alias=%q fork=%v", mappings[1].Name, mappings[1].Alias, mappings[1].Fork) | ||
| 26 | } | ||
| 27 | } | ||
| @@ -80,6 +80,9 @@ func summarizeOAuthModelMappingList(list []config.ModelNameMapping) OAuthModelMa · 79 unmodified lines | |||
| 80 | continue | 80 | continue |
| 81 | } | 81 | } |
| 82 | key := name + "->" + alias | 82 | key := name + "->" + alias |
| 83 | if mapping.Fork { | ||
| 84 | key += "|fork" | ||
| 85 | } | ||
| 83 | if _, exists := seen[key]; exists { | 86 | if _, exists := seen[key]; exists { |
| 84 | continue | 87 | continue |
| 85 | } | 88 | } |
| @@ -1231,7 +1231,13 @@ func applyOAuthModelMappings(cfg *config.Config, provider, authKind string, mode · 1230 unmodified lines | |||
| 1231 | if len(mappings) == 0 { | 1231 | if len(mappings) == 0 { |
| 1232 | return models | 1232 | return models |
| 1233 | } | 1233 | } |
| 1234 | forward := make(map[string]string, len(mappings)) | 1234 | |
| 1235 | type mappingEntry struct { | ||
| 1236 | alias string | ||
| 1237 | fork bool | ||
| 1238 | } | ||
| 1239 | |||
| 1240 | forward := make(map[string]mappingEntry, len(mappings)) | ||
| 1235 | for i := range mappings { | 1241 | for i := range mappings { |
| 1236 | name := strings.TrimSpace(mappings[i].Name) | 1242 | name := strings.TrimSpace(mappings[i].Name) |
| 1237 | alias := strings.TrimSpace(mappings[i].Alias) | 1243 | alias := strings.TrimSpace(mappings[i].Alias) |
| @@ -1245,7 +1251,7 @@ func applyOAuthModelMappings(cfg *config.Config, provider, authKind string, mode · 7 unmodified lines | |||
| 1245 | if _, exists := forward[key]; exists { | 1251 | if _, exists := forward[key]; exists { |
| 1246 | continue | 1252 | continue |
| 1247 | } | 1253 | } |
| 1248 | forward[key] = alias | 1254 | forward[key] = mappingEntry{alias: alias, fork: mappings[i].Fork} |
| 1249 | } | 1255 | } |
| 1250 | if len(forward) == 0 { | 1256 | if len(forward) == 0 { |
| 1251 | return models | 1257 | return models |
| @@ -1260,10 +1266,45 @@ func applyOAuthModelMappings(cfg *config.Config, provider, authKind string, mode · 8 unmodified lines | |||
| 1260 | if id == "" { | 1266 | if id == "" { |
| 1261 | continue | 1267 | continue |
| 1262 | } | 1268 | } |
| 1263 | mappedID := id | 1269 | key := strings.ToLower(id) |
| 1264 | if to, ok := forward[strings.ToLower(id)]; ok && strings.TrimSpace(to) != "" { | 1270 | entry, ok := forward[key] |
| 1265 | mappedID = strings.TrimSpace(to) | 1271 | if !ok { |
| 1272 | if _, exists := seen[key]; exists { | ||
| 1273 | continue | ||
| 1274 | } | ||
| 1275 | seen[key] = struct{}{} | ||
| 1276 | out = append(out, model) | ||
| 1277 | continue | ||
| 1266 | } | 1278 | } |
| 1279 | mappedID := strings.TrimSpace(entry.alias) | ||
| 1280 | if mappedID == "" { | ||
| 1281 | if _, exists := seen[key]; exists { | ||
| 1282 | continue | ||
| 1283 | } | ||
| 1284 | seen[key] = struct{}{} | ||
| 1285 | out = append(out, model) | ||
| 1286 | continue | ||
| 1287 | } | ||
| 1288 | |||
| 1289 | if entry.fork { | ||
| 1290 | if _, exists := seen[key]; !exists { | ||
| 1291 | seen[key] = struct{}{} | ||
| 1292 | out = append(out, model) | ||
| 1293 | } | ||
| 1294 | aliasKey := strings.ToLower(mappedID) | ||
| 1295 | if _, exists := seen[aliasKey]; exists { | ||
| 1296 | continue | ||
| 1297 | } | ||
| 1298 | seen[aliasKey] = struct{}{} | ||
| 1299 | clone := *model | ||
| 1300 | clone.ID = mappedID | ||
| 1301 | if clone.Name != "" { | ||
| 1302 | clone.Name = rewriteModelInfoName(clone.Name, id, mappedID) | ||
| 1303 | } | ||
| 1304 | out = append(out, &clone) | ||
| 1305 | continue | ||
| 1306 | } | ||
| 1307 | |||
| 1267 | uniqueKey := strings.ToLower(mappedID) | 1308 | uniqueKey := strings.ToLower(mappedID) |
| 1268 | if _, exists := seen[uniqueKey]; exists { | 1309 | if _, exists := seen[uniqueKey]; exists { |
| 1269 | continue | 1310 | continue |
| @@ -0,0 +1,58 @@ | |||
| 1 | package cliproxy | ||
| 2 | |||
| 3 | import ( | ||
| 4 | "testing" | ||
| 5 | |||
| 6 | "github.com/router-for-me/CLIProxyAPI/v6/sdk/config" | ||
| 7 | ) | ||
| 8 | |||
| 9 | func TestApplyOAuthModelMappings_Rename(t *testing.T) { | ||
| 10 | cfg := &config.Config{ | ||
| 11 | OAuthModelMappings: map[string][]config.ModelNameMapping{ | ||
| 12 | "codex": { | ||
| 13 | {Name: "gpt-5", Alias: "g5"}, | ||
| 14 | }, | ||
| 15 | }, | ||
| 16 | } | ||
| 17 | models := []*ModelInfo{ | ||
| 18 | {ID: "gpt-5", Name: "models/gpt-5"}, | ||
| 19 | } | ||
| 20 | |||
| 21 | out := applyOAuthModelMappings(cfg, "codex", "oauth", models) | ||
| 22 | if len(out) != 1 { | ||
| 23 | t.Fatalf("expected 1 model, got %d", len(out)) | ||
| 24 | } | ||
| 25 | if out[0].ID != "g5" { | ||
| 26 | t.Fatalf("expected model id %q, got %q", "g5", out[0].ID) | ||
| 27 | } | ||
| 28 | if out[0].Name != "models/g5" { | ||
| 29 | t.Fatalf("expected model name %q, got %q", "models/g5", out[0].Name) | ||
| 30 | } | ||
| 31 | } | ||
| 32 | |||
| 33 | func TestApplyOAuthModelMappings_ForkAddsAlias(t *testing.T) { | ||
| 34 | cfg := &config.Config{ | ||
| 35 | OAuthModelMappings: map[string][]config.ModelNameMapping{ | ||
| 36 | "codex": { | ||
| 37 | {Name: "gpt-5", Alias: "g5", Fork: true}, | ||
| 38 | }, | ||
| 39 | }, | ||
| 40 | } | ||
| 41 | models := []*ModelInfo{ | ||
| 42 | {ID: "gpt-5", Name: "models/gpt-5"}, | ||
| 43 | } | ||
| 44 | |||
| 45 | out := applyOAuthModelMappings(cfg, "codex", "oauth", models) | ||
| 46 | if len(out) != 2 { | ||
| 47 | t.Fatalf("expected 2 models, got %d", len(out)) | ||
| 48 | } | ||
| 49 | if out[0].ID != "gpt-5" { | ||
| 50 | t.Fatalf("expected first model id %q, got %q", "gpt-5", out[0].ID) | ||
| 51 | } | ||
| 52 | if out[1].ID != "g5" { | ||
| 53 | t.Fatalf("expected second model id %q, got %q", "g5", out[1].ID) | ||
| 54 | } | ||
| 55 | if out[1].Name != "models/g5" { | ||
| 56 | t.Fatalf("expected forked model name %q, got %q", "models/g5", out[1].Name) | ||
| 57 | } | ||
| 58 | } | ||