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

This commit is contained in:
hkfires
2026-01-15 18:03:26 +08:00
parent e0ffec885c
commit fe5b3c80cb
19 changed files with 761 additions and 269 deletions

View File

@@ -201,12 +201,12 @@ nonstream-keepalive-interval: 0
# - from: "claude-haiku-4-5-20251001" # - from: "claude-haiku-4-5-20251001"
# to: "gemini-2.5-flash" # to: "gemini-2.5-flash"
# Global OAuth model name mappings (per channel) # Global OAuth model name aliases (per channel)
# These mappings rename model IDs for both model listing and request routing. # These aliases 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: Aliases 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. # You can repeat the same name with different aliases to expose multiple client model names.
oauth-model-mappings: oauth-model-alias:
antigravity: antigravity:
- name: "rev19-uic3-1p" - name: "rev19-uic3-1p"
alias: "gemini-2.5-computer-use-preview-10-2025" alias: "gemini-2.5-computer-use-preview-10-2025"

View File

@@ -703,21 +703,21 @@ func (h *Handler) DeleteOAuthExcludedModels(c *gin.Context) {
h.persist(c) h.persist(c)
} }
// oauth-model-mappings: map[string][]ModelNameMapping // oauth-model-alias: map[string][]OAuthModelAlias
func (h *Handler) GetOAuthModelMappings(c *gin.Context) { func (h *Handler) GetOAuthModelAlias(c *gin.Context) {
c.JSON(200, gin.H{"oauth-model-mappings": sanitizedOAuthModelMappings(h.cfg.OAuthModelMappings)}) c.JSON(200, gin.H{"oauth-model-alias": sanitizedOAuthModelAlias(h.cfg.OAuthModelAlias)})
} }
func (h *Handler) PutOAuthModelMappings(c *gin.Context) { func (h *Handler) PutOAuthModelAlias(c *gin.Context) {
data, err := c.GetRawData() data, err := c.GetRawData()
if err != nil { if err != nil {
c.JSON(400, gin.H{"error": "failed to read body"}) c.JSON(400, gin.H{"error": "failed to read body"})
return return
} }
var entries map[string][]config.ModelNameMapping var entries map[string][]config.OAuthModelAlias
if err = json.Unmarshal(data, &entries); err != nil { if err = json.Unmarshal(data, &entries); err != nil {
var wrapper struct { var wrapper struct {
Items map[string][]config.ModelNameMapping `json:"items"` Items map[string][]config.OAuthModelAlias `json:"items"`
} }
if err2 := json.Unmarshal(data, &wrapper); err2 != nil { if err2 := json.Unmarshal(data, &wrapper); err2 != nil {
c.JSON(400, gin.H{"error": "invalid body"}) c.JSON(400, gin.H{"error": "invalid body"})
@@ -725,15 +725,15 @@ func (h *Handler) PutOAuthModelMappings(c *gin.Context) {
} }
entries = wrapper.Items entries = wrapper.Items
} }
h.cfg.OAuthModelMappings = sanitizedOAuthModelMappings(entries) h.cfg.OAuthModelAlias = sanitizedOAuthModelAlias(entries)
h.persist(c) h.persist(c)
} }
func (h *Handler) PatchOAuthModelMappings(c *gin.Context) { func (h *Handler) PatchOAuthModelAlias(c *gin.Context) {
var body struct { var body struct {
Provider *string `json:"provider"` Provider *string `json:"provider"`
Channel *string `json:"channel"` Channel *string `json:"channel"`
Mappings []config.ModelNameMapping `json:"mappings"` Aliases []config.OAuthModelAlias `json:"aliases"`
} }
if errBindJSON := c.ShouldBindJSON(&body); errBindJSON != nil { if errBindJSON := c.ShouldBindJSON(&body); errBindJSON != nil {
c.JSON(400, gin.H{"error": "invalid body"}) c.JSON(400, gin.H{"error": "invalid body"})
@@ -751,32 +751,32 @@ func (h *Handler) PatchOAuthModelMappings(c *gin.Context) {
return return
} }
normalizedMap := sanitizedOAuthModelMappings(map[string][]config.ModelNameMapping{channel: body.Mappings}) normalizedMap := sanitizedOAuthModelAlias(map[string][]config.OAuthModelAlias{channel: body.Aliases})
normalized := normalizedMap[channel] normalized := normalizedMap[channel]
if len(normalized) == 0 { if len(normalized) == 0 {
if h.cfg.OAuthModelMappings == nil { if h.cfg.OAuthModelAlias == nil {
c.JSON(404, gin.H{"error": "channel not found"}) c.JSON(404, gin.H{"error": "channel not found"})
return return
} }
if _, ok := h.cfg.OAuthModelMappings[channel]; !ok { if _, ok := h.cfg.OAuthModelAlias[channel]; !ok {
c.JSON(404, gin.H{"error": "channel not found"}) c.JSON(404, gin.H{"error": "channel not found"})
return return
} }
delete(h.cfg.OAuthModelMappings, channel) delete(h.cfg.OAuthModelAlias, channel)
if len(h.cfg.OAuthModelMappings) == 0 { if len(h.cfg.OAuthModelAlias) == 0 {
h.cfg.OAuthModelMappings = nil h.cfg.OAuthModelAlias = nil
} }
h.persist(c) h.persist(c)
return return
} }
if h.cfg.OAuthModelMappings == nil { if h.cfg.OAuthModelAlias == nil {
h.cfg.OAuthModelMappings = make(map[string][]config.ModelNameMapping) h.cfg.OAuthModelAlias = make(map[string][]config.OAuthModelAlias)
} }
h.cfg.OAuthModelMappings[channel] = normalized h.cfg.OAuthModelAlias[channel] = normalized
h.persist(c) h.persist(c)
} }
func (h *Handler) DeleteOAuthModelMappings(c *gin.Context) { func (h *Handler) DeleteOAuthModelAlias(c *gin.Context) {
channel := strings.ToLower(strings.TrimSpace(c.Query("channel"))) channel := strings.ToLower(strings.TrimSpace(c.Query("channel")))
if channel == "" { if channel == "" {
channel = strings.ToLower(strings.TrimSpace(c.Query("provider"))) channel = strings.ToLower(strings.TrimSpace(c.Query("provider")))
@@ -785,17 +785,17 @@ func (h *Handler) DeleteOAuthModelMappings(c *gin.Context) {
c.JSON(400, gin.H{"error": "missing channel"}) c.JSON(400, gin.H{"error": "missing channel"})
return return
} }
if h.cfg.OAuthModelMappings == nil { if h.cfg.OAuthModelAlias == nil {
c.JSON(404, gin.H{"error": "channel not found"}) c.JSON(404, gin.H{"error": "channel not found"})
return return
} }
if _, ok := h.cfg.OAuthModelMappings[channel]; !ok { if _, ok := h.cfg.OAuthModelAlias[channel]; !ok {
c.JSON(404, gin.H{"error": "channel not found"}) c.JSON(404, gin.H{"error": "channel not found"})
return return
} }
delete(h.cfg.OAuthModelMappings, channel) delete(h.cfg.OAuthModelAlias, channel)
if len(h.cfg.OAuthModelMappings) == 0 { if len(h.cfg.OAuthModelAlias) == 0 {
h.cfg.OAuthModelMappings = nil h.cfg.OAuthModelAlias = nil
} }
h.persist(c) h.persist(c)
} }
@@ -1042,26 +1042,26 @@ func normalizeVertexCompatKey(entry *config.VertexCompatKey) {
entry.Models = normalized entry.Models = normalized
} }
func sanitizedOAuthModelMappings(entries map[string][]config.ModelNameMapping) map[string][]config.ModelNameMapping { func sanitizedOAuthModelAlias(entries map[string][]config.OAuthModelAlias) map[string][]config.OAuthModelAlias {
if len(entries) == 0 { if len(entries) == 0 {
return nil return nil
} }
copied := make(map[string][]config.ModelNameMapping, len(entries)) copied := make(map[string][]config.OAuthModelAlias, len(entries))
for channel, mappings := range entries { for channel, aliases := range entries {
if len(mappings) == 0 { if len(aliases) == 0 {
continue continue
} }
copied[channel] = append([]config.ModelNameMapping(nil), mappings...) copied[channel] = append([]config.OAuthModelAlias(nil), aliases...)
} }
if len(copied) == 0 { if len(copied) == 0 {
return nil return nil
} }
cfg := config.Config{OAuthModelMappings: copied} cfg := config.Config{OAuthModelAlias: copied}
cfg.SanitizeOAuthModelMappings() cfg.SanitizeOAuthModelAlias()
if len(cfg.OAuthModelMappings) == 0 { if len(cfg.OAuthModelAlias) == 0 {
return nil return nil
} }
return cfg.OAuthModelMappings return cfg.OAuthModelAlias
} }
// GetAmpCode returns the complete ampcode configuration. // GetAmpCode returns the complete ampcode configuration.

View File

@@ -601,10 +601,10 @@ func (s *Server) registerManagementRoutes() {
mgmt.PATCH("/oauth-excluded-models", s.mgmt.PatchOAuthExcludedModels) mgmt.PATCH("/oauth-excluded-models", s.mgmt.PatchOAuthExcludedModels)
mgmt.DELETE("/oauth-excluded-models", s.mgmt.DeleteOAuthExcludedModels) mgmt.DELETE("/oauth-excluded-models", s.mgmt.DeleteOAuthExcludedModels)
mgmt.GET("/oauth-model-mappings", s.mgmt.GetOAuthModelMappings) mgmt.GET("/oauth-model-alias", s.mgmt.GetOAuthModelAlias)
mgmt.PUT("/oauth-model-mappings", s.mgmt.PutOAuthModelMappings) mgmt.PUT("/oauth-model-alias", s.mgmt.PutOAuthModelAlias)
mgmt.PATCH("/oauth-model-mappings", s.mgmt.PatchOAuthModelMappings) mgmt.PATCH("/oauth-model-alias", s.mgmt.PatchOAuthModelAlias)
mgmt.DELETE("/oauth-model-mappings", s.mgmt.DeleteOAuthModelMappings) mgmt.DELETE("/oauth-model-alias", s.mgmt.DeleteOAuthModelAlias)
mgmt.GET("/auth-files", s.mgmt.ListAuthFiles) mgmt.GET("/auth-files", s.mgmt.ListAuthFiles)
mgmt.GET("/auth-files/models", s.mgmt.GetAuthFileModels) mgmt.GET("/auth-files/models", s.mgmt.GetAuthFileModels)

View File

@@ -91,13 +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"`
// OAuthModelMappings defines global model name mappings for OAuth/file-backed auth channels. // OAuthModelAlias defines global model name aliases for OAuth/file-backed auth channels.
// These mappings affect both model listing and model routing for supported channels: // These aliases affect both model listing and model routing for supported channels:
// gemini-cli, vertex, aistudio, antigravity, claude, codex, qwen, iflow. // 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:
// gemini-api-key, 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.
OAuthModelMappings map[string][]ModelNameMapping `yaml:"oauth-model-mappings,omitempty" json:"oauth-model-mappings,omitempty"` OAuthModelAlias map[string][]OAuthModelAlias `yaml:"oauth-model-alias,omitempty" json:"oauth-model-alias,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"`
@@ -145,11 +145,11 @@ type RoutingConfig struct {
Strategy string `yaml:"strategy,omitempty" json:"strategy,omitempty"` Strategy string `yaml:"strategy,omitempty" json:"strategy,omitempty"`
} }
// ModelNameMapping defines a model ID mapping for a specific channel. // OAuthModelAlias defines a model ID alias for a specific channel.
// It maps the upstream 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 // When Fork is true, the alias is added as an additional model in listings while
// keeping the original model ID available. // keeping the original model ID available.
type ModelNameMapping struct { type OAuthModelAlias 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"` Fork bool `yaml:"fork,omitempty" json:"fork,omitempty"`
@@ -436,6 +436,15 @@ func LoadConfig(configFile string) (*Config, error) {
// If optional is true and the file is missing, it returns an empty Config. // If optional is true and the file is missing, it returns an empty Config.
// If optional is true and the file is empty or invalid, it returns an empty Config. // If optional is true and the file is empty or invalid, it returns an empty Config.
func LoadConfigOptional(configFile string, optional bool) (*Config, error) { func LoadConfigOptional(configFile string, optional bool) (*Config, error) {
// Perform oauth-model-alias migration before loading config.
// This migrates oauth-model-mappings to oauth-model-alias if needed.
if migrated, err := MigrateOAuthModelAlias(configFile); err != nil {
// Log warning but don't fail - config loading should still work
fmt.Printf("Warning: oauth-model-alias migration failed: %v\n", err)
} else if migrated {
fmt.Println("Migrated oauth-model-mappings to oauth-model-alias")
}
// Read the entire configuration file into memory. // Read the entire configuration file into memory.
data, err := os.ReadFile(configFile) data, err := os.ReadFile(configFile)
if err != nil { if err != nil {
@@ -528,8 +537,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 OAuth model name mappings. // Normalize global OAuth model name aliases.
cfg.SanitizeOAuthModelMappings() cfg.SanitizeOAuthModelAlias()
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...")
@@ -547,24 +556,24 @@ func LoadConfigOptional(configFile string, optional bool) (*Config, error) {
return &cfg, nil return &cfg, nil
} }
// SanitizeOAuthModelMappings normalizes and deduplicates global OAuth model name mappings. // SanitizeOAuthModelAlias normalizes and deduplicates global OAuth model name aliases.
// It trims whitespace, normalizes channel keys to lower-case, drops empty entries, // It trims whitespace, normalizes channel keys to lower-case, drops empty entries,
// allows multiple aliases per upstream name, and ensures aliases 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) SanitizeOAuthModelAlias() {
if cfg == nil || len(cfg.OAuthModelMappings) == 0 { if cfg == nil || len(cfg.OAuthModelAlias) == 0 {
return return
} }
out := make(map[string][]ModelNameMapping, len(cfg.OAuthModelMappings)) out := make(map[string][]OAuthModelAlias, len(cfg.OAuthModelAlias))
for rawChannel, mappings := range cfg.OAuthModelMappings { for rawChannel, aliases := range cfg.OAuthModelAlias {
channel := strings.ToLower(strings.TrimSpace(rawChannel)) channel := strings.ToLower(strings.TrimSpace(rawChannel))
if channel == "" || len(mappings) == 0 { if channel == "" || len(aliases) == 0 {
continue continue
} }
seenAlias := make(map[string]struct{}, len(mappings)) seenAlias := make(map[string]struct{}, len(aliases))
clean := make([]ModelNameMapping, 0, len(mappings)) clean := make([]OAuthModelAlias, 0, len(aliases))
for _, mapping := range mappings { for _, entry := range aliases {
name := strings.TrimSpace(mapping.Name) name := strings.TrimSpace(entry.Name)
alias := strings.TrimSpace(mapping.Alias) alias := strings.TrimSpace(entry.Alias)
if name == "" || alias == "" { if name == "" || alias == "" {
continue continue
} }
@@ -576,13 +585,13 @@ func (cfg *Config) SanitizeOAuthModelMappings() {
continue continue
} }
seenAlias[aliasKey] = struct{}{} seenAlias[aliasKey] = struct{}{}
clean = append(clean, ModelNameMapping{Name: name, Alias: alias, Fork: mapping.Fork}) clean = append(clean, OAuthModelAlias{Name: name, Alias: alias, Fork: entry.Fork})
} }
if len(clean) > 0 { if len(clean) > 0 {
out[channel] = clean out[channel] = clean
} }
} }
cfg.OAuthModelMappings = out cfg.OAuthModelAlias = out
} }
// SanitizeOpenAICompatibility removes OpenAI-compatibility provider entries that are // SanitizeOpenAICompatibility removes OpenAI-compatibility provider entries that are

View File

@@ -0,0 +1,258 @@
package config
import (
"os"
"strings"
"gopkg.in/yaml.v3"
)
// antigravityModelConversionTable maps old built-in aliases to actual model names
// for the antigravity channel during migration.
var antigravityModelConversionTable = map[string]string{
"gemini-2.5-computer-use-preview-10-2025": "rev19-uic3-1p",
"gemini-3-pro-image-preview": "gemini-3-pro-image",
"gemini-3-pro-preview": "gemini-3-pro-high",
"gemini-3-flash-preview": "gemini-3-flash",
"gemini-claude-sonnet-4-5": "claude-sonnet-4-5",
"gemini-claude-sonnet-4-5-thinking": "claude-sonnet-4-5-thinking",
"gemini-claude-opus-4-5-thinking": "claude-opus-4-5-thinking",
}
// defaultAntigravityAliases returns the default oauth-model-alias configuration
// for the antigravity channel when neither field exists.
func defaultAntigravityAliases() []OAuthModelAlias {
return []OAuthModelAlias{
{Name: "rev19-uic3-1p", Alias: "gemini-2.5-computer-use-preview-10-2025"},
{Name: "gemini-3-pro-image", Alias: "gemini-3-pro-image-preview"},
{Name: "gemini-3-pro-high", Alias: "gemini-3-pro-preview"},
{Name: "gemini-3-flash", Alias: "gemini-3-flash-preview"},
{Name: "claude-sonnet-4-5", Alias: "gemini-claude-sonnet-4-5"},
{Name: "claude-sonnet-4-5-thinking", Alias: "gemini-claude-sonnet-4-5-thinking"},
{Name: "claude-opus-4-5-thinking", Alias: "gemini-claude-opus-4-5-thinking"},
}
}
// MigrateOAuthModelAlias checks for and performs migration from oauth-model-mappings
// to oauth-model-alias at startup. Returns true if migration was performed.
//
// Migration flow:
// 1. Check if oauth-model-alias exists -> skip migration
// 2. Check if oauth-model-mappings exists -> convert and migrate
// - For antigravity channel, convert old built-in aliases to actual model names
//
// 3. Neither exists -> add default antigravity config
func MigrateOAuthModelAlias(configFile string) (bool, error) {
data, err := os.ReadFile(configFile)
if err != nil {
if os.IsNotExist(err) {
return false, nil
}
return false, err
}
if len(data) == 0 {
return false, nil
}
// Parse YAML into node tree to preserve structure
var root yaml.Node
if err := yaml.Unmarshal(data, &root); err != nil {
return false, nil
}
if root.Kind != yaml.DocumentNode || len(root.Content) == 0 {
return false, nil
}
rootMap := root.Content[0]
if rootMap == nil || rootMap.Kind != yaml.MappingNode {
return false, nil
}
// Check if oauth-model-alias already exists
if findMapKeyIndex(rootMap, "oauth-model-alias") >= 0 {
return false, nil
}
// Check if oauth-model-mappings exists
oldIdx := findMapKeyIndex(rootMap, "oauth-model-mappings")
if oldIdx >= 0 {
// Migrate from old field
return migrateFromOldField(configFile, &root, rootMap, oldIdx)
}
// Neither field exists - add default antigravity config
return addDefaultAntigravityConfig(configFile, &root, rootMap)
}
// migrateFromOldField converts oauth-model-mappings to oauth-model-alias
func migrateFromOldField(configFile string, root *yaml.Node, rootMap *yaml.Node, oldIdx int) (bool, error) {
if oldIdx+1 >= len(rootMap.Content) {
return false, nil
}
oldValue := rootMap.Content[oldIdx+1]
if oldValue == nil || oldValue.Kind != yaml.MappingNode {
return false, nil
}
// Parse the old aliases
oldAliases := parseOldAliasNode(oldValue)
if len(oldAliases) == 0 {
// Remove the old field and write
removeMapKeyByIndex(rootMap, oldIdx)
return writeYAMLNode(configFile, root)
}
// Convert model names for antigravity channel
newAliases := make(map[string][]OAuthModelAlias, len(oldAliases))
for channel, entries := range oldAliases {
converted := make([]OAuthModelAlias, 0, len(entries))
for _, entry := range entries {
newEntry := OAuthModelAlias{
Name: entry.Name,
Alias: entry.Alias,
Fork: entry.Fork,
}
// Convert model names for antigravity channel
if strings.EqualFold(channel, "antigravity") {
if actual, ok := antigravityModelConversionTable[entry.Name]; ok {
newEntry.Name = actual
}
}
converted = append(converted, newEntry)
}
newAliases[channel] = converted
}
// Build new node
newNode := buildOAuthModelAliasNode(newAliases)
// Replace old key with new key and value
rootMap.Content[oldIdx].Value = "oauth-model-alias"
rootMap.Content[oldIdx+1] = newNode
return writeYAMLNode(configFile, root)
}
// addDefaultAntigravityConfig adds the default antigravity configuration
func addDefaultAntigravityConfig(configFile string, root *yaml.Node, rootMap *yaml.Node) (bool, error) {
defaults := map[string][]OAuthModelAlias{
"antigravity": defaultAntigravityAliases(),
}
newNode := buildOAuthModelAliasNode(defaults)
// Add new key-value pair
keyNode := &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!str", Value: "oauth-model-alias"}
rootMap.Content = append(rootMap.Content, keyNode, newNode)
return writeYAMLNode(configFile, root)
}
// parseOldAliasNode parses the old oauth-model-mappings node structure
func parseOldAliasNode(node *yaml.Node) map[string][]OAuthModelAlias {
if node == nil || node.Kind != yaml.MappingNode {
return nil
}
result := make(map[string][]OAuthModelAlias)
for i := 0; i+1 < len(node.Content); i += 2 {
channelNode := node.Content[i]
entriesNode := node.Content[i+1]
if channelNode == nil || entriesNode == nil {
continue
}
channel := strings.ToLower(strings.TrimSpace(channelNode.Value))
if channel == "" || entriesNode.Kind != yaml.SequenceNode {
continue
}
entries := make([]OAuthModelAlias, 0, len(entriesNode.Content))
for _, entryNode := range entriesNode.Content {
if entryNode == nil || entryNode.Kind != yaml.MappingNode {
continue
}
entry := parseAliasEntry(entryNode)
if entry.Name != "" && entry.Alias != "" {
entries = append(entries, entry)
}
}
if len(entries) > 0 {
result[channel] = entries
}
}
return result
}
// parseAliasEntry parses a single alias entry node
func parseAliasEntry(node *yaml.Node) OAuthModelAlias {
var entry OAuthModelAlias
for i := 0; i+1 < len(node.Content); i += 2 {
keyNode := node.Content[i]
valNode := node.Content[i+1]
if keyNode == nil || valNode == nil {
continue
}
switch strings.ToLower(strings.TrimSpace(keyNode.Value)) {
case "name":
entry.Name = strings.TrimSpace(valNode.Value)
case "alias":
entry.Alias = strings.TrimSpace(valNode.Value)
case "fork":
entry.Fork = strings.ToLower(strings.TrimSpace(valNode.Value)) == "true"
}
}
return entry
}
// buildOAuthModelAliasNode creates a YAML node for oauth-model-alias
func buildOAuthModelAliasNode(aliases map[string][]OAuthModelAlias) *yaml.Node {
node := &yaml.Node{Kind: yaml.MappingNode, Tag: "!!map"}
for channel, entries := range aliases {
channelNode := &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!str", Value: channel}
entriesNode := &yaml.Node{Kind: yaml.SequenceNode, Tag: "!!seq"}
for _, entry := range entries {
entryNode := &yaml.Node{Kind: yaml.MappingNode, Tag: "!!map"}
entryNode.Content = append(entryNode.Content,
&yaml.Node{Kind: yaml.ScalarNode, Tag: "!!str", Value: "name"},
&yaml.Node{Kind: yaml.ScalarNode, Tag: "!!str", Value: entry.Name},
&yaml.Node{Kind: yaml.ScalarNode, Tag: "!!str", Value: "alias"},
&yaml.Node{Kind: yaml.ScalarNode, Tag: "!!str", Value: entry.Alias},
)
if entry.Fork {
entryNode.Content = append(entryNode.Content,
&yaml.Node{Kind: yaml.ScalarNode, Tag: "!!str", Value: "fork"},
&yaml.Node{Kind: yaml.ScalarNode, Tag: "!!bool", Value: "true"},
)
}
entriesNode.Content = append(entriesNode.Content, entryNode)
}
node.Content = append(node.Content, channelNode, entriesNode)
}
return node
}
// removeMapKeyByIndex removes a key-value pair from a mapping node by index
func removeMapKeyByIndex(mapNode *yaml.Node, keyIdx int) {
if mapNode == nil || mapNode.Kind != yaml.MappingNode {
return
}
if keyIdx < 0 || keyIdx+1 >= len(mapNode.Content) {
return
}
mapNode.Content = append(mapNode.Content[:keyIdx], mapNode.Content[keyIdx+2:]...)
}
// writeYAMLNode writes the YAML node tree back to file
func writeYAMLNode(configFile string, root *yaml.Node) (bool, error) {
f, err := os.Create(configFile)
if err != nil {
return false, err
}
defer f.Close()
enc := yaml.NewEncoder(f)
enc.SetIndent(2)
if err := enc.Encode(root); err != nil {
return false, err
}
if err := enc.Close(); err != nil {
return false, err
}
return true, nil
}

View File

@@ -0,0 +1,225 @@
package config
import (
"os"
"path/filepath"
"strings"
"testing"
"gopkg.in/yaml.v3"
)
func TestMigrateOAuthModelAlias_SkipsIfNewFieldExists(t *testing.T) {
t.Parallel()
dir := t.TempDir()
configFile := filepath.Join(dir, "config.yaml")
content := `oauth-model-alias:
gemini-cli:
- name: "gemini-2.5-pro"
alias: "g2.5p"
`
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
t.Fatal(err)
}
migrated, err := MigrateOAuthModelAlias(configFile)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if migrated {
t.Fatal("expected no migration when oauth-model-alias already exists")
}
// Verify file unchanged
data, _ := os.ReadFile(configFile)
if !strings.Contains(string(data), "oauth-model-alias:") {
t.Fatal("file should still contain oauth-model-alias")
}
}
func TestMigrateOAuthModelAlias_MigratesOldField(t *testing.T) {
t.Parallel()
dir := t.TempDir()
configFile := filepath.Join(dir, "config.yaml")
content := `oauth-model-mappings:
gemini-cli:
- name: "gemini-2.5-pro"
alias: "g2.5p"
fork: true
`
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
t.Fatal(err)
}
migrated, err := MigrateOAuthModelAlias(configFile)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !migrated {
t.Fatal("expected migration to occur")
}
// Verify new field exists and old field removed
data, _ := os.ReadFile(configFile)
if strings.Contains(string(data), "oauth-model-mappings:") {
t.Fatal("old field should be removed")
}
if !strings.Contains(string(data), "oauth-model-alias:") {
t.Fatal("new field should exist")
}
// Parse and verify structure
var root yaml.Node
if err := yaml.Unmarshal(data, &root); err != nil {
t.Fatal(err)
}
}
func TestMigrateOAuthModelAlias_ConvertsAntigravityModels(t *testing.T) {
t.Parallel()
dir := t.TempDir()
configFile := filepath.Join(dir, "config.yaml")
// Use old model names that should be converted
content := `oauth-model-mappings:
antigravity:
- name: "gemini-2.5-computer-use-preview-10-2025"
alias: "computer-use"
- name: "gemini-3-pro-preview"
alias: "g3p"
`
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
t.Fatal(err)
}
migrated, err := MigrateOAuthModelAlias(configFile)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !migrated {
t.Fatal("expected migration to occur")
}
// Verify model names were converted
data, _ := os.ReadFile(configFile)
content = string(data)
if !strings.Contains(content, "rev19-uic3-1p") {
t.Fatal("expected gemini-2.5-computer-use-preview-10-2025 to be converted to rev19-uic3-1p")
}
if !strings.Contains(content, "gemini-3-pro-high") {
t.Fatal("expected gemini-3-pro-preview to be converted to gemini-3-pro-high")
}
}
func TestMigrateOAuthModelAlias_AddsDefaultIfNeitherExists(t *testing.T) {
t.Parallel()
dir := t.TempDir()
configFile := filepath.Join(dir, "config.yaml")
content := `debug: true
port: 8080
`
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
t.Fatal(err)
}
migrated, err := MigrateOAuthModelAlias(configFile)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !migrated {
t.Fatal("expected migration to add default config")
}
// Verify default antigravity config was added
data, _ := os.ReadFile(configFile)
content = string(data)
if !strings.Contains(content, "oauth-model-alias:") {
t.Fatal("expected oauth-model-alias to be added")
}
if !strings.Contains(content, "antigravity:") {
t.Fatal("expected antigravity channel to be added")
}
if !strings.Contains(content, "rev19-uic3-1p") {
t.Fatal("expected default antigravity aliases to include rev19-uic3-1p")
}
}
func TestMigrateOAuthModelAlias_PreservesOtherConfig(t *testing.T) {
t.Parallel()
dir := t.TempDir()
configFile := filepath.Join(dir, "config.yaml")
content := `debug: true
port: 8080
oauth-model-mappings:
gemini-cli:
- name: "test"
alias: "t"
api-keys:
- "key1"
- "key2"
`
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
t.Fatal(err)
}
migrated, err := MigrateOAuthModelAlias(configFile)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !migrated {
t.Fatal("expected migration to occur")
}
// Verify other config preserved
data, _ := os.ReadFile(configFile)
content = string(data)
if !strings.Contains(content, "debug: true") {
t.Fatal("expected debug field to be preserved")
}
if !strings.Contains(content, "port: 8080") {
t.Fatal("expected port field to be preserved")
}
if !strings.Contains(content, "api-keys:") {
t.Fatal("expected api-keys field to be preserved")
}
}
func TestMigrateOAuthModelAlias_NonexistentFile(t *testing.T) {
t.Parallel()
migrated, err := MigrateOAuthModelAlias("/nonexistent/path/config.yaml")
if err != nil {
t.Fatalf("unexpected error for nonexistent file: %v", err)
}
if migrated {
t.Fatal("expected no migration for nonexistent file")
}
}
func TestMigrateOAuthModelAlias_EmptyFile(t *testing.T) {
t.Parallel()
dir := t.TempDir()
configFile := filepath.Join(dir, "config.yaml")
if err := os.WriteFile(configFile, []byte(""), 0644); err != nil {
t.Fatal(err)
}
migrated, err := MigrateOAuthModelAlias(configFile)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if migrated {
t.Fatal("expected no migration for empty file")
}
}

