refactor(config): rename model-name-mappings to oauth-model-mappings

This commit is contained in:
hkfires
2025-12-30 10:14:27 +08:00
parent f6ab6d97b9
commit 7be3f1c36c
7 changed files with 85 additions and 102 deletions

View File

@@ -156,9 +156,9 @@ ws-auth: false
# headers: # headers:
# X-Custom-Header: "custom-value" # X-Custom-Header: "custom-value"
# models: # optional: map aliases to upstream model names # models: # optional: map aliases to upstream model names
# - name: "gemini-2.0-flash" # upstream model name # - name: "gemini-2.5-flash" # upstream model name
# alias: "vertex-flash" # client-visible alias # alias: "vertex-flash" # client-visible alias
# - name: "gemini-1.5-pro" # - name: "gemini-2.5-pro"
# alias: "vertex-pro" # alias: "vertex-pro"
# Amp Integration # Amp Integration
@@ -188,38 +188,42 @@ ws-auth: false
# # Useful when Amp CLI requests models you don't have access to (e.g., Claude Opus 4.5) # # Useful when Amp CLI requests models you don't have access to (e.g., Claude Opus 4.5)
# # but you have a similar model available (e.g., Claude Sonnet 4). # # but you have a similar model available (e.g., Claude Sonnet 4).
# model-mappings: # model-mappings:
# - from: "claude-opus-4.5" # Model requested by Amp CLI # - from: "claude-opus-4-5-20251101" # Model requested by Amp CLI
# to: "claude-sonnet-4" # Route to this available model instead # to: "gemini-claude-opus-4-5-thinking" # Route to this available model instead
# - from: "gpt-5" # - from: "claude-sonnet-4-5-20250929"
# to: "gemini-2.5-pro" # to: "gemini-claude-sonnet-4-5-thinking"
# - from: "claude-3-opus-20240229" # - from: "claude-haiku-4-5-20251001"
# to: "claude-3-5-sonnet-20241022" # to: "gemini-2.5-flash"
# Global model name mappings (per channel) # Global OAuth model name mappings (per channel)
# These mappings rename model IDs for both model listing and request routing. # These mappings rename model IDs for both model listing and request routing.
# NOTE: Mappings do not apply to codex-api-key, claude-api-key, openai-compatibility, vertex-api-key, or ampcode. # Supported channels: gemini-cli, vertex, aistudio, antigravity, claude, codex, qwen, iflow.
# model-name-mappings: # NOTE: Mappings do not apply to gemini-api-key, codex-api-key, claude-api-key, openai-compatibility, vertex-api-key, or ampcode.
# gemini: # oauth-model-mappings:
# gemini-cli:
# - from: "gemini-2.5-pro" # original model name under this channel # - from: "gemini-2.5-pro" # original model name under this channel
# to: "gpt-5" # client-visible alias # to: "gpt-5" # client-visible alias
# apikey-gemini:
# - from: "gemini-2.5-pro"
# to: "gpt-5"
# claude:
# - from: "claude-sonnet-4"
# to: "gpt-4o"
# vertex: # vertex:
# - from: "gemini-2.5-pro" # - from: "gemini-2.5-pro"
# to: "gpt-5" # to: "gpt-5"
# qwen: # aistudio:
# - from: "qwen3-coder-plus" # - from: "gemini-2.5-pro"
# to: "gpt-4o-mini" # to: "gpt-5"
# iflow:
# - from: "glm-4.7"
# to: "gpt-5.1-mini"
# antigravity: # antigravity:
# - from: "gemini-3-pro-preview" # - from: "gemini-3-pro-preview"
# to: "gpt-5" # to: "gpt-5"
# claude:
# - from: "claude-sonnet-4-5-20250929"
# to: "gpt-5"
# codex:
# - from: "gpt-5"
# to: "gemini-2.5-pro"
# qwen:
# - from: "qwen3-coder-plus"
# to: "gpt-5"
# iflow:
# - from: "glm-4.7"
# to: "gpt-5"
# OAuth provider excluded models # OAuth provider excluded models
# oauth-excluded-models: # oauth-excluded-models:

