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
206# gemini-cli:206# gemini-cli:
207# - name: "gemini-2.5-pro" # original model name under this channel207# - name: "gemini-2.5-pro" # original model name under this channel
208# alias: "g2.5p" # client-visible alias208# 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"
internal/config/config.go+6−3
@@ -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.
150type ModelNameMapping struct {152type 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] = clean560 out[channel] = clean
internal/config/oauth_model_mappings_test.go+27−0
@@ -0,0 +1,27 @@
1package config
2
3import "testing"
4
5func 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
80 continue80 continue
81 }81 }
82 key := name + "->" + alias82 key := name + "->" + alias
83 if mapping.Fork {
84 key += "|fork"
85 }
83 if _, exists := seen[key]; exists {86 if _, exists := seen[key]; exists {
84 continue87 continue
85 }88 }
sdk/cliproxy/service.go+46−5
@@ -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 models1232 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 continue1252 continue
1247 }1253 }
1248 forward[key] = alias1254 forward[key] = mappingEntry{alias: alias, fork: mappings[i].Fork}
1249 }1255 }
1250 if len(forward) == 0 {1256 if len(forward) == 0 {
1251 return models1257 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 continue1267 continue
1262 }1268 }
1263 mappedID := id1269 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 continue1310 continue
sdk/cliproxy/service_oauth_model_mappings_test.go+58−0
@@ -0,0 +1,58 @@
1package cliproxy
2
3import (
4 "testing"
5
6 "github.com/router-for-me/CLIProxyAPI/v6/sdk/config"
7)
8
9func 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
33func 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}