View File

@@ -0,0 +1,56 @@
package config
import "testing"
func TestSanitizeOAuthModelAlias_PreservesForkFlag(t *testing.T) {
cfg := &Config{
OAuthModelAlias: map[string][]OAuthModelAlias{
" CoDeX ": {
{Name: " gpt-5 ", Alias: " g5 ", Fork: true},
{Name: "gpt-6", Alias: "g6"},
},
},
}
cfg.SanitizeOAuthModelAlias()
aliases := cfg.OAuthModelAlias["codex"]
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[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)
}
}
func TestSanitizeOAuthModelAlias_AllowsMultipleAliasesForSameName(t *testing.T) {
cfg := &Config{
OAuthModelAlias: map[string][]OAuthModelAlias{
"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.SanitizeOAuthModelAlias()
aliases := cfg.OAuthModelAlias["antigravity"]
expected := []OAuthModelAlias{
{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(aliases) != len(expected) {
t.Fatalf("expected %d sanitized aliases, got %d", len(expected), len(aliases))
}
for i, exp := range expected {
if aliases[i].Name != exp.Name || aliases[i].Alias != exp.Alias || aliases[i].Fork != exp.Fork {
t.Fatalf("expected alias %d to be name=%q alias=%q fork=%v, got name=%q alias=%q fork=%v", i, exp.Name, exp.Alias, exp.Fork, aliases[i].Name, aliases[i].Alias, aliases[i].Fork)
}
}
}

View File

@@ -1,56 +0,0 @@
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)
}
}
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)
}
}
}

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.OAuthModelMappings, newConfig.OAuthModelMappings)) forceAuthRefresh := oldConfig != nil && (oldConfig.ForceModelPrefix != newConfig.ForceModelPrefix || !reflect.DeepEqual(oldConfig.OAuthModelAlias, newConfig.OAuthModelAlias))
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