View File

@@ -91,12 +91,13 @@ type Config struct {
// OAuthExcludedModels defines per-provider global model exclusions applied to OAuth/file-backed auth entries. // 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"` OAuthExcludedModels map[string][]string `yaml:"oauth-excluded-models,omitempty" json:"oauth-excluded-models,omitempty"`
// ModelNameMappings defines global per-channel model name mappings. // OAuthModelMappings defines global model name mappings for OAuth/file-backed auth channels.
// These mappings affect both model listing and model routing for supported channels. // These mappings affect both model listing and model routing for supported channels:
// gemini-cli, vertex, aistudio, antigravity, claude, codex, qwen, iflow.
// //
// NOTE: This does not apply to existing per-credential model alias features under: // 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. // gemini-api-key, 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"` OAuthModelMappings map[string][]ModelNameMapping `yaml:"oauth-model-mappings,omitempty" json:"oauth-model-mappings,omitempty"`
// Payload defines default and override rules for provider payload parameters. // Payload defines default and override rules for provider payload parameters.
Payload PayloadConfig `yaml:"payload" json:"payload"` Payload PayloadConfig `yaml:"payload" json:"payload"`
@@ -475,8 +476,8 @@ func LoadConfigOptional(configFile string, optional bool) (*Config, error) {
// Normalize OAuth provider model exclusion map. // Normalize OAuth provider model exclusion map.
cfg.OAuthExcludedModels = NormalizeOAuthExcludedModels(cfg.OAuthExcludedModels) cfg.OAuthExcludedModels = NormalizeOAuthExcludedModels(cfg.OAuthExcludedModels)
// Normalize global model name mappings. // Normalize global OAuth model name mappings.
cfg.SanitizeModelNameMappings() cfg.SanitizeOAuthModelMappings()
if cfg.legacyMigrationPending { if cfg.legacyMigrationPending {
fmt.Println("Detected legacy configuration keys, attempting to persist the normalized config...") fmt.Println("Detected legacy configuration keys, attempting to persist the normalized config...")
@@ -494,15 +495,15 @@ func LoadConfigOptional(configFile string, optional bool) (*Config, error) {
return &cfg, nil return &cfg, nil
} }
// SanitizeModelNameMappings normalizes and deduplicates global 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. // and ensures (From, To) pairs are unique within each channel.
func (cfg *Config) SanitizeModelNameMappings() { func (cfg *Config) SanitizeOAuthModelMappings() {
if cfg == nil || len(cfg.ModelNameMappings) == 0 { if cfg == nil || len(cfg.OAuthModelMappings) == 0 {
return return
} }
out := make(map[string][]ModelNameMapping, len(cfg.ModelNameMappings)) out := make(map[string][]ModelNameMapping, len(cfg.OAuthModelMappings))
for rawChannel, mappings := range cfg.ModelNameMappings { for rawChannel, mappings := range cfg.OAuthModelMappings {
channel := strings.ToLower(strings.TrimSpace(rawChannel)) channel := strings.ToLower(strings.TrimSpace(rawChannel))
if channel == "" || len(mappings) == 0 { if channel == "" || len(mappings) == 0 {
continue continue
@@ -535,7 +536,7 @@ func (cfg *Config) SanitizeModelNameMappings() {
out[channel] = clean out[channel] = clean
} }
} }
cfg.ModelNameMappings = out cfg.OAuthModelMappings = out
} }
// SanitizeOpenAICompatibility removes OpenAI-compatibility provider entries that are // SanitizeOpenAICompatibility removes OpenAI-compatibility provider entries that are

View File

@@ -127,7 +127,7 @@ func (w *Watcher) reloadConfig() bool {
} }
authDirChanged := oldConfig == nil || oldConfig.AuthDir != newConfig.AuthDir authDirChanged := oldConfig == nil || oldConfig.AuthDir != newConfig.AuthDir
forceAuthRefresh := oldConfig != nil && (oldConfig.ForceModelPrefix != newConfig.ForceModelPrefix || !reflect.DeepEqual(oldConfig.ModelNameMappings, newConfig.ModelNameMappings)) forceAuthRefresh := oldConfig != nil && (oldConfig.ForceModelPrefix != newConfig.ForceModelPrefix || !reflect.DeepEqual(oldConfig.OAuthModelMappings, newConfig.OAuthModelMappings))
log.Infof("config successfully reloaded, triggering client reload") log.Infof("config successfully reloaded, triggering client reload")
w.reloadClients(authDirChanged, affectedOAuthProviders, forceAuthRefresh) w.reloadClients(authDirChanged, affectedOAuthProviders, forceAuthRefresh)

View File

@@ -413,7 +413,7 @@ func (m *Manager) executeWithProvider(ctx context.Context, provider string, req
} }
execReq := req execReq := req
execReq.Model, execReq.Metadata = rewriteModelForAuth(routeModel, req.Metadata, auth) execReq.Model, execReq.Metadata = rewriteModelForAuth(routeModel, req.Metadata, auth)
execReq.Metadata = m.applyGlobalModelNameMappingMetadata(auth, execReq.Model, execReq.Metadata) execReq.Metadata = m.applyOAuthModelMappingMetadata(auth, execReq.Model, execReq.Metadata)
resp, errExec := executor.Execute(execCtx, auth, execReq, opts) resp, errExec := executor.Execute(execCtx, auth, execReq, opts)
result := Result{AuthID: auth.ID, Provider: provider, Model: routeModel, Success: errExec == nil} result := Result{AuthID: auth.ID, Provider: provider, Model: routeModel, Success: errExec == nil}
if errExec != nil { if errExec != nil {
@@ -475,7 +475,7 @@ func (m *Manager) executeCountWithProvider(ctx context.Context, provider string,
} }
execReq := req execReq := req
execReq.Model, execReq.Metadata = rewriteModelForAuth(routeModel, req.Metadata, auth) execReq.Model, execReq.Metadata = rewriteModelForAuth(routeModel, req.Metadata, auth)
execReq.Metadata = m.applyGlobalModelNameMappingMetadata(auth, execReq.Model, execReq.Metadata) execReq.Metadata = m.applyOAuthModelMappingMetadata(auth, execReq.Model, execReq.Metadata)
resp, errExec := executor.CountTokens(execCtx, auth, execReq, opts) resp, errExec := executor.CountTokens(execCtx, auth, execReq, opts)
result := Result{AuthID: auth.ID, Provider: provider, Model: routeModel, Success: errExec == nil} result := Result{AuthID: auth.ID, Provider: provider, Model: routeModel, Success: errExec == nil}
if errExec != nil { if errExec != nil {
@@ -537,7 +537,7 @@ func (m *Manager) executeStreamWithProvider(ctx context.Context, provider string
} }
execReq := req execReq := req
execReq.Model, execReq.Metadata = rewriteModelForAuth(routeModel, req.Metadata, auth) execReq.Model, execReq.Metadata = rewriteModelForAuth(routeModel, req.Metadata, auth)
execReq.Metadata = m.applyGlobalModelNameMappingMetadata(auth, execReq.Model, execReq.Metadata) execReq.Metadata = m.applyOAuthModelMappingMetadata(auth, execReq.Model, execReq.Metadata)
chunks, errStream := executor.ExecuteStream(execCtx, auth, execReq, opts) chunks, errStream := executor.ExecuteStream(execCtx, auth, execReq, opts)
if errStream != nil { if errStream != nil {
rerr := &Error{Message: errStream.Error()} rerr := &Error{Message: errStream.Error()}

View File

@@ -50,10 +50,10 @@ func compileModelNameMappingTable(mappings map[string][]internalconfig.ModelName
return out return out
} }
// SetGlobalModelNameMappings updates the global model name mapping table used during execution. // SetOAuthModelMappings updates the OAuth model name mapping table used during execution.
// The mapping is applied per-auth channel to resolve the upstream model name while keeping the // The mapping is applied per-auth channel to resolve the upstream model name while keeping the
// client-visible model name unchanged for translation/response formatting. // client-visible model name unchanged for translation/response formatting.
func (m *Manager) SetGlobalModelNameMappings(mappings map[string][]internalconfig.ModelNameMapping) { func (m *Manager) SetOAuthModelMappings(mappings map[string][]internalconfig.ModelNameMapping) {
if m == nil { if m == nil {
return return
} }
@@ -65,8 +65,8 @@ func (m *Manager) SetGlobalModelNameMappings(mappings map[string][]internalconfi
m.modelNameMappings.Store(table) m.modelNameMappings.Store(table)
} }
func (m *Manager) applyGlobalModelNameMappingMetadata(auth *Auth, requestedModel string, metadata map[string]any) map[string]any { func (m *Manager) applyOAuthModelMappingMetadata(auth *Auth, requestedModel string, metadata map[string]any) map[string]any {
original := m.resolveGlobalUpstreamModelForAuth(auth, requestedModel) original := m.resolveOAuthUpstreamModel(auth, requestedModel)
if original == "" { if original == "" {
return metadata return metadata
} }
@@ -88,11 +88,11 @@ func (m *Manager) applyGlobalModelNameMappingMetadata(auth *Auth, requestedModel
return out return out
} }
func (m *Manager) resolveGlobalUpstreamModelForAuth(auth *Auth, requestedModel string) string { func (m *Manager) resolveOAuthUpstreamModel(auth *Auth, requestedModel string) string {
if m == nil || auth == nil { if m == nil || auth == nil {
return "" return ""
} }
channel := globalModelMappingChannelForAuth(auth) channel := modelMappingChannel(auth)
if channel == "" { if channel == "" {
return "" return ""
} }
@@ -116,7 +116,10 @@ func (m *Manager) resolveGlobalUpstreamModelForAuth(auth *Auth, requestedModel s
return original return original
} }
func globalModelMappingChannelForAuth(auth *Auth) string { // modelMappingChannel extracts the OAuth model mapping channel from an Auth object.
// It determines the provider and auth kind from the Auth's attributes and delegates
// to OAuthModelMappingChannel for the actual channel resolution.
func modelMappingChannel(auth *Auth) string {
if auth == nil { if auth == nil {
return "" return ""
} }
@@ -130,32 +133,38 @@ func globalModelMappingChannelForAuth(auth *Auth) string {
authKind = "apikey" authKind = "apikey"
} }
} }
return globalModelMappingChannel(provider, authKind) return OAuthModelMappingChannel(provider, authKind)
} }
func globalModelMappingChannel(provider, authKind string) string { // OAuthModelMappingChannel returns the OAuth model mapping channel name for a given provider
// and auth kind. Returns empty string if the provider/authKind combination doesn't support
// OAuth model mappings (e.g., API key authentication).
//
// Supported channels: gemini-cli, vertex, aistudio, antigravity, claude, codex, qwen, iflow.
func OAuthModelMappingChannel(provider, authKind string) string {
provider = strings.ToLower(strings.TrimSpace(provider))
authKind = strings.ToLower(strings.TrimSpace(authKind))
switch provider { switch provider {
case "gemini": case "gemini":
if authKind == "apikey" { // gemini provider uses gemini-api-key config, not oauth-model-mappings.
return "apikey-gemini" // OAuth-based gemini auth is converted to "gemini-cli" by the synthesizer.
} return ""
return "gemini"
case "codex":
if authKind == "apikey" {
return ""
}
return "codex"
case "claude":
if authKind == "apikey" {
return ""
}
return "claude"
case "vertex": case "vertex":
if authKind == "apikey" { if authKind == "apikey" {
return "" return ""
} }
return "vertex" return "vertex"
case "antigravity", "qwen", "iflow": case "claude":
if authKind == "apikey" {
return ""
}
return "claude"
case "codex":
if authKind == "apikey" {
return ""
}
return "codex"
case "gemini-cli", "aistudio", "antigravity", "qwen", "iflow":
return provider return provider
default: default:
return "" return ""

View File

@@ -215,7 +215,7 @@ func (b *Builder) Build() (*Service, error) {
} }
// Attach a default RoundTripper provider so providers can opt-in per-auth transports. // Attach a default RoundTripper provider so providers can opt-in per-auth transports.
coreManager.SetRoundTripperProvider(newDefaultRoundTripperProvider()) coreManager.SetRoundTripperProvider(newDefaultRoundTripperProvider())
coreManager.SetGlobalModelNameMappings(b.cfg.ModelNameMappings) coreManager.SetOAuthModelMappings(b.cfg.OAuthModelMappings)
service := &Service{ service := &Service{
cfg: b.cfg, cfg: b.cfg,

View File

@@ -553,7 +553,7 @@ func (s *Service) Run(ctx context.Context) error {
s.cfg = newCfg s.cfg = newCfg
s.cfgMu.Unlock() s.cfgMu.Unlock()
if s.coreManager != nil { if s.coreManager != nil {
s.coreManager.SetGlobalModelNameMappings(newCfg.ModelNameMappings) s.coreManager.SetOAuthModelMappings(newCfg.OAuthModelMappings)
} }
s.rebindExecutors() s.rebindExecutors()
} }
@@ -844,7 +844,7 @@ func (s *Service) registerModelsForAuth(a *coreauth.Auth) {
} }
} }
} }
models = applyGlobalModelNameMappings(s.cfg, provider, authKind, models) models = applyOAuthModelMappings(s.cfg, provider, authKind, models)
if len(models) > 0 { if len(models) > 0 {
key := provider key := provider
if key == "" { if key == "" {
@@ -1154,37 +1154,6 @@ func buildVertexCompatConfigModels(entry *config.VertexCompatKey) []*ModelInfo {
return out return out
} }
func globalModelMappingChannel(provider, authKind string) string {
provider = strings.ToLower(strings.TrimSpace(provider))
authKind = strings.ToLower(strings.TrimSpace(authKind))
switch provider {
case "gemini":
if authKind == "apikey" {
return "apikey-gemini"
}
return "gemini"
case "codex":
if authKind == "apikey" {
return ""
}
return "codex"
case "claude":
if authKind == "apikey" {
return ""
}
return "claude"
case "vertex":
if authKind == "apikey" {
return ""
}
return "vertex"
case "antigravity", "qwen", "iflow":
return provider
default:
return ""
}
}
func rewriteModelInfoName(name, oldID, newID string) string { func rewriteModelInfoName(name, oldID, newID string) string {
trimmed := strings.TrimSpace(name) trimmed := strings.TrimSpace(name)
if trimmed == "" { if trimmed == "" {
@@ -1208,15 +1177,15 @@ func rewriteModelInfoName(name, oldID, newID string) string {
return name return name
} }
func applyGlobalModelNameMappings(cfg *config.Config, provider, authKind string, models []*ModelInfo) []*ModelInfo { func applyOAuthModelMappings(cfg *config.Config, provider, authKind string, models []*ModelInfo) []*ModelInfo {
if cfg == nil || len(models) == 0 { if cfg == nil || len(models) == 0 {
return models return models
} }
channel := globalModelMappingChannel(provider, authKind) channel := coreauth.OAuthModelMappingChannel(provider, authKind)
if channel == "" || len(cfg.ModelNameMappings) == 0 { if channel == "" || len(cfg.OAuthModelMappings) == 0 {
return models return models
} }
mappings := cfg.ModelNameMappings[channel] mappings := cfg.OAuthModelMappings[channel]
if len(mappings) == 0 { if len(mappings) == 0 {
return models return models
} }