mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-02-03 13:00:52 +08:00
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.
This commit is contained in:
@@ -206,6 +206,7 @@ ws-auth: false
|
|||||||
# 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
|
||||||
# alias: "g2.5p" # client-visible alias
|
# alias: "g2.5p" # client-visible alias
|
||||||
|
# fork: true # when true, keep original and also add the alias as an extra model (default: false)
|
||||||
# vertex:
|
# vertex:
|
||||||
# - name: "gemini-2.5-pro"
|
# - name: "gemini-2.5-pro"
|
||||||
# alias: "g2.5p"
|
# alias: "g2.5p"
|
||||||
|
|||||||
@@ -145,11 +145,14 @@ type RoutingConfig struct {
|
|||||||
Strategy string `yaml:"strategy,omitempty" json:"strategy,omitempty"`
|
Strategy string `yaml:"strategy,omitempty" json:"strategy,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ModelNameMapping defines a model ID rename mapping for a specific channel.
|
// ModelNameMapping defines a model ID mapping for a specific channel.
|
||||||
// It maps the original model name (Name) to the client-visible alias (Alias).
|
// It maps the upstream model name (Name) to the client-visible alias (Alias).
|
||||||
|
// When Fork is true, the alias is added as an additional model in listings while
|
||||||
|
// keeping the original model ID available.
|
||||||
type ModelNameMapping struct {
|
type ModelNameMapping struct {
|
||||||
Name string `yaml:"name" json:"name"`
|
Name string `yaml:"name" json:"name"`
|
||||||
Alias string `yaml:"alias" json:"alias"`
|
Alias string `yaml:"alias" json:"alias"`
|
||||||
|
Fork bool `yaml:"fork,omitempty" json:"fork,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// AmpModelMapping defines a model name mapping for Amp CLI requests.
|
// AmpModelMapping defines a model name mapping for Amp CLI requests.
|
||||||
@@ -551,7 +554,7 @@ func (cfg *Config) SanitizeOAuthModelMappings() {
|
|||||||
}
|
}
|
||||||
seenName[nameKey] = struct{}{}
|
seenName[nameKey] = struct{}{}
|
||||||
seenAlias[aliasKey] = struct{}{}
|
seenAlias[aliasKey] = struct{}{}
|
||||||
clean = append(clean, ModelNameMapping{Name: name, Alias: alias})
|
clean = append(clean, ModelNameMapping{Name: name, Alias: alias, Fork: mapping.Fork})
|
||||||
}
|
}
|
||||||
if len(clean) > 0 {
|
if len(clean) > 0 {
|
||||||
out[channel] = clean
|
out[channel] = clean
|
||||||
|
|||||||
27
internal/config/oauth_model_mappings_test.go
Normal file
27
internal/config/oauth_model_mappings_test.go
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestSanitizeOAuthModelMappings_PreservesForkFlag(t *testing.T) {
|
||||||
|
cfg := &Config{
|
||||||
|
OAuthModelMappings: map[string][]ModelNameMapping{
|
||||||
|
" CoDeX ": {
|
||||||
|
{Name: " gpt-5 ", Alias: " g5 ", Fork: true},
|
||||||
|
{Name: "gpt-6", Alias: "g6"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.SanitizeOAuthModelMappings()
|
||||||
|
|
||||||
|
mappings := cfg.OAuthModelMappings["codex"]
|
||||||
|
if len(mappings) != 2 {
|
||||||
|
t.Fatalf("expected 2 sanitized mappings, got %d", len(mappings))
|
||||||
|
}
|
||||||
|
if mappings[0].Name != "gpt-5" || mappings[0].Alias != "g5" || !mappings[0].Fork {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
if mappings[1].Name != "gpt-6" || mappings[1].Alias != "g6" || 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -80,6 +80,9 @@ func summarizeOAuthModelMappingList(list []config.ModelNameMapping) OAuthModelMa
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
key := name + "->" + alias
|
key := name + "->" + alias
|
||||||
|
if mapping.Fork {
|
||||||
|
key += "|fork"
|
||||||
|
}
|
||||||
if _, exists := seen[key]; exists {
|
if _, exists := seen[key]; exists {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1231,7 +1231,13 @@ func applyOAuthModelMappings(cfg *config.Config, provider, authKind string, mode
|
|||||||
if len(mappings) == 0 {
|
if len(mappings) == 0 {
|
||||||
return models
|
return models
|
||||||
}
|
}
|
||||||
forward := make(map[string]string, len(mappings))
|
|
||||||
|
type mappingEntry struct {
|
||||||
|
alias string
|
||||||
|
fork bool
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
@@ -1245,7 +1251,7 @@ func applyOAuthModelMappings(cfg *config.Config, provider, authKind string, mode
|
|||||||
if _, exists := forward[key]; exists {
|
if _, exists := forward[key]; exists {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
forward[key] = alias
|
forward[key] = mappingEntry{alias: alias, fork: mappings[i].Fork}
|
||||||
}
|
}
|
||||||
if len(forward) == 0 {
|
if len(forward) == 0 {
|
||||||
return models
|
return models
|
||||||
@@ -1260,10 +1266,45 @@ func applyOAuthModelMappings(cfg *config.Config, provider, authKind string, mode
|
|||||||
if id == "" {
|
if id == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
mappedID := id
|
key := strings.ToLower(id)
|
||||||
if to, ok := forward[strings.ToLower(id)]; ok && strings.TrimSpace(to) != "" {
|
entry, ok := forward[key]
|
||||||
mappedID = strings.TrimSpace(to)
|
if !ok {
|
||||||
|
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 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[key] = struct{}{}
|
||||||
|
out = append(out, model)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if entry.fork {
|
||||||
|
if _, exists := seen[key]; !exists {
|
||||||
|
seen[key] = struct{}{}
|
||||||
|
out = append(out, model)
|
||||||
|
}
|
||||||
|
aliasKey := strings.ToLower(mappedID)
|
||||||
|
if _, exists := seen[aliasKey]; exists {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[aliasKey] = struct{}{}
|
||||||
|
clone := *model
|
||||||
|
clone.ID = mappedID
|
||||||
|
if clone.Name != "" {
|
||||||
|
clone.Name = rewriteModelInfoName(clone.Name, id, mappedID)
|
||||||
|
}
|
||||||
|
out = append(out, &clone)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
uniqueKey := strings.ToLower(mappedID)
|
uniqueKey := strings.ToLower(mappedID)
|
||||||
if _, exists := seen[uniqueKey]; exists {
|
if _, exists := seen[uniqueKey]; exists {
|
||||||
continue
|
continue
|
||||||
|
|||||||
58
sdk/cliproxy/service_oauth_model_mappings_test.go
Normal file
58
sdk/cliproxy/service_oauth_model_mappings_test.go
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
package cliproxy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/router-for-me/CLIProxyAPI/v6/sdk/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestApplyOAuthModelMappings_Rename(t *testing.T) {
|
||||||
|
cfg := &config.Config{
|
||||||
|
OAuthModelMappings: map[string][]config.ModelNameMapping{
|
||||||
|
"codex": {
|
||||||
|
{Name: "gpt-5", Alias: "g5"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
models := []*ModelInfo{
|
||||||
|
{ID: "gpt-5", Name: "models/gpt-5"},
|
||||||
|
}
|
||||||
|
|
||||||
|
out := applyOAuthModelMappings(cfg, "codex", "oauth", models)
|
||||||
|
if len(out) != 1 {
|
||||||
|
t.Fatalf("expected 1 model, got %d", len(out))
|
||||||
|
}
|
||||||
|
if out[0].ID != "g5" {
|
||||||
|
t.Fatalf("expected model id %q, got %q", "g5", out[0].ID)
|
||||||
|
}
|
||||||
|
if out[0].Name != "models/g5" {
|
||||||
|
t.Fatalf("expected model name %q, got %q", "models/g5", out[0].Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestApplyOAuthModelMappings_ForkAddsAlias(t *testing.T) {
|
||||||
|
cfg := &config.Config{
|
||||||
|
OAuthModelMappings: map[string][]config.ModelNameMapping{
|
||||||
|
"codex": {
|
||||||
|
{Name: "gpt-5", Alias: "g5", Fork: true},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
models := []*ModelInfo{
|
||||||
|
{ID: "gpt-5", Name: "models/gpt-5"},
|
||||||
|
}
|
||||||
|
|
||||||
|
out := applyOAuthModelMappings(cfg, "codex", "oauth", models)
|
||||||
|
if len(out) != 2 {
|
||||||
|
t.Fatalf("expected 2 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user