mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-02-18 20:30:51 +08:00
Fixed: #936
feat(cliproxy): support multiple aliases for OAuth model mappings - Updated mapping logic to allow multiple aliases per upstream model name. - Adjusted `SanitizeOAuthModelMappings` to ensure aliases remain unique within channels. - Added test cases to validate multi-alias scenarios. - Updated example config to clarify multi-alias support.
This commit is contained in:
@@ -202,6 +202,7 @@ ws-auth: false
|
|||||||
# These mappings rename model IDs for both model listing and request routing.
|
# These mappings rename model IDs for both model listing and request routing.
|
||||||
# Supported channels: gemini-cli, vertex, aistudio, antigravity, claude, codex, qwen, iflow.
|
# Supported channels: gemini-cli, vertex, aistudio, antigravity, claude, codex, qwen, iflow.
|
||||||
# NOTE: Mappings do not apply to gemini-api-key, codex-api-key, claude-api-key, openai-compatibility, vertex-api-key, or ampcode.
|
# NOTE: Mappings do not apply to gemini-api-key, codex-api-key, claude-api-key, openai-compatibility, vertex-api-key, or ampcode.
|
||||||
|
# You can repeat the same name with different aliases to expose multiple client model names.
|
||||||
# oauth-model-mappings:
|
# oauth-model-mappings:
|
||||||
# gemini-cli:
|
# gemini-cli:
|
||||||
# - name: "gemini-2.5-pro" # original model name under this channel
|
# - name: "gemini-2.5-pro" # original model name under this channel
|
||||||
|
|||||||
@@ -521,7 +521,7 @@ func LoadConfigOptional(configFile string, optional bool) (*Config, error) {
|
|||||||
|
|
||||||
// SanitizeOAuthModelMappings normalizes and deduplicates global OAuth model name mappings.
|
// SanitizeOAuthModelMappings normalizes and deduplicates global OAuth model name mappings.
|
||||||
// It trims whitespace, normalizes channel keys to lower-case, drops empty entries,
|
// It trims whitespace, normalizes channel keys to lower-case, drops empty entries,
|
||||||
// and ensures (From, To) pairs are unique within each channel.
|
// allows multiple aliases per upstream name, and ensures aliases are unique within each channel.
|
||||||
func (cfg *Config) SanitizeOAuthModelMappings() {
|
func (cfg *Config) SanitizeOAuthModelMappings() {
|
||||||
if cfg == nil || len(cfg.OAuthModelMappings) == 0 {
|
if cfg == nil || len(cfg.OAuthModelMappings) == 0 {
|
||||||
return
|
return
|
||||||
@@ -532,7 +532,6 @@ func (cfg *Config) SanitizeOAuthModelMappings() {
|
|||||||
if channel == "" || len(mappings) == 0 {
|
if channel == "" || len(mappings) == 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
seenName := make(map[string]struct{}, len(mappings))
|
|
||||||
seenAlias := make(map[string]struct{}, len(mappings))
|
seenAlias := make(map[string]struct{}, len(mappings))
|
||||||
clean := make([]ModelNameMapping, 0, len(mappings))
|
clean := make([]ModelNameMapping, 0, len(mappings))
|
||||||
for _, mapping := range mappings {
|
for _, mapping := range mappings {
|
||||||
@@ -544,15 +543,10 @@ func (cfg *Config) SanitizeOAuthModelMappings() {
|
|||||||
if strings.EqualFold(name, alias) {
|
if strings.EqualFold(name, alias) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
nameKey := strings.ToLower(name)
|
|
||||||
aliasKey := strings.ToLower(alias)
|
aliasKey := strings.ToLower(alias)
|
||||||
if _, ok := seenName[nameKey]; ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if _, ok := seenAlias[aliasKey]; ok {
|
if _, ok := seenAlias[aliasKey]; ok {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
seenName[nameKey] = struct{}{}
|
|
||||||
seenAlias[aliasKey] = struct{}{}
|
seenAlias[aliasKey] = struct{}{}
|
||||||
clean = append(clean, ModelNameMapping{Name: name, Alias: alias, Fork: mapping.Fork})
|
clean = append(clean, ModelNameMapping{Name: name, Alias: alias, Fork: mapping.Fork})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,3 +25,32 @@ func TestSanitizeOAuthModelMappings_PreservesForkFlag(t *testing.T) {
|
|||||||
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)
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSanitizeOAuthModelMappings_AllowsMultipleAliasesForSameName(t *testing.T) {
|
||||||
|
cfg := &Config{
|
||||||
|
OAuthModelMappings: map[string][]ModelNameMapping{
|
||||||
|
"antigravity": {
|
||||||
|
{Name: "gemini-claude-opus-4-5-thinking", Alias: "claude-opus-4-5-20251101", Fork: true},
|
||||||
|
{Name: "gemini-claude-opus-4-5-thinking", Alias: "claude-opus-4-5-20251101-thinking", Fork: true},
|
||||||
|
{Name: "gemini-claude-opus-4-5-thinking", Alias: "claude-opus-4-5", Fork: true},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.SanitizeOAuthModelMappings()
|
||||||
|
|
||||||
|
mappings := cfg.OAuthModelMappings["antigravity"]
|
||||||
|
expected := []ModelNameMapping{
|
||||||
|
{Name: "gemini-claude-opus-4-5-thinking", Alias: "claude-opus-4-5-20251101", Fork: true},
|
||||||
|
{Name: "gemini-claude-opus-4-5-thinking", Alias: "claude-opus-4-5-20251101-thinking", Fork: true},
|
||||||
|
{Name: "gemini-claude-opus-4-5-thinking", Alias: "claude-opus-4-5", Fork: true},
|
||||||
|
}
|
||||||
|
if len(mappings) != len(expected) {
|
||||||
|
t.Fatalf("expected %d sanitized mappings, got %d", len(expected), len(mappings))
|
||||||
|
}
|
||||||
|
for i, exp := range expected {
|
||||||
|
if mappings[i].Name != exp.Name || mappings[i].Alias != exp.Alias || mappings[i].Fork != exp.Fork {
|
||||||
|
t.Fatalf("expected mapping %d to be name=%q alias=%q fork=%v, got name=%q alias=%q fork=%v", i, exp.Name, exp.Alias, exp.Fork, mappings[i].Name, mappings[i].Alias, mappings[i].Fork)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1237,7 +1237,7 @@ func applyOAuthModelMappings(cfg *config.Config, provider, authKind string, mode
|
|||||||
fork bool
|
fork bool
|
||||||
}
|
}
|
||||||
|
|
||||||
forward := make(map[string]mappingEntry, len(mappings))
|
forward := make(map[string][]mappingEntry, len(mappings))
|
||||||
for i := range mappings {
|
for i := range mappings {
|
||||||
name := strings.TrimSpace(mappings[i].Name)
|
name := strings.TrimSpace(mappings[i].Name)
|
||||||
alias := strings.TrimSpace(mappings[i].Alias)
|
alias := strings.TrimSpace(mappings[i].Alias)
|
||||||
@@ -1248,14 +1248,12 @@ func applyOAuthModelMappings(cfg *config.Config, provider, authKind string, mode
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
key := strings.ToLower(name)
|
key := strings.ToLower(name)
|
||||||
if _, exists := forward[key]; exists {
|
forward[key] = append(forward[key], mappingEntry{alias: alias, fork: mappings[i].Fork})
|
||||||
continue
|
|
||||||
}
|
|
||||||
forward[key] = mappingEntry{alias: alias, fork: mappings[i].Fork}
|
|
||||||
}
|
}
|
||||||
if len(forward) == 0 {
|
if len(forward) == 0 {
|
||||||
return models
|
return models
|
||||||
}
|
}
|
||||||
|
|
||||||
out := make([]*ModelInfo, 0, len(models))
|
out := make([]*ModelInfo, 0, len(models))
|
||||||
seen := make(map[string]struct{}, len(models))
|
seen := make(map[string]struct{}, len(models))
|
||||||
for _, model := range models {
|
for _, model := range models {
|
||||||
@@ -1267,17 +1265,8 @@ func applyOAuthModelMappings(cfg *config.Config, provider, authKind string, mode
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
key := strings.ToLower(id)
|
key := strings.ToLower(id)
|
||||||
entry, ok := forward[key]
|
entries := forward[key]
|
||||||
if !ok {
|
if len(entries) == 0 {
|
||||||
if _, exists := seen[key]; exists {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
seen[key] = struct{}{}
|
|
||||||
out = append(out, model)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
mappedID := strings.TrimSpace(entry.alias)
|
|
||||||
if mappedID == "" {
|
|
||||||
if _, exists := seen[key]; exists {
|
if _, exists := seen[key]; exists {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -1286,11 +1275,29 @@ func applyOAuthModelMappings(cfg *config.Config, provider, authKind string, mode
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
keepOriginal := false
|
||||||
|
for _, entry := range entries {
|
||||||
if entry.fork {
|
if entry.fork {
|
||||||
|
keepOriginal = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if keepOriginal {
|
||||||
if _, exists := seen[key]; !exists {
|
if _, exists := seen[key]; !exists {
|
||||||
seen[key] = struct{}{}
|
seen[key] = struct{}{}
|
||||||
out = append(out, model)
|
out = append(out, model)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addedAlias := false
|
||||||
|
for _, entry := range entries {
|
||||||
|
mappedID := strings.TrimSpace(entry.alias)
|
||||||
|
if mappedID == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strings.EqualFold(mappedID, id) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
aliasKey := strings.ToLower(mappedID)
|
aliasKey := strings.ToLower(mappedID)
|
||||||
if _, exists := seen[aliasKey]; exists {
|
if _, exists := seen[aliasKey]; exists {
|
||||||
continue
|
continue
|
||||||
@@ -1302,24 +1309,16 @@ func applyOAuthModelMappings(cfg *config.Config, provider, authKind string, mode
|
|||||||
clone.Name = rewriteModelInfoName(clone.Name, id, mappedID)
|
clone.Name = rewriteModelInfoName(clone.Name, id, mappedID)
|
||||||
}
|
}
|
||||||
out = append(out, &clone)
|
out = append(out, &clone)
|
||||||
continue
|
addedAlias = true
|
||||||
}
|
}
|
||||||
|
|
||||||
uniqueKey := strings.ToLower(mappedID)
|
if !keepOriginal && !addedAlias {
|
||||||
if _, exists := seen[uniqueKey]; exists {
|
if _, exists := seen[key]; exists {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
seen[uniqueKey] = struct{}{}
|
seen[key] = struct{}{}
|
||||||
if mappedID == id {
|
|
||||||
out = append(out, model)
|
out = append(out, model)
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
clone := *model
|
|
||||||
clone.ID = mappedID
|
|
||||||
if clone.Name != "" {
|
|
||||||
clone.Name = rewriteModelInfoName(clone.Name, id, mappedID)
|
|
||||||
}
|
|
||||||
out = append(out, &clone)
|
|
||||||
}
|
}
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,3 +56,37 @@ func TestApplyOAuthModelMappings_ForkAddsAlias(t *testing.T) {
|
|||||||
t.Fatalf("expected forked model name %q, got %q", "models/g5", out[1].Name)
|
t.Fatalf("expected forked model name %q, got %q", "models/g5", out[1].Name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestApplyOAuthModelMappings_ForkAddsMultipleAliases(t *testing.T) {
|
||||||
|
cfg := &config.Config{
|
||||||
|
OAuthModelMappings: map[string][]config.ModelNameMapping{
|
||||||
|
"codex": {
|
||||||
|
{Name: "gpt-5", Alias: "g5", Fork: true},
|
||||||
|
{Name: "gpt-5", Alias: "g5-2", Fork: true},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
models := []*ModelInfo{
|
||||||
|
{ID: "gpt-5", Name: "models/gpt-5"},
|
||||||
|
}
|
||||||
|
|
||||||
|
out := applyOAuthModelMappings(cfg, "codex", "oauth", models)
|
||||||
|
if len(out) != 3 {
|
||||||
|
t.Fatalf("expected 3 models, got %d", len(out))
|
||||||
|
}
|
||||||
|
if out[0].ID != "gpt-5" {
|
||||||
|
t.Fatalf("expected first model id %q, got %q", "gpt-5", out[0].ID)
|
||||||
|
}
|
||||||
|
if out[1].ID != "g5" {
|
||||||
|
t.Fatalf("expected second model id %q, got %q", "g5", out[1].ID)
|
||||||
|
}
|
||||||
|
if out[1].Name != "models/g5" {
|
||||||
|
t.Fatalf("expected forked model name %q, got %q", "models/g5", out[1].Name)
|
||||||
|
}
|
||||||
|
if out[2].ID != "g5-2" {
|
||||||
|
t.Fatalf("expected third model id %q, got %q", "g5-2", out[2].ID)
|
||||||
|
}
|
||||||
|
if out[2].Name != "models/g5-2" {
|
||||||
|
t.Fatalf("expected forked model name %q, got %q", "models/g5-2", out[2].Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user