@@ -212,7 +212,7 @@ func BuildConfigChangeDetails(oldCfg, newCfg *config.Config) []string {
if entries, _ := DiffOAuthExcludedModelChanges(oldCfg.OAuthExcludedModels, newCfg.OAuthExcludedModels); len(entries) > 0 { if entries, _ := DiffOAuthExcludedModelChanges(oldCfg.OAuthExcludedModels, newCfg.OAuthExcludedModels); len(entries) > 0 {
changes = append(changes, entries...) changes = append(changes, entries...)
} }
if entries, _ := DiffOAuthModelMappingChanges(oldCfg.OAuthModelMappings, newCfg.OAuthModelMappings); len(entries) > 0 { if entries, _ := DiffOAuthModelAliasChanges(oldCfg.OAuthModelAlias, newCfg.OAuthModelAlias); len(entries) > 0 {
changes = append(changes, entries...) changes = append(changes, entries...)
} }

View File

@@ -10,23 +10,23 @@ import (
"github.com/router-for-me/CLIProxyAPI/v6/internal/config" "github.com/router-for-me/CLIProxyAPI/v6/internal/config"
) )
type OAuthModelMappingsSummary struct { type OAuthModelAliasSummary struct {
hash string hash string
count int count int
} }
// SummarizeOAuthModelMappings summarizes OAuth model mappings per channel. // SummarizeOAuthModelAlias summarizes OAuth model alias per channel.
func SummarizeOAuthModelMappings(entries map[string][]config.ModelNameMapping) map[string]OAuthModelMappingsSummary { func SummarizeOAuthModelAlias(entries map[string][]config.OAuthModelAlias) map[string]OAuthModelAliasSummary {
if len(entries) == 0 { if len(entries) == 0 {
return nil return nil
} }
out := make(map[string]OAuthModelMappingsSummary, len(entries)) out := make(map[string]OAuthModelAliasSummary, len(entries))
for k, v := range entries { for k, v := range entries {
key := strings.ToLower(strings.TrimSpace(k)) key := strings.ToLower(strings.TrimSpace(k))
if key == "" { if key == "" {
continue continue
} }
out[key] = summarizeOAuthModelMappingList(v) out[key] = summarizeOAuthModelAliasList(v)
} }
if len(out) == 0 { if len(out) == 0 {
return nil return nil
@@ -34,10 +34,10 @@ func SummarizeOAuthModelMappings(entries map[string][]config.ModelNameMapping) m
return out return out
} }
// DiffOAuthModelMappingChanges compares OAuth model mappings maps. // DiffOAuthModelAliasChanges compares OAuth model alias maps.
func DiffOAuthModelMappingChanges(oldMap, newMap map[string][]config.ModelNameMapping) ([]string, []string) { func DiffOAuthModelAliasChanges(oldMap, newMap map[string][]config.OAuthModelAlias) ([]string, []string) {
oldSummary := SummarizeOAuthModelMappings(oldMap) oldSummary := SummarizeOAuthModelAlias(oldMap)
newSummary := SummarizeOAuthModelMappings(newMap) newSummary := SummarizeOAuthModelAlias(newMap)
keys := make(map[string]struct{}, len(oldSummary)+len(newSummary)) keys := make(map[string]struct{}, len(oldSummary)+len(newSummary))
for k := range oldSummary { for k := range oldSummary {
keys[k] = struct{}{} keys[k] = struct{}{}
@@ -52,13 +52,13 @@ func DiffOAuthModelMappingChanges(oldMap, newMap map[string][]config.ModelNameMa
newInfo, okNew := newSummary[key] newInfo, okNew := newSummary[key]
switch { switch {
case okOld && !okNew: case okOld && !okNew:
changes = append(changes, fmt.Sprintf("oauth-model-mappings[%s]: removed", key)) changes = append(changes, fmt.Sprintf("oauth-model-alias[%s]: removed", key))
affected = append(affected, key) affected = append(affected, key)
case !okOld && okNew: case !okOld && okNew:
changes = append(changes, fmt.Sprintf("oauth-model-mappings[%s]: added (%d entries)", key, newInfo.count)) changes = append(changes, fmt.Sprintf("oauth-model-alias[%s]: added (%d entries)", key, newInfo.count))
affected = append(affected, key) affected = append(affected, key)
case okOld && okNew && oldInfo.hash != newInfo.hash: case okOld && okNew && oldInfo.hash != newInfo.hash:
changes = append(changes, fmt.Sprintf("oauth-model-mappings[%s]: updated (%d -> %d entries)", key, oldInfo.count, newInfo.count)) changes = append(changes, fmt.Sprintf("oauth-model-alias[%s]: updated (%d -> %d entries)", key, oldInfo.count, newInfo.count))
affected = append(affected, key) affected = append(affected, key)
} }
} }
@@ -67,20 +67,20 @@ func DiffOAuthModelMappingChanges(oldMap, newMap map[string][]config.ModelNameMa
return changes, affected return changes, affected
} }
func summarizeOAuthModelMappingList(list []config.ModelNameMapping) OAuthModelMappingsSummary { func summarizeOAuthModelAliasList(list []config.OAuthModelAlias) OAuthModelAliasSummary {
if len(list) == 0 { if len(list) == 0 {
return OAuthModelMappingsSummary{} return OAuthModelAliasSummary{}
} }
seen := make(map[string]struct{}, len(list)) seen := make(map[string]struct{}, len(list))
normalized := make([]string, 0, len(list)) normalized := make([]string, 0, len(list))
for _, mapping := range list { for _, alias := range list {
name := strings.ToLower(strings.TrimSpace(mapping.Name)) name := strings.ToLower(strings.TrimSpace(alias.Name))
alias := strings.ToLower(strings.TrimSpace(mapping.Alias)) aliasVal := strings.ToLower(strings.TrimSpace(alias.Alias))
if name == "" || alias == "" { if name == "" || aliasVal == "" {
continue continue
} }
key := name + "->" + alias key := name + "->" + aliasVal
if mapping.Fork { if alias.Fork {
key += "|fork" key += "|fork"
} }
if _, exists := seen[key]; exists { if _, exists := seen[key]; exists {
@@ -90,11 +90,11 @@ func summarizeOAuthModelMappingList(list []config.ModelNameMapping) OAuthModelMa
normalized = append(normalized, key) normalized = append(normalized, key)
} }
if len(normalized) == 0 { if len(normalized) == 0 {
return OAuthModelMappingsSummary{} return OAuthModelAliasSummary{}
} }
sort.Strings(normalized) sort.Strings(normalized)
sum := sha256.Sum256([]byte(strings.Join(normalized, "|"))) sum := sha256.Sum256([]byte(strings.Join(normalized, "|")))
return OAuthModelMappingsSummary{ return OAuthModelAliasSummary{
hash: hex.EncodeToString(sum[:]), hash: hex.EncodeToString(sum[:]),
count: len(normalized), count: len(normalized),
} }

View File

@@ -66,7 +66,7 @@ func TestLookupAPIKeyUpstreamModel(t *testing.T) {
} }
} }
func TestAPIKeyModelMappings_ConfigHotReload(t *testing.T) { func TestAPIKeyModelAlias_ConfigHotReload(t *testing.T) {
cfg := &internalconfig.Config{ cfg := &internalconfig.Config{
GeminiKey: []internalconfig.GeminiKey{ GeminiKey: []internalconfig.GeminiKey{
{ {
@@ -82,12 +82,12 @@ func TestAPIKeyModelMappings_ConfigHotReload(t *testing.T) {
ctx := context.Background() ctx := context.Background()
_, _ = mgr.Register(ctx, &Auth{ID: "a1", Provider: "gemini", Attributes: map[string]string{"api_key": "k"}}) _, _ = mgr.Register(ctx, &Auth{ID: "a1", Provider: "gemini", Attributes: map[string]string{"api_key": "k"}})
// Initial mapping // Initial alias
if resolved := mgr.lookupAPIKeyUpstreamModel("a1", "g25p"); resolved != "gemini-2.5-pro-exp-03-25" { if resolved := mgr.lookupAPIKeyUpstreamModel("a1", "g25p"); resolved != "gemini-2.5-pro-exp-03-25" {
t.Fatalf("before reload: got %q, want %q", resolved, "gemini-2.5-pro-exp-03-25") t.Fatalf("before reload: got %q, want %q", resolved, "gemini-2.5-pro-exp-03-25")
} }
// Hot reload with new mapping // Hot reload with new alias
mgr.SetConfig(&internalconfig.Config{ mgr.SetConfig(&internalconfig.Config{
GeminiKey: []internalconfig.GeminiKey{ GeminiKey: []internalconfig.GeminiKey{
{ {
@@ -97,13 +97,13 @@ func TestAPIKeyModelMappings_ConfigHotReload(t *testing.T) {
}, },
}) })
// New mapping should take effect // New alias should take effect
if resolved := mgr.lookupAPIKeyUpstreamModel("a1", "g25p"); resolved != "gemini-2.5-flash" { if resolved := mgr.lookupAPIKeyUpstreamModel("a1", "g25p"); resolved != "gemini-2.5-flash" {
t.Fatalf("after reload: got %q, want %q", resolved, "gemini-2.5-flash") t.Fatalf("after reload: got %q, want %q", resolved, "gemini-2.5-flash")
} }
} }
func TestAPIKeyModelMappings_MultipleProviders(t *testing.T) { func TestAPIKeyModelAlias_MultipleProviders(t *testing.T) {
cfg := &internalconfig.Config{ cfg := &internalconfig.Config{
GeminiKey: []internalconfig.GeminiKey{{APIKey: "gemini-key", Models: []internalconfig.GeminiModel{{Name: "gemini-2.5-pro", Alias: "gp"}}}}, GeminiKey: []internalconfig.GeminiKey{{APIKey: "gemini-key", Models: []internalconfig.GeminiModel{{Name: "gemini-2.5-pro", Alias: "gp"}}}},
ClaudeKey: []internalconfig.ClaudeKey{{APIKey: "claude-key", Models: []internalconfig.ClaudeModel{{Name: "claude-sonnet-4", Alias: "cs4"}}}}, ClaudeKey: []internalconfig.ClaudeKey{{APIKey: "claude-key", Models: []internalconfig.ClaudeModel{{Name: "claude-sonnet-4", Alias: "cs4"}}}},
@@ -133,7 +133,7 @@ func TestAPIKeyModelMappings_MultipleProviders(t *testing.T) {
} }
} }
func TestApplyAPIKeyModelMapping(t *testing.T) { func TestApplyAPIKeyModelAlias(t *testing.T) {
cfg := &internalconfig.Config{ cfg := &internalconfig.Config{
GeminiKey: []internalconfig.GeminiKey{ GeminiKey: []internalconfig.GeminiKey{
{APIKey: "k", Models: []internalconfig.GeminiModel{{Name: "gemini-2.5-pro-exp-03-25", Alias: "g25p"}}}, {APIKey: "k", Models: []internalconfig.GeminiModel{{Name: "gemini-2.5-pro-exp-03-25", Alias: "g25p"}}},
@@ -170,7 +170,7 @@ func TestApplyAPIKeyModelMapping(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
resolvedModel := mgr.applyAPIKeyModelMapping(tt.auth, tt.inputModel) resolvedModel := mgr.applyAPIKeyModelAlias(tt.auth, tt.inputModel)
if resolvedModel != tt.wantModel { if resolvedModel != tt.wantModel {
t.Errorf("model = %q, want %q", resolvedModel, tt.wantModel) t.Errorf("model = %q, want %q", resolvedModel, tt.wantModel)

View File

@@ -119,17 +119,17 @@ type Manager struct {
requestRetry atomic.Int32 requestRetry atomic.Int32
maxRetryInterval atomic.Int64 maxRetryInterval atomic.Int64
// modelNameMappings stores global model name alias mappings (alias -> upstream name) keyed by channel. // oauthModelAlias stores global OAuth model alias mappings (alias -> upstream name) keyed by channel.
modelNameMappings atomic.Value oauthModelAlias atomic.Value
// apiKeyModelAlias caches resolved model alias mappings for API-key auths.
// Keyed by auth.ID, value is alias(lower) -> upstream model (including suffix).
apiKeyModelAlias atomic.Value
// runtimeConfig stores the latest application config for request-time decisions. // runtimeConfig stores the latest application config for request-time decisions.
// It is initialized in NewManager; never Load() before first Store(). // It is initialized in NewManager; never Load() before first Store().
runtimeConfig atomic.Value runtimeConfig atomic.Value
// apiKeyModelMappings caches resolved model alias mappings for API-key auths.
// Keyed by auth.ID, value is alias(lower) -> upstream model (including suffix).
apiKeyModelMappings atomic.Value
// Optional HTTP RoundTripper provider injected by host. // Optional HTTP RoundTripper provider injected by host.
rtProvider RoundTripperProvider rtProvider RoundTripperProvider
@@ -155,7 +155,7 @@ func NewManager(store Store, selector Selector, hook Hook) *Manager {
} }
// atomic.Value requires non-nil initial value. // atomic.Value requires non-nil initial value.
manager.runtimeConfig.Store(&internalconfig.Config{}) manager.runtimeConfig.Store(&internalconfig.Config{})
manager.apiKeyModelMappings.Store(apiKeyModelMappingTable(nil)) manager.apiKeyModelAlias.Store(apiKeyModelAliasTable(nil))
return manager return manager
} }
@@ -195,7 +195,7 @@ func (m *Manager) SetConfig(cfg *internalconfig.Config) {
cfg = &internalconfig.Config{} cfg = &internalconfig.Config{}
} }
m.runtimeConfig.Store(cfg) m.runtimeConfig.Store(cfg)
m.rebuildAPIKeyModelMappingsFromRuntimeConfig() m.rebuildAPIKeyModelAliasFromRuntimeConfig()
} }
func (m *Manager) lookupAPIKeyUpstreamModel(authID, requestedModel string) string { func (m *Manager) lookupAPIKeyUpstreamModel(authID, requestedModel string) string {
@@ -210,7 +210,7 @@ func (m *Manager) lookupAPIKeyUpstreamModel(authID, requestedModel string) strin
if requestedModel == "" { if requestedModel == "" {
return "" return ""
} }
table, _ := m.apiKeyModelMappings.Load().(apiKeyModelMappingTable) table, _ := m.apiKeyModelAlias.Load().(apiKeyModelAliasTable)
if table == nil { if table == nil {
return "" return ""
} }
@@ -238,7 +238,7 @@ func (m *Manager) lookupAPIKeyUpstreamModel(authID, requestedModel string) strin
} }
func (m *Manager) rebuildAPIKeyModelMappingsFromRuntimeConfig() { func (m *Manager) rebuildAPIKeyModelAliasFromRuntimeConfig() {
if m == nil { if m == nil {
return return
} }
@@ -248,10 +248,10 @@ func (m *Manager) rebuildAPIKeyModelMappingsFromRuntimeConfig() {
} }
m.mu.Lock() m.mu.Lock()
defer m.mu.Unlock() defer m.mu.Unlock()
m.rebuildAPIKeyModelMappingsLocked(cfg) m.rebuildAPIKeyModelAliasLocked(cfg)
} }
func (m *Manager) rebuildAPIKeyModelMappingsLocked(cfg *internalconfig.Config) { func (m *Manager) rebuildAPIKeyModelAliasLocked(cfg *internalconfig.Config) {
if m == nil { if m == nil {
return return
} }
@@ -259,7 +259,7 @@ func (m *Manager) rebuildAPIKeyModelMappingsLocked(cfg *internalconfig.Config) {
cfg = &internalconfig.Config{} cfg = &internalconfig.Config{}
} }
out := make(apiKeyModelMappingTable) out := make(apiKeyModelAliasTable)
for _, auth := range m.auths { for _, auth := range m.auths {
if auth == nil { if auth == nil {
continue continue
@@ -277,19 +277,19 @@ func (m *Manager) rebuildAPIKeyModelMappingsLocked(cfg *internalconfig.Config) {
switch provider { switch provider {
case "gemini": case "gemini":
if entry := resolveGeminiAPIKeyConfig(cfg, auth); entry != nil { if entry := resolveGeminiAPIKeyConfig(cfg, auth); entry != nil {
compileAPIKeyModelMappingsForModels(byAlias, entry.Models) compileAPIKeyModelAliasForModels(byAlias, entry.Models)
} }
case "claude": case "claude":
if entry := resolveClaudeAPIKeyConfig(cfg, auth); entry != nil { if entry := resolveClaudeAPIKeyConfig(cfg, auth); entry != nil {
compileAPIKeyModelMappingsForModels(byAlias, entry.Models) compileAPIKeyModelAliasForModels(byAlias, entry.Models)
} }
case "codex": case "codex":
if entry := resolveCodexAPIKeyConfig(cfg, auth); entry != nil { if entry := resolveCodexAPIKeyConfig(cfg, auth); entry != nil {
compileAPIKeyModelMappingsForModels(byAlias, entry.Models) compileAPIKeyModelAliasForModels(byAlias, entry.Models)
} }
case "vertex": case "vertex":
if entry := resolveVertexAPIKeyConfig(cfg, auth); entry != nil { if entry := resolveVertexAPIKeyConfig(cfg, auth); entry != nil {
compileAPIKeyModelMappingsForModels(byAlias, entry.Models) compileAPIKeyModelAliasForModels(byAlias, entry.Models)
} }
default: default:
// OpenAI-compat uses config selection from auth.Attributes. // OpenAI-compat uses config selection from auth.Attributes.
@@ -301,7 +301,7 @@ func (m *Manager) rebuildAPIKeyModelMappingsLocked(cfg *internalconfig.Config) {
} }
if compatName != "" || strings.EqualFold(strings.TrimSpace(auth.Provider), "openai-compatibility") { if compatName != "" || strings.EqualFold(strings.TrimSpace(auth.Provider), "openai-compatibility") {
if entry := resolveOpenAICompatConfig(cfg, providerKey, compatName, auth.Provider); entry != nil { if entry := resolveOpenAICompatConfig(cfg, providerKey, compatName, auth.Provider); entry != nil {
compileAPIKeyModelMappingsForModels(byAlias, entry.Models) compileAPIKeyModelAliasForModels(byAlias, entry.Models)
} }
} }
} }
@@ -311,10 +311,10 @@ func (m *Manager) rebuildAPIKeyModelMappingsLocked(cfg *internalconfig.Config) {
} }
} }
m.apiKeyModelMappings.Store(out) m.apiKeyModelAlias.Store(out)
} }
func compileAPIKeyModelMappingsForModels[T interface { func compileAPIKeyModelAliasForModels[T interface {
GetName() string GetName() string
GetAlias() string GetAlias() string
}](out map[string]string, models []T) { }](out map[string]string, models []T) {
@@ -408,7 +408,7 @@ func (m *Manager) Register(ctx context.Context, auth *Auth) (*Auth, error) {
m.mu.Lock() m.mu.Lock()
m.auths[auth.ID] = auth.Clone() m.auths[auth.ID] = auth.Clone()
m.mu.Unlock() m.mu.Unlock()
m.rebuildAPIKeyModelMappingsFromRuntimeConfig() m.rebuildAPIKeyModelAliasFromRuntimeConfig()
_ = m.persist(ctx, auth) _ = m.persist(ctx, auth)
m.hook.OnAuthRegistered(ctx, auth.Clone()) m.hook.OnAuthRegistered(ctx, auth.Clone())
return auth.Clone(), nil return auth.Clone(), nil
@@ -427,7 +427,7 @@ func (m *Manager) Update(ctx context.Context, auth *Auth) (*Auth, error) {
auth.EnsureIndex() auth.EnsureIndex()
m.auths[auth.ID] = auth.Clone() m.auths[auth.ID] = auth.Clone()
m.mu.Unlock() m.mu.Unlock()
m.rebuildAPIKeyModelMappingsFromRuntimeConfig() m.rebuildAPIKeyModelAliasFromRuntimeConfig()
_ = m.persist(ctx, auth) _ = m.persist(ctx, auth)
m.hook.OnAuthUpdated(ctx, auth.Clone()) m.hook.OnAuthUpdated(ctx, auth.Clone())
return auth.Clone(), nil return auth.Clone(), nil
@@ -456,7 +456,7 @@ func (m *Manager) Load(ctx context.Context) error {
if cfg == nil { if cfg == nil {
cfg = &internalconfig.Config{} cfg = &internalconfig.Config{}
} }
m.rebuildAPIKeyModelMappingsLocked(cfg) m.rebuildAPIKeyModelAliasLocked(cfg)
return nil return nil
} }
@@ -592,8 +592,8 @@ func (m *Manager) executeMixedOnce(ctx context.Context, providers []string, req
} }
execReq := req execReq := req
execReq.Model = rewriteModelForAuth(routeModel, auth) execReq.Model = rewriteModelForAuth(routeModel, auth)
execReq.Model = m.applyOAuthModelMapping(auth, execReq.Model) execReq.Model = m.applyOAuthModelAlias(auth, execReq.Model)
execReq.Model = m.applyAPIKeyModelMapping(auth, execReq.Model) execReq.Model = m.applyAPIKeyModelAlias(auth, execReq.Model)
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 {
@@ -641,8 +641,8 @@ func (m *Manager) executeCountMixedOnce(ctx context.Context, providers []string,
} }
execReq := req execReq := req
execReq.Model = rewriteModelForAuth(routeModel, auth) execReq.Model = rewriteModelForAuth(routeModel, auth)
execReq.Model = m.applyOAuthModelMapping(auth, execReq.Model) execReq.Model = m.applyOAuthModelAlias(auth, execReq.Model)
execReq.Model = m.applyAPIKeyModelMapping(auth, execReq.Model) execReq.Model = m.applyAPIKeyModelAlias(auth, execReq.Model)
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 {
@@ -690,8 +690,8 @@ func (m *Manager) executeStreamMixedOnce(ctx context.Context, providers []string
} }
execReq := req execReq := req
execReq.Model = rewriteModelForAuth(routeModel, auth) execReq.Model = rewriteModelForAuth(routeModel, auth)
execReq.Model = m.applyOAuthModelMapping(auth, execReq.Model) execReq.Model = m.applyOAuthModelAlias(auth, execReq.Model)
execReq.Model = m.applyAPIKeyModelMapping(auth, execReq.Model) execReq.Model = m.applyAPIKeyModelAlias(auth, execReq.Model)
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()}
@@ -756,8 +756,8 @@ func (m *Manager) executeWithProvider(ctx context.Context, provider string, req
} }
execReq := req execReq := req
execReq.Model = rewriteModelForAuth(routeModel, auth) execReq.Model = rewriteModelForAuth(routeModel, auth)
execReq.Model = m.applyOAuthModelMapping(auth, execReq.Model) execReq.Model = m.applyOAuthModelAlias(auth, execReq.Model)
execReq.Model = m.applyAPIKeyModelMapping(auth, execReq.Model) execReq.Model = m.applyAPIKeyModelAlias(auth, execReq.Model)
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 {
@@ -805,8 +805,8 @@ func (m *Manager) executeCountWithProvider(ctx context.Context, provider string,
} }
execReq := req execReq := req
execReq.Model = rewriteModelForAuth(routeModel, auth) execReq.Model = rewriteModelForAuth(routeModel, auth)
execReq.Model = m.applyOAuthModelMapping(auth, execReq.Model) execReq.Model = m.applyOAuthModelAlias(auth, execReq.Model)
execReq.Model = m.applyAPIKeyModelMapping(auth, execReq.Model) execReq.Model = m.applyAPIKeyModelAlias(auth, execReq.Model)
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 {
@@ -854,8 +854,8 @@ func (m *Manager) executeStreamWithProvider(ctx context.Context, provider string
} }
execReq := req execReq := req
execReq.Model = rewriteModelForAuth(routeModel, auth) execReq.Model = rewriteModelForAuth(routeModel, auth)
execReq.Model = m.applyOAuthModelMapping(auth, execReq.Model) execReq.Model = m.applyOAuthModelAlias(auth, execReq.Model)
execReq.Model = m.applyAPIKeyModelMapping(auth, execReq.Model) execReq.Model = m.applyAPIKeyModelAlias(auth, execReq.Model)
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()}
@@ -908,7 +908,7 @@ func rewriteModelForAuth(model string, auth *Auth) string {
return strings.TrimPrefix(model, needle) return strings.TrimPrefix(model, needle)
} }
func (m *Manager) applyAPIKeyModelMapping(auth *Auth, requestedModel string) string { func (m *Manager) applyAPIKeyModelAlias(auth *Auth, requestedModel string) string {
if m == nil || auth == nil { if m == nil || auth == nil {
return requestedModel return requestedModel
} }
@@ -1079,7 +1079,7 @@ func resolveUpstreamModelForOpenAICompatAPIKey(cfg *internalconfig.Config, auth
return resolveModelAliasFromConfigModels(requestedModel, asModelAliasEntries(entry.Models)) return resolveModelAliasFromConfigModels(requestedModel, asModelAliasEntries(entry.Models))
} }
type apiKeyModelMappingTable map[string]map[string]string type apiKeyModelAliasTable map[string]map[string]string
func resolveOpenAICompatConfig(cfg *internalconfig.Config, providerKey, compatName, authProvider string) *internalconfig.OpenAICompatibility { func resolveOpenAICompatConfig(cfg *internalconfig.Config, providerKey, compatName, authProvider string) *internalconfig.OpenAICompatibility {
if cfg == nil { if cfg == nil {
@@ -1109,11 +1109,11 @@ func resolveOpenAICompatConfig(cfg *internalconfig.Config, providerKey, compatNa
func asModelAliasEntries[T interface { func asModelAliasEntries[T interface {
GetName() string GetName() string
GetAlias() string GetAlias() string
}](models []T) []modelMappingEntry { }](models []T) []modelAliasEntry {
if len(models) == 0 { if len(models) == 0 {
return nil return nil
} }
out := make([]modelMappingEntry, 0, len(models)) out := make([]modelAliasEntry, 0, len(models))
for i := range models { for i := range models {
out = append(out, models[i]) out = append(out, models[i])
} }

View File

@@ -7,24 +7,24 @@ import (
"github.com/router-for-me/CLIProxyAPI/v6/internal/thinking" "github.com/router-for-me/CLIProxyAPI/v6/internal/thinking"
) )
type modelMappingEntry interface { type modelAliasEntry interface {
GetName() string GetName() string
GetAlias() string GetAlias() string
} }
type modelNameMappingTable struct { type oauthModelAliasTable struct {
// reverse maps channel -> alias (lower) -> original upstream model name. // reverse maps channel -> alias (lower) -> original upstream model name.
reverse map[string]map[string]string reverse map[string]map[string]string
} }
func compileModelNameMappingTable(mappings map[string][]internalconfig.ModelNameMapping) *modelNameMappingTable { func compileOAuthModelAliasTable(aliases map[string][]internalconfig.OAuthModelAlias) *oauthModelAliasTable {
if len(mappings) == 0 { if len(aliases) == 0 {
return &modelNameMappingTable{} return &oauthModelAliasTable{}
} }
out := &modelNameMappingTable{ out := &oauthModelAliasTable{
reverse: make(map[string]map[string]string, len(mappings)), reverse: make(map[string]map[string]string, len(aliases)),
} }
for rawChannel, entries := range mappings { for rawChannel, entries := range aliases {
channel := strings.ToLower(strings.TrimSpace(rawChannel)) channel := strings.ToLower(strings.TrimSpace(rawChannel))
if channel == "" || len(entries) == 0 { if channel == "" || len(entries) == 0 {
continue continue
@@ -55,24 +55,24 @@ func compileModelNameMappingTable(mappings map[string][]internalconfig.ModelName
return out return out
} }
// SetOAuthModelMappings updates the OAuth model name mapping table used during execution. // SetOAuthModelAlias updates the OAuth model name alias table used during execution.
// The mapping is applied per-auth channel to resolve the upstream model name while keeping the // The alias 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) SetOAuthModelMappings(mappings map[string][]internalconfig.ModelNameMapping) { func (m *Manager) SetOAuthModelAlias(aliases map[string][]internalconfig.OAuthModelAlias) {
if m == nil { if m == nil {
return return
} }
table := compileModelNameMappingTable(mappings) table := compileOAuthModelAliasTable(aliases)
// atomic.Value requires non-nil store values. // atomic.Value requires non-nil store values.
if table == nil { if table == nil {
table = &modelNameMappingTable{} table = &oauthModelAliasTable{}
} }
m.modelNameMappings.Store(table) m.oauthModelAlias.Store(table)
} }
// applyOAuthModelMapping resolves the upstream model from OAuth model mappings. // applyOAuthModelAlias resolves the upstream model from OAuth model alias.
// If a mapping exists, the returned model is the upstream model. // If an alias exists, the returned model is the upstream model.
func (m *Manager) applyOAuthModelMapping(auth *Auth, requestedModel string) string { func (m *Manager) applyOAuthModelAlias(auth *Auth, requestedModel string) string {
upstreamModel := m.resolveOAuthUpstreamModel(auth, requestedModel) upstreamModel := m.resolveOAuthUpstreamModel(auth, requestedModel)
if upstreamModel == "" { if upstreamModel == "" {
return requestedModel return requestedModel
@@ -80,7 +80,7 @@ func (m *Manager) applyOAuthModelMapping(auth *Auth, requestedModel string) stri
return upstreamModel return upstreamModel
} }
func resolveModelAliasFromConfigModels(requestedModel string, models []modelMappingEntry) string { func resolveModelAliasFromConfigModels(requestedModel string, models []modelAliasEntry) string {
requestedModel = strings.TrimSpace(requestedModel) requestedModel = strings.TrimSpace(requestedModel)
if requestedModel == "" { if requestedModel == "" {
return "" return ""
@@ -131,18 +131,18 @@ func resolveModelAliasFromConfigModels(requestedModel string, models []modelMapp
return "" return ""
} }
// resolveOAuthUpstreamModel resolves the upstream model name from OAuth model mappings. // resolveOAuthUpstreamModel resolves the upstream model name from OAuth model alias.
// If a mapping exists, returns the original (upstream) model name that corresponds // If an alias exists, returns the original (upstream) model name that corresponds
// to the requested alias. // to the requested alias.
// //
// If the requested model contains a thinking suffix (e.g., "gemini-2.5-pro(8192)"), // If the requested model contains a thinking suffix (e.g., "gemini-2.5-pro(8192)"),
// the suffix is preserved in the returned model name. However, if the mapping's // the suffix is preserved in the returned model name. However, if the alias's
// original name already contains a suffix, the config suffix takes priority. // original name already contains a suffix, the config suffix takes priority.
func (m *Manager) resolveOAuthUpstreamModel(auth *Auth, requestedModel string) string { func (m *Manager) resolveOAuthUpstreamModel(auth *Auth, requestedModel string) string {
return resolveUpstreamModelFromMappingTable(m, auth, requestedModel, modelMappingChannel(auth)) return resolveUpstreamModelFromAliasTable(m, auth, requestedModel, modelAliasChannel(auth))
} }
func resolveUpstreamModelFromMappingTable(m *Manager, auth *Auth, requestedModel, channel string) string { func resolveUpstreamModelFromAliasTable(m *Manager, auth *Auth, requestedModel, channel string) string {
if m == nil || auth == nil { if m == nil || auth == nil {
return "" return ""
} }
@@ -160,8 +160,8 @@ func resolveUpstreamModelFromMappingTable(m *Manager, auth *Auth, requestedModel
candidates = append(candidates, requestedModel) candidates = append(candidates, requestedModel)
} }
raw := m.modelNameMappings.Load() raw := m.oauthModelAlias.Load()
table, _ := raw.(*modelNameMappingTable) table, _ := raw.(*oauthModelAliasTable)
if table == nil || table.reverse == nil { if table == nil || table.reverse == nil {
return "" return ""
} }
@@ -197,10 +197,10 @@ func resolveUpstreamModelFromMappingTable(m *Manager, auth *Auth, requestedModel
return "" return ""
} }
// modelMappingChannel extracts the OAuth model mapping channel from an Auth object. // modelAliasChannel extracts the OAuth model alias channel from an Auth object.
// It determines the provider and auth kind from the Auth's attributes and delegates // It determines the provider and auth kind from the Auth's attributes and delegates
// to OAuthModelMappingChannel for the actual channel resolution. // to OAuthModelAliasChannel for the actual channel resolution.
func modelMappingChannel(auth *Auth) string { func modelAliasChannel(auth *Auth) string {
if auth == nil { if auth == nil {
return "" return ""
} }
@@ -214,20 +214,20 @@ func modelMappingChannel(auth *Auth) string {
authKind = "apikey" authKind = "apikey"
} }
} }
return OAuthModelMappingChannel(provider, authKind) return OAuthModelAliasChannel(provider, authKind)
} }
// OAuthModelMappingChannel returns the OAuth model mapping channel name for a given provider // OAuthModelAliasChannel returns the OAuth model alias channel name for a given provider
// and auth kind. Returns empty string if the provider/authKind combination doesn't support // and auth kind. Returns empty string if the provider/authKind combination doesn't support
// OAuth model mappings (e.g., API key authentication). // OAuth model alias (e.g., API key authentication).
// //
// Supported channels: gemini-cli, vertex, aistudio, antigravity, claude, codex, qwen, iflow. // Supported channels: gemini-cli, vertex, aistudio, antigravity, claude, codex, qwen, iflow.
func OAuthModelMappingChannel(provider, authKind string) string { func OAuthModelAliasChannel(provider, authKind string) string {
provider = strings.ToLower(strings.TrimSpace(provider)) provider = strings.ToLower(strings.TrimSpace(provider))
authKind = strings.ToLower(strings.TrimSpace(authKind)) authKind = strings.ToLower(strings.TrimSpace(authKind))
switch provider { switch provider {
case "gemini": case "gemini":
// gemini provider uses gemini-api-key config, not oauth-model-mappings. // gemini provider uses gemini-api-key config, not oauth-model-alias.
// OAuth-based gemini auth is converted to "gemini-cli" by the synthesizer. // OAuth-based gemini auth is converted to "gemini-cli" by the synthesizer.
return "" return ""
case "vertex": case "vertex":

View File

@@ -10,15 +10,15 @@ func TestResolveOAuthUpstreamModel_SuffixPreservation(t *testing.T) {
t.Parallel() t.Parallel()
tests := []struct { tests := []struct {
name string name string
mappings map[string][]internalconfig.ModelNameMapping aliases map[string][]internalconfig.OAuthModelAlias
channel string channel string
input string input string
want string want string
}{ }{
{ {
name: "numeric suffix preserved", name: "numeric suffix preserved",
mappings: map[string][]internalconfig.ModelNameMapping{ aliases: map[string][]internalconfig.OAuthModelAlias{
"gemini-cli": {{Name: "gemini-2.5-pro-exp-03-25", Alias: "gemini-2.5-pro"}}, "gemini-cli": {{Name: "gemini-2.5-pro-exp-03-25", Alias: "gemini-2.5-pro"}},
}, },
channel: "gemini-cli", channel: "gemini-cli",
@@ -27,7 +27,7 @@ func TestResolveOAuthUpstreamModel_SuffixPreservation(t *testing.T) {
}, },
{ {
name: "level suffix preserved", name: "level suffix preserved",
mappings: map[string][]internalconfig.ModelNameMapping{ aliases: map[string][]internalconfig.OAuthModelAlias{
"claude": {{Name: "claude-sonnet-4-5-20250514", Alias: "claude-sonnet-4-5"}}, "claude": {{Name: "claude-sonnet-4-5-20250514", Alias: "claude-sonnet-4-5"}},
}, },
channel: "claude", channel: "claude",
@@ -36,7 +36,7 @@ func TestResolveOAuthUpstreamModel_SuffixPreservation(t *testing.T) {
}, },
{ {
name: "no suffix unchanged", name: "no suffix unchanged",
mappings: map[string][]internalconfig.ModelNameMapping{ aliases: map[string][]internalconfig.OAuthModelAlias{
"gemini-cli": {{Name: "gemini-2.5-pro-exp-03-25", Alias: "gemini-2.5-pro"}}, "gemini-cli": {{Name: "gemini-2.5-pro-exp-03-25", Alias: "gemini-2.5-pro"}},
}, },
channel: "gemini-cli", channel: "gemini-cli",
@@ -45,7 +45,7 @@ func TestResolveOAuthUpstreamModel_SuffixPreservation(t *testing.T) {
}, },
{ {
name: "config suffix takes priority", name: "config suffix takes priority",
mappings: map[string][]internalconfig.ModelNameMapping{ aliases: map[string][]internalconfig.OAuthModelAlias{
"claude": {{Name: "claude-sonnet-4-5-20250514(low)", Alias: "claude-sonnet-4-5"}}, "claude": {{Name: "claude-sonnet-4-5-20250514(low)", Alias: "claude-sonnet-4-5"}},
}, },
channel: "claude", channel: "claude",
@@ -54,7 +54,7 @@ func TestResolveOAuthUpstreamModel_SuffixPreservation(t *testing.T) {
}, },
{ {
name: "auto suffix preserved", name: "auto suffix preserved",
mappings: map[string][]internalconfig.ModelNameMapping{ aliases: map[string][]internalconfig.OAuthModelAlias{
"gemini-cli": {{Name: "gemini-2.5-pro-exp-03-25", Alias: "gemini-2.5-pro"}}, "gemini-cli": {{Name: "gemini-2.5-pro-exp-03-25", Alias: "gemini-2.5-pro"}},
}, },
channel: "gemini-cli", channel: "gemini-cli",
@@ -63,7 +63,7 @@ func TestResolveOAuthUpstreamModel_SuffixPreservation(t *testing.T) {
}, },
{ {
name: "none suffix preserved", name: "none suffix preserved",
mappings: map[string][]internalconfig.ModelNameMapping{ aliases: map[string][]internalconfig.OAuthModelAlias{
"gemini-cli": {{Name: "gemini-2.5-pro-exp-03-25", Alias: "gemini-2.5-pro"}}, "gemini-cli": {{Name: "gemini-2.5-pro-exp-03-25", Alias: "gemini-2.5-pro"}},
}, },
channel: "gemini-cli", channel: "gemini-cli",
@@ -72,7 +72,7 @@ func TestResolveOAuthUpstreamModel_SuffixPreservation(t *testing.T) {
}, },
{ {
name: "case insensitive alias lookup with suffix", name: "case insensitive alias lookup with suffix",
mappings: map[string][]internalconfig.ModelNameMapping{ aliases: map[string][]internalconfig.OAuthModelAlias{
"gemini-cli": {{Name: "gemini-2.5-pro-exp-03-25", Alias: "Gemini-2.5-Pro"}}, "gemini-cli": {{Name: "gemini-2.5-pro-exp-03-25", Alias: "Gemini-2.5-Pro"}},
}, },
channel: "gemini-cli", channel: "gemini-cli",
@@ -80,8 +80,8 @@ func TestResolveOAuthUpstreamModel_SuffixPreservation(t *testing.T) {
want: "gemini-2.5-pro-exp-03-25(high)", want: "gemini-2.5-pro-exp-03-25(high)",
}, },
{ {
name: "no mapping returns empty", name: "no alias returns empty",
mappings: map[string][]internalconfig.ModelNameMapping{ aliases: map[string][]internalconfig.OAuthModelAlias{
"gemini-cli": {{Name: "gemini-2.5-pro-exp-03-25", Alias: "gemini-2.5-pro"}}, "gemini-cli": {{Name: "gemini-2.5-pro-exp-03-25", Alias: "gemini-2.5-pro"}},
}, },
channel: "gemini-cli", channel: "gemini-cli",
@@ -90,7 +90,7 @@ func TestResolveOAuthUpstreamModel_SuffixPreservation(t *testing.T) {
}, },
{ {
name: "wrong channel returns empty", name: "wrong channel returns empty",
mappings: map[string][]internalconfig.ModelNameMapping{ aliases: map[string][]internalconfig.OAuthModelAlias{
"gemini-cli": {{Name: "gemini-2.5-pro-exp-03-25", Alias: "gemini-2.5-pro"}}, "gemini-cli": {{Name: "gemini-2.5-pro-exp-03-25", Alias: "gemini-2.5-pro"}},
}, },
channel: "claude", channel: "claude",
@@ -99,7 +99,7 @@ func TestResolveOAuthUpstreamModel_SuffixPreservation(t *testing.T) {
}, },
{ {
name: "empty suffix filtered out", name: "empty suffix filtered out",
mappings: map[string][]internalconfig.ModelNameMapping{ aliases: map[string][]internalconfig.OAuthModelAlias{
"gemini-cli": {{Name: "gemini-2.5-pro-exp-03-25", Alias: "gemini-2.5-pro"}}, "gemini-cli": {{Name: "gemini-2.5-pro-exp-03-25", Alias: "gemini-2.5-pro"}},
}, },
channel: "gemini-cli", channel: "gemini-cli",
@@ -108,7 +108,7 @@ func TestResolveOAuthUpstreamModel_SuffixPreservation(t *testing.T) {
}, },
{ {
name: "incomplete suffix treated as no suffix", name: "incomplete suffix treated as no suffix",
mappings: map[string][]internalconfig.ModelNameMapping{ aliases: map[string][]internalconfig.OAuthModelAlias{
"gemini-cli": {{Name: "gemini-2.5-pro-exp-03-25", Alias: "gemini-2.5-pro(high"}}, "gemini-cli": {{Name: "gemini-2.5-pro-exp-03-25", Alias: "gemini-2.5-pro(high"}},
}, },
channel: "gemini-cli", channel: "gemini-cli",
@@ -123,7 +123,7 @@ func TestResolveOAuthUpstreamModel_SuffixPreservation(t *testing.T) {
mgr := NewManager(nil, nil, nil) mgr := NewManager(nil, nil, nil)
mgr.SetConfig(&internalconfig.Config{}) mgr.SetConfig(&internalconfig.Config{})
mgr.SetOAuthModelMappings(tt.mappings) mgr.SetOAuthModelAlias(tt.aliases)
auth := createAuthForChannel(tt.channel) auth := createAuthForChannel(tt.channel)
got := mgr.resolveOAuthUpstreamModel(auth, tt.input) got := mgr.resolveOAuthUpstreamModel(auth, tt.input)
@@ -157,21 +157,21 @@ func createAuthForChannel(channel string) *Auth {
} }
} }
func TestApplyOAuthModelMapping_SuffixPreservation(t *testing.T) { func TestApplyOAuthModelAlias_SuffixPreservation(t *testing.T) {
t.Parallel() t.Parallel()
mappings := map[string][]internalconfig.ModelNameMapping{ aliases := map[string][]internalconfig.OAuthModelAlias{
"gemini-cli": {{Name: "gemini-2.5-pro-exp-03-25", Alias: "gemini-2.5-pro"}}, "gemini-cli": {{Name: "gemini-2.5-pro-exp-03-25", Alias: "gemini-2.5-pro"}},
} }
mgr := NewManager(nil, nil, nil) mgr := NewManager(nil, nil, nil)
mgr.SetConfig(&internalconfig.Config{}) mgr.SetConfig(&internalconfig.Config{})
mgr.SetOAuthModelMappings(mappings) mgr.SetOAuthModelAlias(aliases)
auth := &Auth{ID: "test-auth-id", Provider: "gemini-cli"} auth := &Auth{ID: "test-auth-id", Provider: "gemini-cli"}
resolvedModel := mgr.applyOAuthModelMapping(auth, "gemini-2.5-pro(8192)") resolvedModel := mgr.applyOAuthModelAlias(auth, "gemini-2.5-pro(8192)")
if resolvedModel != "gemini-2.5-pro-exp-03-25(8192)" { if resolvedModel != "gemini-2.5-pro-exp-03-25(8192)" {
t.Errorf("applyOAuthModelMapping() model = %q, want %q", resolvedModel, "gemini-2.5-pro-exp-03-25(8192)") t.Errorf("applyOAuthModelAlias() model = %q, want %q", resolvedModel, "gemini-2.5-pro-exp-03-25(8192)")
} }
} }

View File

@@ -216,7 +216,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.SetConfig(b.cfg) coreManager.SetConfig(b.cfg)
coreManager.SetOAuthModelMappings(b.cfg.OAuthModelMappings) coreManager.SetOAuthModelAlias(b.cfg.OAuthModelAlias)
service := &Service{ service := &Service{
cfg: b.cfg, cfg: b.cfg,

View File

@@ -554,7 +554,7 @@ func (s *Service) Run(ctx context.Context) error {
s.cfgMu.Unlock() s.cfgMu.Unlock()
if s.coreManager != nil { if s.coreManager != nil {
s.coreManager.SetConfig(newCfg) s.coreManager.SetConfig(newCfg)
s.coreManager.SetOAuthModelMappings(newCfg.OAuthModelMappings) s.coreManager.SetOAuthModelAlias(newCfg.OAuthModelAlias)
} }
s.rebindExecutors() s.rebindExecutors()
} }
@@ -849,7 +849,7 @@ func (s *Service) registerModelsForAuth(a *coreauth.Auth) {
} }
} }
} }
models = applyOAuthModelMappings(s.cfg, provider, authKind, models) models = applyOAuthModelAlias(s.cfg, provider, authKind, models)
if len(models) > 0 { if len(models) > 0 {
key := provider key := provider
if key == "" { if key == "" {
@@ -1222,28 +1222,28 @@ func rewriteModelInfoName(name, oldID, newID string) string {
return name return name
} }
func applyOAuthModelMappings(cfg *config.Config, provider, authKind string, models []*ModelInfo) []*ModelInfo { func applyOAuthModelAlias(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 := coreauth.OAuthModelMappingChannel(provider, authKind) channel := coreauth.OAuthModelAliasChannel(provider, authKind)
if channel == "" || len(cfg.OAuthModelMappings) == 0 { if channel == "" || len(cfg.OAuthModelAlias) == 0 {
return models return models
} }
mappings := cfg.OAuthModelMappings[channel] aliases := cfg.OAuthModelAlias[channel]
if len(mappings) == 0 { if len(aliases) == 0 {
return models return models
} }
type mappingEntry struct { type aliasEntry struct {
alias string alias string
fork bool fork bool
} }
forward := make(map[string][]mappingEntry, len(mappings)) forward := make(map[string][]aliasEntry, len(aliases))
for i := range mappings { for i := range aliases {
name := strings.TrimSpace(mappings[i].Name) name := strings.TrimSpace(aliases[i].Name)
alias := strings.TrimSpace(mappings[i].Alias) alias := strings.TrimSpace(aliases[i].Alias)
if name == "" || alias == "" { if name == "" || alias == "" {
continue continue
} }
@@ -1251,7 +1251,7 @@ func applyOAuthModelMappings(cfg *config.Config, provider, authKind string, mode
continue continue
} }
key := strings.ToLower(name) key := strings.ToLower(name)
forward[key] = append(forward[key], mappingEntry{alias: alias, fork: mappings[i].Fork}) forward[key] = append(forward[key], aliasEntry{alias: alias, fork: aliases[i].Fork})
} }
if len(forward) == 0 { if len(forward) == 0 {
return models return models

View File

@@ -6,9 +6,9 @@ import (
"github.com/router-for-me/CLIProxyAPI/v6/sdk/config" "github.com/router-for-me/CLIProxyAPI/v6/sdk/config"
) )
func TestApplyOAuthModelMappings_Rename(t *testing.T) { func TestApplyOAuthModelAlias_Rename(t *testing.T) {
cfg := &config.Config{ cfg := &config.Config{
OAuthModelMappings: map[string][]config.ModelNameMapping{ OAuthModelAlias: map[string][]config.OAuthModelAlias{
"codex": { "codex": {
{Name: "gpt-5", Alias: "g5"}, {Name: "gpt-5", Alias: "g5"},
}, },
@@ -18,7 +18,7 @@ func TestApplyOAuthModelMappings_Rename(t *testing.T) {
{ID: "gpt-5", Name: "models/gpt-5"}, {ID: "gpt-5", Name: "models/gpt-5"},
} }
out := applyOAuthModelMappings(cfg, "codex", "oauth", models) out := applyOAuthModelAlias(cfg, "codex", "oauth", models)
if len(out) != 1 { if len(out) != 1 {
t.Fatalf("expected 1 model, got %d", len(out)) t.Fatalf("expected 1 model, got %d", len(out))
} }
@@ -30,9 +30,9 @@ func TestApplyOAuthModelMappings_Rename(t *testing.T) {
} }
} }
func TestApplyOAuthModelMappings_ForkAddsAlias(t *testing.T) { func TestApplyOAuthModelAlias_ForkAddsAlias(t *testing.T) {
cfg := &config.Config{ cfg := &config.Config{
OAuthModelMappings: map[string][]config.ModelNameMapping{ OAuthModelAlias: map[string][]config.OAuthModelAlias{
"codex": { "codex": {
{Name: "gpt-5", Alias: "g5", Fork: true}, {Name: "gpt-5", Alias: "g5", Fork: true},
}, },
@@ -42,7 +42,7 @@ func TestApplyOAuthModelMappings_ForkAddsAlias(t *testing.T) {
{ID: "gpt-5", Name: "models/gpt-5"}, {ID: "gpt-5", Name: "models/gpt-5"},
} }
out := applyOAuthModelMappings(cfg, "codex", "oauth", models) out := applyOAuthModelAlias(cfg, "codex", "oauth", models)
if len(out) != 2 { if len(out) != 2 {
t.Fatalf("expected 2 models, got %d", len(out)) t.Fatalf("expected 2 models, got %d", len(out))
} }
@@ -57,9 +57,9 @@ func TestApplyOAuthModelMappings_ForkAddsAlias(t *testing.T) {
} }
} }
func TestApplyOAuthModelMappings_ForkAddsMultipleAliases(t *testing.T) { func TestApplyOAuthModelAlias_ForkAddsMultipleAliases(t *testing.T) {
cfg := &config.Config{ cfg := &config.Config{
OAuthModelMappings: map[string][]config.ModelNameMapping{ OAuthModelAlias: map[string][]config.OAuthModelAlias{
"codex": { "codex": {
{Name: "gpt-5", Alias: "g5", Fork: true}, {Name: "gpt-5", Alias: "g5", Fork: true},
{Name: "gpt-5", Alias: "g5-2", Fork: true}, {Name: "gpt-5", Alias: "g5-2", Fork: true},
@@ -70,7 +70,7 @@ func TestApplyOAuthModelMappings_ForkAddsMultipleAliases(t *testing.T) {
{ID: "gpt-5", Name: "models/gpt-5"}, {ID: "gpt-5", Name: "models/gpt-5"},
} }
out := applyOAuthModelMappings(cfg, "codex", "oauth", models) out := applyOAuthModelAlias(cfg, "codex", "oauth", models)
if len(out) != 3 { if len(out) != 3 {
t.Fatalf("expected 3 models, got %d", len(out)) t.Fatalf("expected 3 models, got %d", len(out))
} }

View File

@@ -16,7 +16,7 @@ type StreamingConfig = internalconfig.StreamingConfig
type TLSConfig = internalconfig.TLSConfig type TLSConfig = internalconfig.TLSConfig
type RemoteManagement = internalconfig.RemoteManagement type RemoteManagement = internalconfig.RemoteManagement
type AmpCode = internalconfig.AmpCode type AmpCode = internalconfig.AmpCode
type ModelNameMapping = internalconfig.ModelNameMapping type OAuthModelAlias = internalconfig.OAuthModelAlias
type PayloadConfig = internalconfig.PayloadConfig type PayloadConfig = internalconfig.PayloadConfig
type PayloadRule = internalconfig.PayloadRule type PayloadRule = internalconfig.PayloadRule
type PayloadModelRule = internalconfig.PayloadModelRule type PayloadModelRule = internalconfig.PayloadModelRule