History

feat(config): add support for `Fork` in OAuth model mappings with alias handling

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.

Luis Pater committed 10 months ago

Commit 44b6c871 parent 7a77b236 files · +141 −8Browse files

Unified · Split

6 files
  1. config.example.yaml +1 −0
  2. internal/config/config.go +6 −3
  3. internal/config/oauth_model_mappings_test.go +27 −0
  4. internal/watcher/diff/oauth_model_mappings.go +3 −0
  5. sdk/cliproxy/service.go +46 −5
  6. sdk/cliproxy/service_oauth_model_mappings_test.go +58 −0
config.example.yaml+1−0
@@ -206,6 +206,7 @@ ws-auth: false · 205 unmodified lines
206206 # gemini-cli:
207207 # - name: "gemini-2.5-pro" # original model name under this channel
208208 # alias: "g2.5p" # client-visible alias
209+# fork: true # when true, keep original and also add the alias as an extra model (default: false)
209210 # vertex:
210211 # - name: "gemini-2.5-pro"
211212 # alias: "g2.5p"
internal/config/config.go+6−3
@@ -145,11 +145,14 @@ type RoutingConfig struct { · 144 unmodified lines
145145 Strategy string `yaml:"strategy,omitempty" json:"strategy,omitempty"`
146146 }
147147
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.
150152 type ModelNameMapping struct {
151153 Name string `yaml:"name" json:"name"`
152154 Alias string `yaml:"alias" json:"alias"`
155+ Fork bool `yaml:"fork,omitempty" json:"fork,omitempty"`
153156 }
154157
155158 // AmpModelMapping defines a model name mapping for Amp CLI requests.
@@ -551,7 +554,7 @@ func (cfg *Config) SanitizeOAuthModelMappings() { · 395 unmodified lines
551554 }
552555 seenName[nameKey] = struct{}{}
553556 seenAlias[aliasKey] = struct{}{}
554− clean = append(clean, ModelNameMapping{Name: name, Alias: alias})
557+ clean = append(clean, ModelNameMapping{Name: name, Alias: alias, Fork: mapping.Fork})
555558 }
556559 if len(clean) > 0 {
557560 out[channel] = clean
internal/config/oauth_model_mappings_test.go+27−0
@@ -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+}
internal/watcher/diff/oauth_model_mappings.go+3−0
@@ -80,6 +80,9 @@ func summarizeOAuthModelMappingList(list []config.ModelNameMapping) OAuthModelMa · 79 unmodified lines
8080 continue
8181 }
8282 key := name + "->" + alias
83+ if mapping.Fork {
84+ key += "|fork"
85+ }
8386 if _, exists := seen[key]; exists {
8487 continue
8588 }
sdk/cliproxy/service.go+46−5
@@ -1231,7 +1231,13 @@ func applyOAuthModelMappings(cfg *config.Config, provider, authKind string, mode · 1230 unmodified lines
12311231 if len(mappings) == 0 {
12321232 return models
12331233 }
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))
12351241 for i := range mappings {
12361242 name := strings.TrimSpace(mappings[i].Name)
12371243 alias := strings.TrimSpace(mappings[i].Alias)
@@ -1245,7 +1251,7 @@ func applyOAuthModelMappings(cfg *config.Config, provider, authKind string, mode · 7 unmodified lines
12451251 if _, exists := forward[key]; exists {
12461252 continue
12471253 }
1248− forward[key] = alias
1254+ forward[key] = mappingEntry{alias: alias, fork: mappings[i].Fork}
12491255 }
12501256 if len(forward) == 0 {
12511257 return models
@@ -1260,10 +1266,45 @@ func applyOAuthModelMappings(cfg *config.Config, provider, authKind string, mode · 8 unmodified lines
12601266 if id == "" {
12611267 continue
12621268 }
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
12661278 }
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+
12671308 uniqueKey := strings.ToLower(mappedID)
12681309 if _, exists := seen[uniqueKey]; exists {
12691310 continue
sdk/cliproxy/service_oauth_model_mappings_test.go+58−0
@@ -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+}