diff --git a/config.example.yaml b/config.example.yaml index 1e2e4fe9..ed9ad6e8 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -410,9 +410,10 @@ nonstream-keepalive-interval: 0 # client-visible names can become ambiguous across providers. For strict backend pinning, use # unique aliases/prefixes or avoid overlapping names. # You can repeat the same name with different aliases to expose multiple client model names. -# Optional per-entry flags: -# fork: true # keep the upstream model and also expose the alias as a separate client-visible model -# force-mapping: true # optional: rewrite upstream response model fields back to the client-visible alias (example below uses antigravity only) +# Optional per-entry fields: +# fork: true # keep the upstream model and also expose the alias as a separate client-visible model +# display-name: "Model Name" # override the human-readable name shown in model catalogs +# force-mapping: true # rewrite upstream response model fields back to the client-visible alias (example below uses antigravity only) # Per-auth OAuth aliases can also be stored in an OAuth auth JSON file as "model-aliases". # They apply only to that selected auth and take precedence over global aliases for the same client-visible alias. # Example auth JSON: @@ -432,8 +433,9 @@ nonstream-keepalive-interval: 0 # - name: "gemini-2.5-pro" # alias: "g2.5p" # antigravity: -# - name: "gemini-pro-agent" # upstream Antigravity model id -# alias: "gemini-3.1-pro-preview" # client-visible id (Gemini 3.1 Pro Preview) +# - name: "gemini-pro-agent" # upstream Antigravity model id +# alias: "gemini-3.1-pro-preview" # client-visible id (Gemini 3.1 Pro Preview) +# display-name: "Antigravity Gemini 3.1 Pro" # optional catalog display name # fork: true # force-mapping: true # claude: diff --git a/internal/config/config.go b/internal/config/config.go index bcbe250f..c73e4389 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -363,6 +363,9 @@ type OAuthModelAlias struct { Alias string `yaml:"alias" json:"alias"` Fork bool `yaml:"fork,omitempty" json:"fork,omitempty"` + // DisplayName is the optional human-readable name shown in model catalogs. + DisplayName string `yaml:"display-name,omitempty" json:"display-name,omitempty"` + ForceMapping bool `yaml:"force-mapping,omitempty" json:"force-mapping,omitempty"` } @@ -991,7 +994,13 @@ func (cfg *Config) SanitizeOAuthModelAlias() { continue } seenAlias[aliasKey] = struct{}{} - clean = append(clean, OAuthModelAlias{Name: name, Alias: alias, Fork: entry.Fork, ForceMapping: entry.ForceMapping}) + clean = append(clean, OAuthModelAlias{ + Name: name, + Alias: alias, + Fork: entry.Fork, + DisplayName: strings.TrimSpace(entry.DisplayName), + ForceMapping: entry.ForceMapping, + }) } if len(clean) > 0 { out[channel] = clean diff --git a/internal/config/oauth_model_alias_test.go b/internal/config/oauth_model_alias_test.go index a5886474..01fbf4b5 100644 --- a/internal/config/oauth_model_alias_test.go +++ b/internal/config/oauth_model_alias_test.go @@ -2,11 +2,11 @@ package config import "testing" -func TestSanitizeOAuthModelAlias_PreservesForkFlag(t *testing.T) { +func TestSanitizeOAuthModelAlias_PreservesOptionalFields(t *testing.T) { cfg := &Config{ OAuthModelAlias: map[string][]OAuthModelAlias{ " CoDeX ": { - {Name: " gpt-5 ", Alias: " g5 ", Fork: true}, + {Name: " gpt-5 ", Alias: " g5 ", Fork: true, DisplayName: " GPT Five ", ForceMapping: true}, {Name: "gpt-6", Alias: "g6"}, }, }, @@ -18,11 +18,11 @@ func TestSanitizeOAuthModelAlias_PreservesForkFlag(t *testing.T) { if len(aliases) != 2 { t.Fatalf("expected 2 sanitized aliases, got %d", len(aliases)) } - if aliases[0].Name != "gpt-5" || aliases[0].Alias != "g5" || !aliases[0].Fork { - t.Fatalf("expected first alias to be gpt-5->g5 fork=true, got name=%q alias=%q fork=%v", aliases[0].Name, aliases[0].Alias, aliases[0].Fork) + if aliases[0].Name != "gpt-5" || aliases[0].Alias != "g5" || !aliases[0].Fork || aliases[0].DisplayName != "GPT Five" || !aliases[0].ForceMapping { + t.Fatalf("unexpected sanitized first alias: %+v", aliases[0]) } - if aliases[1].Name != "gpt-6" || aliases[1].Alias != "g6" || aliases[1].Fork { - t.Fatalf("expected second alias to be gpt-6->g6 fork=false, got name=%q alias=%q fork=%v", aliases[1].Name, aliases[1].Alias, aliases[1].Fork) + if aliases[1].Name != "gpt-6" || aliases[1].Alias != "g6" || aliases[1].Fork || aliases[1].DisplayName != "" || aliases[1].ForceMapping { + t.Fatalf("unexpected sanitized second alias: %+v", aliases[1]) } } diff --git a/internal/watcher/diff/oauth_model_alias.go b/internal/watcher/diff/oauth_model_alias.go index d95bfd39..45b2f4df 100644 --- a/internal/watcher/diff/oauth_model_alias.go +++ b/internal/watcher/diff/oauth_model_alias.go @@ -83,6 +83,9 @@ func summarizeOAuthModelAliasList(list []config.OAuthModelAlias) OAuthModelAlias if alias.Fork { key += "|fork" } + if displayName := strings.TrimSpace(alias.DisplayName); displayName != "" { + key += "|display-name=" + displayName + } if alias.ForceMapping { key += "|force-mapping" } diff --git a/internal/watcher/diff/oauth_model_alias_test.go b/internal/watcher/diff/oauth_model_alias_test.go new file mode 100644 index 00000000..7cd89aee --- /dev/null +++ b/internal/watcher/diff/oauth_model_alias_test.go @@ -0,0 +1,26 @@ +package diff + +import ( + "testing" + + "github.com/router-for-me/CLIProxyAPI/v7/internal/config" +) + +func TestDiffOAuthModelAliasChanges_IncludesDisplayName(t *testing.T) { + oldMap := map[string][]config.OAuthModelAlias{ + "antigravity": { + {Name: "claude-opus-4-6-thinking", Alias: "claude-antigravity-opus-4-6-thinking", DisplayName: "Antigravity Opus 4.6"}, + }, + } + newMap := map[string][]config.OAuthModelAlias{ + "antigravity": { + {Name: "claude-opus-4-6-thinking", Alias: "claude-antigravity-opus-4-6-thinking", DisplayName: "Antigravity Opus 4.6 (Thinking)"}, + }, + } + + changes, affected := DiffOAuthModelAliasChanges(oldMap, newMap) + expectContains(t, changes, "oauth-model-alias[antigravity]: updated (1 -> 1 entries)") + if len(affected) != 1 || affected[0] != "antigravity" { + t.Fatalf("expected antigravity to be affected, got %#v", affected) + } +} diff --git a/sdk/cliproxy/service.go b/sdk/cliproxy/service.go index e711cdf5..50b3822e 100644 --- a/sdk/cliproxy/service.go +++ b/sdk/cliproxy/service.go @@ -2759,8 +2759,9 @@ func oauthModelAliasesForAuth(cfg *config.Config, channel string, attributes map func applyOAuthModelAliasEntries(aliases []config.OAuthModelAlias, models []*ModelInfo) []*ModelInfo { type aliasEntry struct { - alias string - fork bool + alias string + displayName string + fork bool } forward := make(map[string][]aliasEntry, len(aliases)) @@ -2774,7 +2775,11 @@ func applyOAuthModelAliasEntries(aliases []config.OAuthModelAlias, models []*Mod continue } key := strings.ToLower(name) - forward[key] = append(forward[key], aliasEntry{alias: alias, fork: aliases[i].Fork}) + forward[key] = append(forward[key], aliasEntry{ + alias: alias, + displayName: strings.TrimSpace(aliases[i].DisplayName), + fork: aliases[i].Fork, + }) } if len(forward) == 0 { return models @@ -2831,6 +2836,9 @@ func applyOAuthModelAliasEntries(aliases []config.OAuthModelAlias, models []*Mod seen[aliasKey] = struct{}{} clone := *model clone.ID = mappedID + if entry.displayName != "" { + clone.DisplayName = entry.displayName + } if clone.Name != "" { clone.Name = rewriteModelInfoName(clone.Name, id, mappedID) } diff --git a/sdk/cliproxy/service_oauth_model_alias_test.go b/sdk/cliproxy/service_oauth_model_alias_test.go index df77cfa4..784f34c9 100644 --- a/sdk/cliproxy/service_oauth_model_alias_test.go +++ b/sdk/cliproxy/service_oauth_model_alias_test.go @@ -10,12 +10,12 @@ func TestApplyOAuthModelAlias_Rename(t *testing.T) { cfg := &config.Config{ OAuthModelAlias: map[string][]config.OAuthModelAlias{ "codex": { - {Name: "gpt-5", Alias: "g5"}, + {Name: "gpt-5", Alias: "g5", DisplayName: "Configured GPT Five"}, }, }, } models := []*ModelInfo{ - {ID: "gpt-5", Name: "models/gpt-5"}, + {ID: "gpt-5", Name: "models/gpt-5", DisplayName: "Upstream GPT Five"}, } out := applyOAuthModelAlias(cfg, "codex", "oauth", models) @@ -28,18 +28,21 @@ func TestApplyOAuthModelAlias_Rename(t *testing.T) { if out[0].Name != "models/g5" { t.Fatalf("expected model name %q, got %q", "models/g5", out[0].Name) } + if out[0].DisplayName != "Configured GPT Five" { + t.Fatalf("expected display name %q, got %q", "Configured GPT Five", out[0].DisplayName) + } } func TestApplyOAuthModelAlias_ForkAddsAlias(t *testing.T) { cfg := &config.Config{ OAuthModelAlias: map[string][]config.OAuthModelAlias{ "codex": { - {Name: "gpt-5", Alias: "g5", Fork: true}, + {Name: "gpt-5", Alias: "g5", Fork: true, DisplayName: "Configured GPT Five"}, }, }, } models := []*ModelInfo{ - {ID: "gpt-5", Name: "models/gpt-5"}, + {ID: "gpt-5", Name: "models/gpt-5", DisplayName: "Upstream GPT Five"}, } out := applyOAuthModelAlias(cfg, "codex", "oauth", models) @@ -55,6 +58,33 @@ func TestApplyOAuthModelAlias_ForkAddsAlias(t *testing.T) { if out[1].Name != "models/g5" { t.Fatalf("expected forked model name %q, got %q", "models/g5", out[1].Name) } + if out[0].DisplayName != "Upstream GPT Five" { + t.Fatalf("expected original display name %q, got %q", "Upstream GPT Five", out[0].DisplayName) + } + if out[1].DisplayName != "Configured GPT Five" { + t.Fatalf("expected alias display name %q, got %q", "Configured GPT Five", out[1].DisplayName) + } +} + +func TestApplyOAuthModelAlias_PreservesUpstreamDisplayNameByDefault(t *testing.T) { + cfg := &config.Config{ + OAuthModelAlias: map[string][]config.OAuthModelAlias{ + "codex": { + {Name: "gpt-5", Alias: "g5"}, + }, + }, + } + models := []*ModelInfo{ + {ID: "gpt-5", DisplayName: "Upstream GPT Five"}, + } + + out := applyOAuthModelAlias(cfg, "codex", "oauth", models) + if len(out) != 1 { + t.Fatalf("expected 1 model, got %d", len(out)) + } + if out[0].DisplayName != "Upstream GPT Five" { + t.Fatalf("expected upstream display name %q, got %q", "Upstream GPT Five", out[0].DisplayName) + } } func TestApplyOAuthModelAlias_ForkAddsMultipleAliases(t *testing.T) { @@ -138,7 +168,7 @@ func TestApplyOAuthModelAlias_PerAuthAlias(t *testing.T) { {ID: "gpt-5.3-codex-spark", Name: "models/gpt-5.3-codex-spark"}, } attributes := map[string]string{ - "model_aliases": `[{"name":"gpt-5.3-codex-spark","alias":"gpt-5.5"}]`, + "model_aliases": `[{"name":"gpt-5.3-codex-spark","alias":"gpt-5.5","display-name":"Configured GPT Five"}]`, } out := applyOAuthModelAliasForAuth(nil, "codex", "oauth", attributes, models) @@ -151,4 +181,7 @@ func TestApplyOAuthModelAlias_PerAuthAlias(t *testing.T) { if out[0].Name != "models/gpt-5.5" { t.Fatalf("expected per-auth alias name %q, got %q", "models/gpt-5.5", out[0].Name) } + if out[0].DisplayName != "Configured GPT Five" { + t.Fatalf("expected per-auth display name %q, got %q", "Configured GPT Five", out[0].DisplayName) + } }