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 | 206 | # gemini-cli: |
| 207 | 207 | # - name: "gemini-2.5-pro" # original model name under this channel |
| 208 | 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 | 210 | # vertex: |
| 210 | 211 | # - name: "gemini-2.5-pro" |
| 211 | 212 | # alias: "g2.5p" |
| @@ -145,11 +145,14 @@ type RoutingConfig struct { · 144 unmodified lines | ||
| 145 | 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. | |
| 149 | −// It maps the original model name (Name) to the client-visible alias (Alias). | |
| 148 | +// ModelNameMapping defines a model ID mapping for a specific channel. | |
| 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 | 152 | type ModelNameMapping struct { |
| 151 | 153 | Name string `yaml:"name" json:"name"` |
| 152 | 154 | Alias string `yaml:"alias" json:"alias"` |
| 155 | + Fork bool `yaml:"fork,omitempty" json:"fork,omitempty"` | |
| 153 | 156 | } |
| 154 | 157 | |
| 155 | 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 | 555 | seenName[nameKey] = struct{}{} |
| 553 | 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 | 559 | if len(clean) > 0 { |
| 557 | 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 | 80 | continue |
| 81 | 81 | } |
| 82 | 82 | key := name + "->" + alias |
| 83 | + if mapping.Fork { | |
| 84 | + key += "|fork" | |
| 85 | + } | |
| 83 | 86 | if _, exists := seen[key]; exists { |
| 84 | 87 | continue |
| 85 | 88 | } |
| @@ -1231,7 +1231,13 @@ func applyOAuthModelMappings(cfg *config.Config, provider, authKind string, mode · 1230 unmodified lines | ||
| 1231 | 1231 | if len(mappings) == 0 { |
| 1232 | 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 | 1241 | for i := range mappings { |
| 1236 | 1242 | name := strings.TrimSpace(mappings[i].Name) |
| 1237 | 1243 | alias := strings.TrimSpace(mappings[i].Alias) |
| @@ -1245,7 +1251,7 @@ func applyOAuthModelMappings(cfg *config.Config, provider, authKind string, mode · 7 unmodified lines | ||
| 1245 | 1251 | if _, exists := forward[key]; exists { |
| 1246 | 1252 | continue |
| 1247 | 1253 | } |
| 1248 | − forward[key] = alias | |
| 1254 | + forward[key] = mappingEntry{alias: alias, fork: mappings[i].Fork} | |
| 1249 | 1255 | } |
| 1250 | 1256 | if len(forward) == 0 { |
| 1251 | 1257 | return models |
| @@ -1260,10 +1266,45 @@ func applyOAuthModelMappings(cfg *config.Config, provider, authKind string, mode · 8 unmodified lines | ||
| 1260 | 1266 | if id == "" { |
| 1261 | 1267 | continue |
| 1262 | 1268 | } |
| 1263 | − mappedID := id | |
| 1264 | − if to, ok := forward[strings.ToLower(id)]; ok && strings.TrimSpace(to) != "" { | |
| 1265 | − mappedID = strings.TrimSpace(to) | |
| 1269 | + key := strings.ToLower(id) | |
| 1270 | + entry, ok := forward[key] | |
| 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 | 1308 | uniqueKey := strings.ToLower(mappedID) |
| 1268 | 1309 | if _, exists := seen[uniqueKey]; exists { |
| 1269 | 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 | +} | |