feat(cliproxy): introduce global model name mappings for improved aliasing and routing

This commit is contained in:
Luis Pater
2025-12-30 08:13:06 +08:00
parent a8cb01819d
commit 50e6d845f4
10 changed files with 431 additions and 15 deletions
+61
View File
@@ -91,6 +91,13 @@ type Config struct {
// OAuthExcludedModels defines per-provider global model exclusions applied to OAuth/file-backed auth entries.
OAuthExcludedModels map[string][]string `yaml:"oauth-excluded-models,omitempty" json:"oauth-excluded-models,omitempty"`
// ModelNameMappings defines global per-channel model name mappings.
// These mappings affect both model listing and model routing for supported channels.
//
// NOTE: This does not apply to existing per-credential model alias features under:
// codex-api-key, claude-api-key, openai-compatibility, vertex-api-key, and ampcode.
ModelNameMappings map[string][]ModelNameMapping `yaml:"model-name-mappings,omitempty" json:"model-name-mappings,omitempty"`
// Payload defines default and override rules for provider payload parameters.
Payload PayloadConfig `yaml:"payload" json:"payload"`
@@ -137,6 +144,13 @@ type RoutingConfig struct {
Strategy string `yaml:"strategy,omitempty" json:"strategy,omitempty"`
}
// ModelNameMapping defines a model ID rename mapping for a specific channel.
// It maps the original model name (From) to the client-visible alias (To).
type ModelNameMapping struct {
From string `yaml:"from" json:"from"`
To string `yaml:"to" json:"to"`
}
// AmpModelMapping defines a model name mapping for Amp CLI requests.
// When Amp requests a model that isn't available locally, this mapping
// allows routing to an alternative model that IS available.
@@ -461,6 +475,9 @@ func LoadConfigOptional(configFile string, optional bool) (*Config, error) {
// Normalize OAuth provider model exclusion map.
cfg.OAuthExcludedModels = NormalizeOAuthExcludedModels(cfg.OAuthExcludedModels)
// Normalize global model name mappings.
cfg.SanitizeModelNameMappings()
if cfg.legacyMigrationPending {
fmt.Println("Detected legacy configuration keys, attempting to persist the normalized config...")
if !optional && configFile != "" {
@@ -477,6 +494,50 @@ func LoadConfigOptional(configFile string, optional bool) (*Config, error) {
return &cfg, nil
}
// SanitizeModelNameMappings normalizes and deduplicates global model name mappings.
// It trims whitespace, normalizes channel keys to lower-case, drops empty entries,
// and ensures (From, To) pairs are unique within each channel.
func (cfg *Config) SanitizeModelNameMappings() {
if cfg == nil || len(cfg.ModelNameMappings) == 0 {
return
}
out := make(map[string][]ModelNameMapping, len(cfg.ModelNameMappings))
for rawChannel, mappings := range cfg.ModelNameMappings {
channel := strings.ToLower(strings.TrimSpace(rawChannel))
if channel == "" || len(mappings) == 0 {
continue
}
seenFrom := make(map[string]struct{}, len(mappings))
seenTo := make(map[string]struct{}, len(mappings))
clean := make([]ModelNameMapping, 0, len(mappings))
for _, mapping := range mappings {
from := strings.TrimSpace(mapping.From)
to := strings.TrimSpace(mapping.To)
if from == "" || to == "" {
continue
}
if strings.EqualFold(from, to) {
continue
}
fromKey := strings.ToLower(from)
toKey := strings.ToLower(to)
if _, ok := seenFrom[fromKey]; ok {
continue
}
if _, ok := seenTo[toKey]; ok {
continue
}
seenFrom[fromKey] = struct{}{}
seenTo[toKey] = struct{}{}
clean = append(clean, ModelNameMapping{From: from, To: to})
}
if len(clean) > 0 {
out[channel] = clean
}
}
cfg.ModelNameMappings = out
}
// SanitizeOpenAICompatibility removes OpenAI-compatibility provider entries that are
// not actionable, specifically those missing a BaseURL. It trims whitespace before
// evaluation and preserves the relative order of remaining entries.