From fe5b3c80cb3a01527573d342b92bbe91c4b13874 Mon Sep 17 00:00:00 2001 From: hkfires <10558748+hkfires@users.noreply.github.com> Date: Thu, 15 Jan 2026 18:03:26 +0800 Subject: [PATCH] refactor(config): rename oauth-model-mappings to oauth-model-alias --- config.example.yaml | 8 +- .../api/handlers/management/config_lists.go | 70 ++--- internal/api/server.go | 8 +- internal/config/config.go | 49 ++-- .../config/oauth_model_alias_migration.go | 258 ++++++++++++++++++ .../oauth_model_alias_migration_test.go | 225 +++++++++++++++ internal/config/oauth_model_alias_test.go | 56 ++++ internal/config/oauth_model_mappings_test.go | 56 ---- internal/watcher/config_reload.go | 2 +- internal/watcher/diff/config_diff.go | 2 +- ...model_mappings.go => oauth_model_alias.go} | 44 +-- ...gs_test.go => api_key_model_alias_test.go} | 14 +- sdk/cliproxy/auth/conductor.go | 78 +++--- ..._name_mappings.go => oauth_model_alias.go} | 66 ++--- ...ings_test.go => oauth_model_alias_test.go} | 46 ++-- sdk/cliproxy/builder.go | 2 +- sdk/cliproxy/service.go | 26 +- ...t.go => service_oauth_model_alias_test.go} | 18 +- sdk/config/config.go | 2 +- 19 files changed, 761 insertions(+), 269 deletions(-) create mode 100644 internal/config/oauth_model_alias_migration.go create mode 100644 internal/config/oauth_model_alias_migration_test.go create mode 100644 internal/config/oauth_model_alias_test.go delete mode 100644 internal/config/oauth_model_mappings_test.go rename internal/watcher/diff/{oauth_model_mappings.go => oauth_model_alias.go} (51%) rename sdk/cliproxy/auth/{api_key_model_mappings_test.go => api_key_model_alias_test.go} (94%) rename sdk/cliproxy/auth/{model_name_mappings.go => oauth_model_alias.go} (73%) rename sdk/cliproxy/auth/{model_name_mappings_test.go => oauth_model_alias_test.go} (77%) rename sdk/cliproxy/{service_oauth_model_mappings_test.go => service_oauth_model_alias_test.go} (77%) diff --git a/config.example.yaml b/config.example.yaml index 353b4f1b..b397be07 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -201,12 +201,12 @@ nonstream-keepalive-interval: 0 # - from: "claude-haiku-4-5-20251001" # to: "gemini-2.5-flash" -# Global OAuth model name mappings (per channel) -# These mappings rename model IDs for both model listing and request routing. +# Global OAuth model name aliases (per channel) +# These aliases rename model IDs for both model listing and request routing. # 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. -oauth-model-mappings: +oauth-model-alias: antigravity: - name: "rev19-uic3-1p" alias: "gemini-2.5-computer-use-preview-10-2025" diff --git a/internal/api/handlers/management/config_lists.go b/internal/api/handlers/management/config_lists.go index edb7a677..4e0e0284 100644 --- a/internal/api/handlers/management/config_lists.go +++ b/internal/api/handlers/management/config_lists.go @@ -703,21 +703,21 @@ func (h *Handler) DeleteOAuthExcludedModels(c *gin.Context) { h.persist(c) } -// oauth-model-mappings: map[string][]ModelNameMapping -func (h *Handler) GetOAuthModelMappings(c *gin.Context) { - c.JSON(200, gin.H{"oauth-model-mappings": sanitizedOAuthModelMappings(h.cfg.OAuthModelMappings)}) +// oauth-model-alias: map[string][]OAuthModelAlias +func (h *Handler) GetOAuthModelAlias(c *gin.Context) { + 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() if err != nil { c.JSON(400, gin.H{"error": "failed to read body"}) return } - var entries map[string][]config.ModelNameMapping + var entries map[string][]config.OAuthModelAlias if err = json.Unmarshal(data, &entries); err != nil { 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 { c.JSON(400, gin.H{"error": "invalid body"}) @@ -725,15 +725,15 @@ func (h *Handler) PutOAuthModelMappings(c *gin.Context) { } entries = wrapper.Items } - h.cfg.OAuthModelMappings = sanitizedOAuthModelMappings(entries) + h.cfg.OAuthModelAlias = sanitizedOAuthModelAlias(entries) h.persist(c) } -func (h *Handler) PatchOAuthModelMappings(c *gin.Context) { +func (h *Handler) PatchOAuthModelAlias(c *gin.Context) { var body struct { - Provider *string `json:"provider"` - Channel *string `json:"channel"` - Mappings []config.ModelNameMapping `json:"mappings"` + Provider *string `json:"provider"` + Channel *string `json:"channel"` + Aliases []config.OAuthModelAlias `json:"aliases"` } if errBindJSON := c.ShouldBindJSON(&body); errBindJSON != nil { c.JSON(400, gin.H{"error": "invalid body"}) @@ -751,32 +751,32 @@ func (h *Handler) PatchOAuthModelMappings(c *gin.Context) { return } - normalizedMap := sanitizedOAuthModelMappings(map[string][]config.ModelNameMapping{channel: body.Mappings}) + normalizedMap := sanitizedOAuthModelAlias(map[string][]config.OAuthModelAlias{channel: body.Aliases}) normalized := normalizedMap[channel] if len(normalized) == 0 { - if h.cfg.OAuthModelMappings == nil { + if h.cfg.OAuthModelAlias == nil { c.JSON(404, gin.H{"error": "channel not found"}) return } - if _, ok := h.cfg.OAuthModelMappings[channel]; !ok { + if _, ok := h.cfg.OAuthModelAlias[channel]; !ok { c.JSON(404, gin.H{"error": "channel not found"}) return } - delete(h.cfg.OAuthModelMappings, channel) - if len(h.cfg.OAuthModelMappings) == 0 { - h.cfg.OAuthModelMappings = nil + delete(h.cfg.OAuthModelAlias, channel) + if len(h.cfg.OAuthModelAlias) == 0 { + h.cfg.OAuthModelAlias = nil } h.persist(c) return } - if h.cfg.OAuthModelMappings == nil { - h.cfg.OAuthModelMappings = make(map[string][]config.ModelNameMapping) + if h.cfg.OAuthModelAlias == nil { + h.cfg.OAuthModelAlias = make(map[string][]config.OAuthModelAlias) } - h.cfg.OAuthModelMappings[channel] = normalized + h.cfg.OAuthModelAlias[channel] = normalized 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"))) if channel == "" { 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"}) return } - if h.cfg.OAuthModelMappings == nil { + if h.cfg.OAuthModelAlias == nil { c.JSON(404, gin.H{"error": "channel not found"}) return } - if _, ok := h.cfg.OAuthModelMappings[channel]; !ok { + if _, ok := h.cfg.OAuthModelAlias[channel]; !ok { c.JSON(404, gin.H{"error": "channel not found"}) return } - delete(h.cfg.OAuthModelMappings, channel) - if len(h.cfg.OAuthModelMappings) == 0 { - h.cfg.OAuthModelMappings = nil + delete(h.cfg.OAuthModelAlias, channel) + if len(h.cfg.OAuthModelAlias) == 0 { + h.cfg.OAuthModelAlias = nil } h.persist(c) } @@ -1042,26 +1042,26 @@ func normalizeVertexCompatKey(entry *config.VertexCompatKey) { 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 { return nil } - copied := make(map[string][]config.ModelNameMapping, len(entries)) - for channel, mappings := range entries { - if len(mappings) == 0 { + copied := make(map[string][]config.OAuthModelAlias, len(entries)) + for channel, aliases := range entries { + if len(aliases) == 0 { continue } - copied[channel] = append([]config.ModelNameMapping(nil), mappings...) + copied[channel] = append([]config.OAuthModelAlias(nil), aliases...) } if len(copied) == 0 { return nil } - cfg := config.Config{OAuthModelMappings: copied} - cfg.SanitizeOAuthModelMappings() - if len(cfg.OAuthModelMappings) == 0 { + cfg := config.Config{OAuthModelAlias: copied} + cfg.SanitizeOAuthModelAlias() + if len(cfg.OAuthModelAlias) == 0 { return nil } - return cfg.OAuthModelMappings + return cfg.OAuthModelAlias } // GetAmpCode returns the complete ampcode configuration. diff --git a/internal/api/server.go b/internal/api/server.go index 05bb2fee..5b425e7c 100644 --- a/internal/api/server.go +++ b/internal/api/server.go @@ -601,10 +601,10 @@ func (s *Server) registerManagementRoutes() { mgmt.PATCH("/oauth-excluded-models", s.mgmt.PatchOAuthExcludedModels) mgmt.DELETE("/oauth-excluded-models", s.mgmt.DeleteOAuthExcludedModels) - mgmt.GET("/oauth-model-mappings", s.mgmt.GetOAuthModelMappings) - mgmt.PUT("/oauth-model-mappings", s.mgmt.PutOAuthModelMappings) - mgmt.PATCH("/oauth-model-mappings", s.mgmt.PatchOAuthModelMappings) - mgmt.DELETE("/oauth-model-mappings", s.mgmt.DeleteOAuthModelMappings) + mgmt.GET("/oauth-model-alias", s.mgmt.GetOAuthModelAlias) + mgmt.PUT("/oauth-model-alias", s.mgmt.PutOAuthModelAlias) + mgmt.PATCH("/oauth-model-alias", s.mgmt.PatchOAuthModelAlias) + mgmt.DELETE("/oauth-model-alias", s.mgmt.DeleteOAuthModelAlias) mgmt.GET("/auth-files", s.mgmt.ListAuthFiles) mgmt.GET("/auth-files/models", s.mgmt.GetAuthFileModels) diff --git a/internal/config/config.go b/internal/config/config.go index effb44f5..c66229a8 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -91,13 +91,13 @@ type Config struct { // OAuthExcludedModels defines per-provider global model exclusions applied to OAuth/file-backed auth entries. OAuthExcludedModels map[string][]string `yaml:"oauth-excluded-models,omitempty" json:"oauth-excluded-models,omitempty"` - // OAuthModelMappings defines global model name mappings for OAuth/file-backed auth channels. - // These mappings affect both model listing and model routing for supported channels: + // OAuthModelAlias defines global model name aliases for OAuth/file-backed auth channels. + // These aliases 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: // 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 PayloadConfig `yaml:"payload" json:"payload"` @@ -145,11 +145,11 @@ type RoutingConfig struct { 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). // When Fork is true, the alias is added as an additional model in listings while // keeping the original model ID available. -type ModelNameMapping struct { +type OAuthModelAlias struct { Name string `yaml:"name" json:"name"` Alias string `yaml:"alias" json:"alias"` 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 empty or invalid, it returns an empty Config. 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. data, err := os.ReadFile(configFile) if err != nil { @@ -528,8 +537,8 @@ func LoadConfigOptional(configFile string, optional bool) (*Config, error) { // Normalize OAuth provider model exclusion map. cfg.OAuthExcludedModels = NormalizeOAuthExcludedModels(cfg.OAuthExcludedModels) - // Normalize global OAuth model name mappings. - cfg.SanitizeOAuthModelMappings() + // Normalize global OAuth model name aliases. + cfg.SanitizeOAuthModelAlias() if cfg.legacyMigrationPending { 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 } -// 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, // allows multiple aliases per upstream name, and ensures aliases are unique within each channel. -func (cfg *Config) SanitizeOAuthModelMappings() { - if cfg == nil || len(cfg.OAuthModelMappings) == 0 { +func (cfg *Config) SanitizeOAuthModelAlias() { + if cfg == nil || len(cfg.OAuthModelAlias) == 0 { return } - out := make(map[string][]ModelNameMapping, len(cfg.OAuthModelMappings)) - for rawChannel, mappings := range cfg.OAuthModelMappings { + out := make(map[string][]OAuthModelAlias, len(cfg.OAuthModelAlias)) + for rawChannel, aliases := range cfg.OAuthModelAlias { channel := strings.ToLower(strings.TrimSpace(rawChannel)) - if channel == "" || len(mappings) == 0 { + if channel == "" || len(aliases) == 0 { continue } - seenAlias := make(map[string]struct{}, len(mappings)) - clean := make([]ModelNameMapping, 0, len(mappings)) - for _, mapping := range mappings { - name := strings.TrimSpace(mapping.Name) - alias := strings.TrimSpace(mapping.Alias) + seenAlias := make(map[string]struct{}, len(aliases)) + clean := make([]OAuthModelAlias, 0, len(aliases)) + for _, entry := range aliases { + name := strings.TrimSpace(entry.Name) + alias := strings.TrimSpace(entry.Alias) if name == "" || alias == "" { continue } @@ -576,13 +585,13 @@ func (cfg *Config) SanitizeOAuthModelMappings() { continue } 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 { out[channel] = clean } } - cfg.OAuthModelMappings = out + cfg.OAuthModelAlias = out } // SanitizeOpenAICompatibility removes OpenAI-compatibility provider entries that are diff --git a/internal/config/oauth_model_alias_migration.go b/internal/config/oauth_model_alias_migration.go new file mode 100644 index 00000000..0e3b2156 --- /dev/null +++ b/internal/config/oauth_model_alias_migration.go @@ -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 +} diff --git a/internal/config/oauth_model_alias_migration_test.go b/internal/config/oauth_model_alias_migration_test.go new file mode 100644 index 00000000..ab5a1f49 --- /dev/null +++ b/internal/config/oauth_model_alias_migration_test.go @@ -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") + } +} diff --git a/internal/config/oauth_model_alias_test.go b/internal/config/oauth_model_alias_test.go new file mode 100644 index 00000000..a5886474 --- /dev/null +++ b/internal/config/oauth_model_alias_test.go @@ -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) + } + } +} diff --git a/internal/config/oauth_model_mappings_test.go b/internal/config/oauth_model_mappings_test.go deleted file mode 100644 index 10bfe165..00000000 --- a/internal/config/oauth_model_mappings_test.go +++ /dev/null @@ -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) - } - } -} diff --git a/internal/watcher/config_reload.go b/internal/watcher/config_reload.go index 370ee4e1..edac3474 100644 --- a/internal/watcher/config_reload.go +++ b/internal/watcher/config_reload.go @@ -127,7 +127,7 @@ func (w *Watcher) reloadConfig() bool { } 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") w.reloadClients(authDirChanged, affectedOAuthProviders, forceAuthRefresh) diff --git a/internal/watcher/diff/config_diff.go b/internal/watcher/diff/config_diff.go index fecbc242..2620f4ee 100644 --- a/internal/watcher/diff/config_diff.go +++ b/internal/watcher/diff/config_diff.go @@ -212,7 +212,7 @@ func BuildConfigChangeDetails(oldCfg, newCfg *config.Config) []string { if entries, _ := DiffOAuthExcludedModelChanges(oldCfg.OAuthExcludedModels, newCfg.OAuthExcludedModels); len(entries) > 0 { 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...) } diff --git a/internal/watcher/diff/oauth_model_mappings.go b/internal/watcher/diff/oauth_model_alias.go similarity index 51% rename from internal/watcher/diff/oauth_model_mappings.go rename to internal/watcher/diff/oauth_model_alias.go index c002855c..c5a17d29 100644 --- a/internal/watcher/diff/oauth_model_mappings.go +++ b/internal/watcher/diff/oauth_model_alias.go @@ -10,23 +10,23 @@ import ( "github.com/router-for-me/CLIProxyAPI/v6/internal/config" ) -type OAuthModelMappingsSummary struct { +type OAuthModelAliasSummary struct { hash string count int } -// SummarizeOAuthModelMappings summarizes OAuth model mappings per channel. -func SummarizeOAuthModelMappings(entries map[string][]config.ModelNameMapping) map[string]OAuthModelMappingsSummary { +// SummarizeOAuthModelAlias summarizes OAuth model alias per channel. +func SummarizeOAuthModelAlias(entries map[string][]config.OAuthModelAlias) map[string]OAuthModelAliasSummary { if len(entries) == 0 { return nil } - out := make(map[string]OAuthModelMappingsSummary, len(entries)) + out := make(map[string]OAuthModelAliasSummary, len(entries)) for k, v := range entries { key := strings.ToLower(strings.TrimSpace(k)) if key == "" { continue } - out[key] = summarizeOAuthModelMappingList(v) + out[key] = summarizeOAuthModelAliasList(v) } if len(out) == 0 { return nil @@ -34,10 +34,10 @@ func SummarizeOAuthModelMappings(entries map[string][]config.ModelNameMapping) m return out } -// DiffOAuthModelMappingChanges compares OAuth model mappings maps. -func DiffOAuthModelMappingChanges(oldMap, newMap map[string][]config.ModelNameMapping) ([]string, []string) { - oldSummary := SummarizeOAuthModelMappings(oldMap) - newSummary := SummarizeOAuthModelMappings(newMap) +// DiffOAuthModelAliasChanges compares OAuth model alias maps. +func DiffOAuthModelAliasChanges(oldMap, newMap map[string][]config.OAuthModelAlias) ([]string, []string) { + oldSummary := SummarizeOAuthModelAlias(oldMap) + newSummary := SummarizeOAuthModelAlias(newMap) keys := make(map[string]struct{}, len(oldSummary)+len(newSummary)) for k := range oldSummary { keys[k] = struct{}{} @@ -52,13 +52,13 @@ func DiffOAuthModelMappingChanges(oldMap, newMap map[string][]config.ModelNameMa newInfo, okNew := newSummary[key] switch { 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) 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) 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) } } @@ -67,20 +67,20 @@ func DiffOAuthModelMappingChanges(oldMap, newMap map[string][]config.ModelNameMa return changes, affected } -func summarizeOAuthModelMappingList(list []config.ModelNameMapping) OAuthModelMappingsSummary { +func summarizeOAuthModelAliasList(list []config.OAuthModelAlias) OAuthModelAliasSummary { if len(list) == 0 { - return OAuthModelMappingsSummary{} + return OAuthModelAliasSummary{} } seen := make(map[string]struct{}, len(list)) normalized := make([]string, 0, len(list)) - for _, mapping := range list { - name := strings.ToLower(strings.TrimSpace(mapping.Name)) - alias := strings.ToLower(strings.TrimSpace(mapping.Alias)) - if name == "" || alias == "" { + for _, alias := range list { + name := strings.ToLower(strings.TrimSpace(alias.Name)) + aliasVal := strings.ToLower(strings.TrimSpace(alias.Alias)) + if name == "" || aliasVal == "" { continue } - key := name + "->" + alias - if mapping.Fork { + key := name + "->" + aliasVal + if alias.Fork { key += "|fork" } if _, exists := seen[key]; exists { @@ -90,11 +90,11 @@ func summarizeOAuthModelMappingList(list []config.ModelNameMapping) OAuthModelMa normalized = append(normalized, key) } if len(normalized) == 0 { - return OAuthModelMappingsSummary{} + return OAuthModelAliasSummary{} } sort.Strings(normalized) sum := sha256.Sum256([]byte(strings.Join(normalized, "|"))) - return OAuthModelMappingsSummary{ + return OAuthModelAliasSummary{ hash: hex.EncodeToString(sum[:]), count: len(normalized), } diff --git a/sdk/cliproxy/auth/api_key_model_mappings_test.go b/sdk/cliproxy/auth/api_key_model_alias_test.go similarity index 94% rename from sdk/cliproxy/auth/api_key_model_mappings_test.go rename to sdk/cliproxy/auth/api_key_model_alias_test.go index 9f3bd7fe..70915d9e 100644 --- a/sdk/cliproxy/auth/api_key_model_mappings_test.go +++ b/sdk/cliproxy/auth/api_key_model_alias_test.go @@ -66,7 +66,7 @@ func TestLookupAPIKeyUpstreamModel(t *testing.T) { } } -func TestAPIKeyModelMappings_ConfigHotReload(t *testing.T) { +func TestAPIKeyModelAlias_ConfigHotReload(t *testing.T) { cfg := &internalconfig.Config{ GeminiKey: []internalconfig.GeminiKey{ { @@ -82,12 +82,12 @@ func TestAPIKeyModelMappings_ConfigHotReload(t *testing.T) { ctx := context.Background() _, _ = 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" { 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{ 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" { 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{ 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"}}}}, @@ -133,7 +133,7 @@ func TestAPIKeyModelMappings_MultipleProviders(t *testing.T) { } } -func TestApplyAPIKeyModelMapping(t *testing.T) { +func TestApplyAPIKeyModelAlias(t *testing.T) { cfg := &internalconfig.Config{ GeminiKey: []internalconfig.GeminiKey{ {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 { 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 { t.Errorf("model = %q, want %q", resolvedModel, tt.wantModel) diff --git a/sdk/cliproxy/auth/conductor.go b/sdk/cliproxy/auth/conductor.go index f7605d87..43483672 100644 --- a/sdk/cliproxy/auth/conductor.go +++ b/sdk/cliproxy/auth/conductor.go @@ -119,17 +119,17 @@ type Manager struct { requestRetry atomic.Int32 maxRetryInterval atomic.Int64 - // modelNameMappings stores global model name alias mappings (alias -> upstream name) keyed by channel. - modelNameMappings atomic.Value + // oauthModelAlias stores global OAuth model alias mappings (alias -> upstream name) keyed by channel. + 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. // It is initialized in NewManager; never Load() before first Store(). 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. rtProvider RoundTripperProvider @@ -155,7 +155,7 @@ func NewManager(store Store, selector Selector, hook Hook) *Manager { } // atomic.Value requires non-nil initial value. manager.runtimeConfig.Store(&internalconfig.Config{}) - manager.apiKeyModelMappings.Store(apiKeyModelMappingTable(nil)) + manager.apiKeyModelAlias.Store(apiKeyModelAliasTable(nil)) return manager } @@ -195,7 +195,7 @@ func (m *Manager) SetConfig(cfg *internalconfig.Config) { cfg = &internalconfig.Config{} } m.runtimeConfig.Store(cfg) - m.rebuildAPIKeyModelMappingsFromRuntimeConfig() + m.rebuildAPIKeyModelAliasFromRuntimeConfig() } func (m *Manager) lookupAPIKeyUpstreamModel(authID, requestedModel string) string { @@ -210,7 +210,7 @@ func (m *Manager) lookupAPIKeyUpstreamModel(authID, requestedModel string) strin if requestedModel == "" { return "" } - table, _ := m.apiKeyModelMappings.Load().(apiKeyModelMappingTable) + table, _ := m.apiKeyModelAlias.Load().(apiKeyModelAliasTable) if table == nil { return "" } @@ -238,7 +238,7 @@ func (m *Manager) lookupAPIKeyUpstreamModel(authID, requestedModel string) strin } -func (m *Manager) rebuildAPIKeyModelMappingsFromRuntimeConfig() { +func (m *Manager) rebuildAPIKeyModelAliasFromRuntimeConfig() { if m == nil { return } @@ -248,10 +248,10 @@ func (m *Manager) rebuildAPIKeyModelMappingsFromRuntimeConfig() { } m.mu.Lock() 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 { return } @@ -259,7 +259,7 @@ func (m *Manager) rebuildAPIKeyModelMappingsLocked(cfg *internalconfig.Config) { cfg = &internalconfig.Config{} } - out := make(apiKeyModelMappingTable) + out := make(apiKeyModelAliasTable) for _, auth := range m.auths { if auth == nil { continue @@ -277,19 +277,19 @@ func (m *Manager) rebuildAPIKeyModelMappingsLocked(cfg *internalconfig.Config) { switch provider { case "gemini": if entry := resolveGeminiAPIKeyConfig(cfg, auth); entry != nil { - compileAPIKeyModelMappingsForModels(byAlias, entry.Models) + compileAPIKeyModelAliasForModels(byAlias, entry.Models) } case "claude": if entry := resolveClaudeAPIKeyConfig(cfg, auth); entry != nil { - compileAPIKeyModelMappingsForModels(byAlias, entry.Models) + compileAPIKeyModelAliasForModels(byAlias, entry.Models) } case "codex": if entry := resolveCodexAPIKeyConfig(cfg, auth); entry != nil { - compileAPIKeyModelMappingsForModels(byAlias, entry.Models) + compileAPIKeyModelAliasForModels(byAlias, entry.Models) } case "vertex": if entry := resolveVertexAPIKeyConfig(cfg, auth); entry != nil { - compileAPIKeyModelMappingsForModels(byAlias, entry.Models) + compileAPIKeyModelAliasForModels(byAlias, entry.Models) } default: // 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 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 GetAlias() string }](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.auths[auth.ID] = auth.Clone() m.mu.Unlock() - m.rebuildAPIKeyModelMappingsFromRuntimeConfig() + m.rebuildAPIKeyModelAliasFromRuntimeConfig() _ = m.persist(ctx, auth) m.hook.OnAuthRegistered(ctx, auth.Clone()) return auth.Clone(), nil @@ -427,7 +427,7 @@ func (m *Manager) Update(ctx context.Context, auth *Auth) (*Auth, error) { auth.EnsureIndex() m.auths[auth.ID] = auth.Clone() m.mu.Unlock() - m.rebuildAPIKeyModelMappingsFromRuntimeConfig() + m.rebuildAPIKeyModelAliasFromRuntimeConfig() _ = m.persist(ctx, auth) m.hook.OnAuthUpdated(ctx, auth.Clone()) return auth.Clone(), nil @@ -456,7 +456,7 @@ func (m *Manager) Load(ctx context.Context) error { if cfg == nil { cfg = &internalconfig.Config{} } - m.rebuildAPIKeyModelMappingsLocked(cfg) + m.rebuildAPIKeyModelAliasLocked(cfg) return nil } @@ -592,8 +592,8 @@ func (m *Manager) executeMixedOnce(ctx context.Context, providers []string, req } execReq := req execReq.Model = rewriteModelForAuth(routeModel, auth) - execReq.Model = m.applyOAuthModelMapping(auth, execReq.Model) - execReq.Model = m.applyAPIKeyModelMapping(auth, execReq.Model) + execReq.Model = m.applyOAuthModelAlias(auth, execReq.Model) + execReq.Model = m.applyAPIKeyModelAlias(auth, execReq.Model) resp, errExec := executor.Execute(execCtx, auth, execReq, opts) result := Result{AuthID: auth.ID, Provider: provider, Model: routeModel, Success: errExec == nil} if errExec != nil { @@ -641,8 +641,8 @@ func (m *Manager) executeCountMixedOnce(ctx context.Context, providers []string, } execReq := req execReq.Model = rewriteModelForAuth(routeModel, auth) - execReq.Model = m.applyOAuthModelMapping(auth, execReq.Model) - execReq.Model = m.applyAPIKeyModelMapping(auth, execReq.Model) + execReq.Model = m.applyOAuthModelAlias(auth, execReq.Model) + execReq.Model = m.applyAPIKeyModelAlias(auth, execReq.Model) resp, errExec := executor.CountTokens(execCtx, auth, execReq, opts) result := Result{AuthID: auth.ID, Provider: provider, Model: routeModel, Success: errExec == nil} if errExec != nil { @@ -690,8 +690,8 @@ func (m *Manager) executeStreamMixedOnce(ctx context.Context, providers []string } execReq := req execReq.Model = rewriteModelForAuth(routeModel, auth) - execReq.Model = m.applyOAuthModelMapping(auth, execReq.Model) - execReq.Model = m.applyAPIKeyModelMapping(auth, execReq.Model) + execReq.Model = m.applyOAuthModelAlias(auth, execReq.Model) + execReq.Model = m.applyAPIKeyModelAlias(auth, execReq.Model) chunks, errStream := executor.ExecuteStream(execCtx, auth, execReq, opts) if errStream != nil { rerr := &Error{Message: errStream.Error()} @@ -756,8 +756,8 @@ func (m *Manager) executeWithProvider(ctx context.Context, provider string, req } execReq := req execReq.Model = rewriteModelForAuth(routeModel, auth) - execReq.Model = m.applyOAuthModelMapping(auth, execReq.Model) - execReq.Model = m.applyAPIKeyModelMapping(auth, execReq.Model) + execReq.Model = m.applyOAuthModelAlias(auth, execReq.Model) + execReq.Model = m.applyAPIKeyModelAlias(auth, execReq.Model) resp, errExec := executor.Execute(execCtx, auth, execReq, opts) result := Result{AuthID: auth.ID, Provider: provider, Model: routeModel, Success: errExec == nil} if errExec != nil { @@ -805,8 +805,8 @@ func (m *Manager) executeCountWithProvider(ctx context.Context, provider string, } execReq := req execReq.Model = rewriteModelForAuth(routeModel, auth) - execReq.Model = m.applyOAuthModelMapping(auth, execReq.Model) - execReq.Model = m.applyAPIKeyModelMapping(auth, execReq.Model) + execReq.Model = m.applyOAuthModelAlias(auth, execReq.Model) + execReq.Model = m.applyAPIKeyModelAlias(auth, execReq.Model) resp, errExec := executor.CountTokens(execCtx, auth, execReq, opts) result := Result{AuthID: auth.ID, Provider: provider, Model: routeModel, Success: errExec == nil} if errExec != nil { @@ -854,8 +854,8 @@ func (m *Manager) executeStreamWithProvider(ctx context.Context, provider string } execReq := req execReq.Model = rewriteModelForAuth(routeModel, auth) - execReq.Model = m.applyOAuthModelMapping(auth, execReq.Model) - execReq.Model = m.applyAPIKeyModelMapping(auth, execReq.Model) + execReq.Model = m.applyOAuthModelAlias(auth, execReq.Model) + execReq.Model = m.applyAPIKeyModelAlias(auth, execReq.Model) chunks, errStream := executor.ExecuteStream(execCtx, auth, execReq, opts) if errStream != nil { rerr := &Error{Message: errStream.Error()} @@ -908,7 +908,7 @@ func rewriteModelForAuth(model string, auth *Auth) string { 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 { return requestedModel } @@ -1079,7 +1079,7 @@ func resolveUpstreamModelForOpenAICompatAPIKey(cfg *internalconfig.Config, auth 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 { if cfg == nil { @@ -1109,11 +1109,11 @@ func resolveOpenAICompatConfig(cfg *internalconfig.Config, providerKey, compatNa func asModelAliasEntries[T interface { GetName() string GetAlias() string -}](models []T) []modelMappingEntry { +}](models []T) []modelAliasEntry { if len(models) == 0 { return nil } - out := make([]modelMappingEntry, 0, len(models)) + out := make([]modelAliasEntry, 0, len(models)) for i := range models { out = append(out, models[i]) } diff --git a/sdk/cliproxy/auth/model_name_mappings.go b/sdk/cliproxy/auth/oauth_model_alias.go similarity index 73% rename from sdk/cliproxy/auth/model_name_mappings.go rename to sdk/cliproxy/auth/oauth_model_alias.go index 24fcf50f..4111663e 100644 --- a/sdk/cliproxy/auth/model_name_mappings.go +++ b/sdk/cliproxy/auth/oauth_model_alias.go @@ -7,24 +7,24 @@ import ( "github.com/router-for-me/CLIProxyAPI/v6/internal/thinking" ) -type modelMappingEntry interface { +type modelAliasEntry interface { GetName() string GetAlias() string } -type modelNameMappingTable struct { +type oauthModelAliasTable struct { // reverse maps channel -> alias (lower) -> original upstream model name. reverse map[string]map[string]string } -func compileModelNameMappingTable(mappings map[string][]internalconfig.ModelNameMapping) *modelNameMappingTable { - if len(mappings) == 0 { - return &modelNameMappingTable{} +func compileOAuthModelAliasTable(aliases map[string][]internalconfig.OAuthModelAlias) *oauthModelAliasTable { + if len(aliases) == 0 { + return &oauthModelAliasTable{} } - out := &modelNameMappingTable{ - reverse: make(map[string]map[string]string, len(mappings)), + out := &oauthModelAliasTable{ + 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)) if channel == "" || len(entries) == 0 { continue @@ -55,24 +55,24 @@ func compileModelNameMappingTable(mappings map[string][]internalconfig.ModelName return out } -// 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 +// SetOAuthModelAlias updates the OAuth model name alias table used during execution. +// 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. -func (m *Manager) SetOAuthModelMappings(mappings map[string][]internalconfig.ModelNameMapping) { +func (m *Manager) SetOAuthModelAlias(aliases map[string][]internalconfig.OAuthModelAlias) { if m == nil { return } - table := compileModelNameMappingTable(mappings) + table := compileOAuthModelAliasTable(aliases) // atomic.Value requires non-nil store values. if table == nil { - table = &modelNameMappingTable{} + table = &oauthModelAliasTable{} } - m.modelNameMappings.Store(table) + m.oauthModelAlias.Store(table) } -// applyOAuthModelMapping resolves the upstream model from OAuth model mappings. -// If a mapping exists, the returned model is the upstream model. -func (m *Manager) applyOAuthModelMapping(auth *Auth, requestedModel string) string { +// applyOAuthModelAlias resolves the upstream model from OAuth model alias. +// If an alias exists, the returned model is the upstream model. +func (m *Manager) applyOAuthModelAlias(auth *Auth, requestedModel string) string { upstreamModel := m.resolveOAuthUpstreamModel(auth, requestedModel) if upstreamModel == "" { return requestedModel @@ -80,7 +80,7 @@ func (m *Manager) applyOAuthModelMapping(auth *Auth, requestedModel string) stri return upstreamModel } -func resolveModelAliasFromConfigModels(requestedModel string, models []modelMappingEntry) string { +func resolveModelAliasFromConfigModels(requestedModel string, models []modelAliasEntry) string { requestedModel = strings.TrimSpace(requestedModel) if requestedModel == "" { return "" @@ -131,18 +131,18 @@ func resolveModelAliasFromConfigModels(requestedModel string, models []modelMapp return "" } -// resolveOAuthUpstreamModel resolves the upstream model name from OAuth model mappings. -// If a mapping exists, returns the original (upstream) model name that corresponds +// resolveOAuthUpstreamModel resolves the upstream model name from OAuth model alias. +// If an alias exists, returns the original (upstream) model name that corresponds // to the requested alias. // // 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. 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 { return "" } @@ -160,8 +160,8 @@ func resolveUpstreamModelFromMappingTable(m *Manager, auth *Auth, requestedModel candidates = append(candidates, requestedModel) } - raw := m.modelNameMappings.Load() - table, _ := raw.(*modelNameMappingTable) + raw := m.oauthModelAlias.Load() + table, _ := raw.(*oauthModelAliasTable) if table == nil || table.reverse == nil { return "" } @@ -197,10 +197,10 @@ func resolveUpstreamModelFromMappingTable(m *Manager, auth *Auth, requestedModel 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 -// to OAuthModelMappingChannel for the actual channel resolution. -func modelMappingChannel(auth *Auth) string { +// to OAuthModelAliasChannel for the actual channel resolution. +func modelAliasChannel(auth *Auth) string { if auth == nil { return "" } @@ -214,20 +214,20 @@ func modelMappingChannel(auth *Auth) string { 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 -// 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. -func OAuthModelMappingChannel(provider, authKind string) string { +func OAuthModelAliasChannel(provider, authKind string) string { provider = strings.ToLower(strings.TrimSpace(provider)) authKind = strings.ToLower(strings.TrimSpace(authKind)) switch provider { 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. return "" case "vertex": diff --git a/sdk/cliproxy/auth/model_name_mappings_test.go b/sdk/cliproxy/auth/oauth_model_alias_test.go similarity index 77% rename from sdk/cliproxy/auth/model_name_mappings_test.go rename to sdk/cliproxy/auth/oauth_model_alias_test.go index 77f33bd6..6956411c 100644 --- a/sdk/cliproxy/auth/model_name_mappings_test.go +++ b/sdk/cliproxy/auth/oauth_model_alias_test.go @@ -10,15 +10,15 @@ func TestResolveOAuthUpstreamModel_SuffixPreservation(t *testing.T) { t.Parallel() tests := []struct { - name string - mappings map[string][]internalconfig.ModelNameMapping - channel string - input string - want string + name string + aliases map[string][]internalconfig.OAuthModelAlias + channel string + input string + want string }{ { 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"}}, }, channel: "gemini-cli", @@ -27,7 +27,7 @@ func TestResolveOAuthUpstreamModel_SuffixPreservation(t *testing.T) { }, { 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"}}, }, channel: "claude", @@ -36,7 +36,7 @@ func TestResolveOAuthUpstreamModel_SuffixPreservation(t *testing.T) { }, { 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"}}, }, channel: "gemini-cli", @@ -45,7 +45,7 @@ func TestResolveOAuthUpstreamModel_SuffixPreservation(t *testing.T) { }, { 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"}}, }, channel: "claude", @@ -54,7 +54,7 @@ func TestResolveOAuthUpstreamModel_SuffixPreservation(t *testing.T) { }, { 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"}}, }, channel: "gemini-cli", @@ -63,7 +63,7 @@ func TestResolveOAuthUpstreamModel_SuffixPreservation(t *testing.T) { }, { 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"}}, }, channel: "gemini-cli", @@ -72,7 +72,7 @@ func TestResolveOAuthUpstreamModel_SuffixPreservation(t *testing.T) { }, { 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"}}, }, channel: "gemini-cli", @@ -80,8 +80,8 @@ func TestResolveOAuthUpstreamModel_SuffixPreservation(t *testing.T) { want: "gemini-2.5-pro-exp-03-25(high)", }, { - name: "no mapping returns empty", - mappings: map[string][]internalconfig.ModelNameMapping{ + name: "no alias returns empty", + aliases: map[string][]internalconfig.OAuthModelAlias{ "gemini-cli": {{Name: "gemini-2.5-pro-exp-03-25", Alias: "gemini-2.5-pro"}}, }, channel: "gemini-cli", @@ -90,7 +90,7 @@ func TestResolveOAuthUpstreamModel_SuffixPreservation(t *testing.T) { }, { 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"}}, }, channel: "claude", @@ -99,7 +99,7 @@ func TestResolveOAuthUpstreamModel_SuffixPreservation(t *testing.T) { }, { 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"}}, }, channel: "gemini-cli", @@ -108,7 +108,7 @@ func TestResolveOAuthUpstreamModel_SuffixPreservation(t *testing.T) { }, { 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"}}, }, channel: "gemini-cli", @@ -123,7 +123,7 @@ func TestResolveOAuthUpstreamModel_SuffixPreservation(t *testing.T) { mgr := NewManager(nil, nil, nil) mgr.SetConfig(&internalconfig.Config{}) - mgr.SetOAuthModelMappings(tt.mappings) + mgr.SetOAuthModelAlias(tt.aliases) auth := createAuthForChannel(tt.channel) 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() - 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"}}, } mgr := NewManager(nil, nil, nil) mgr.SetConfig(&internalconfig.Config{}) - mgr.SetOAuthModelMappings(mappings) + mgr.SetOAuthModelAlias(aliases) 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)" { - 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)") } } diff --git a/sdk/cliproxy/builder.go b/sdk/cliproxy/builder.go index 2e2427f9..5eba18a0 100644 --- a/sdk/cliproxy/builder.go +++ b/sdk/cliproxy/builder.go @@ -216,7 +216,7 @@ func (b *Builder) Build() (*Service, error) { // Attach a default RoundTripper provider so providers can opt-in per-auth transports. coreManager.SetRoundTripperProvider(newDefaultRoundTripperProvider()) coreManager.SetConfig(b.cfg) - coreManager.SetOAuthModelMappings(b.cfg.OAuthModelMappings) + coreManager.SetOAuthModelAlias(b.cfg.OAuthModelAlias) service := &Service{ cfg: b.cfg, diff --git a/sdk/cliproxy/service.go b/sdk/cliproxy/service.go index 71603479..7a06ae78 100644 --- a/sdk/cliproxy/service.go +++ b/sdk/cliproxy/service.go @@ -554,7 +554,7 @@ func (s *Service) Run(ctx context.Context) error { s.cfgMu.Unlock() if s.coreManager != nil { s.coreManager.SetConfig(newCfg) - s.coreManager.SetOAuthModelMappings(newCfg.OAuthModelMappings) + s.coreManager.SetOAuthModelAlias(newCfg.OAuthModelAlias) } 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 { key := provider if key == "" { @@ -1222,28 +1222,28 @@ func rewriteModelInfoName(name, oldID, newID string) string { 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 { return models } - channel := coreauth.OAuthModelMappingChannel(provider, authKind) - if channel == "" || len(cfg.OAuthModelMappings) == 0 { + channel := coreauth.OAuthModelAliasChannel(provider, authKind) + if channel == "" || len(cfg.OAuthModelAlias) == 0 { return models } - mappings := cfg.OAuthModelMappings[channel] - if len(mappings) == 0 { + aliases := cfg.OAuthModelAlias[channel] + if len(aliases) == 0 { return models } - type mappingEntry struct { + type aliasEntry struct { alias string fork bool } - forward := make(map[string][]mappingEntry, len(mappings)) - for i := range mappings { - name := strings.TrimSpace(mappings[i].Name) - alias := strings.TrimSpace(mappings[i].Alias) + forward := make(map[string][]aliasEntry, len(aliases)) + for i := range aliases { + name := strings.TrimSpace(aliases[i].Name) + alias := strings.TrimSpace(aliases[i].Alias) if name == "" || alias == "" { continue } @@ -1251,7 +1251,7 @@ func applyOAuthModelMappings(cfg *config.Config, provider, authKind string, mode continue } 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 { return models diff --git a/sdk/cliproxy/service_oauth_model_mappings_test.go b/sdk/cliproxy/service_oauth_model_alias_test.go similarity index 77% rename from sdk/cliproxy/service_oauth_model_mappings_test.go rename to sdk/cliproxy/service_oauth_model_alias_test.go index ca9ff35a..2caf7a17 100644 --- a/sdk/cliproxy/service_oauth_model_mappings_test.go +++ b/sdk/cliproxy/service_oauth_model_alias_test.go @@ -6,9 +6,9 @@ import ( "github.com/router-for-me/CLIProxyAPI/v6/sdk/config" ) -func TestApplyOAuthModelMappings_Rename(t *testing.T) { +func TestApplyOAuthModelAlias_Rename(t *testing.T) { cfg := &config.Config{ - OAuthModelMappings: map[string][]config.ModelNameMapping{ + OAuthModelAlias: map[string][]config.OAuthModelAlias{ "codex": { {Name: "gpt-5", Alias: "g5"}, }, @@ -18,7 +18,7 @@ func TestApplyOAuthModelMappings_Rename(t *testing.T) { {ID: "gpt-5", Name: "models/gpt-5"}, } - out := applyOAuthModelMappings(cfg, "codex", "oauth", models) + out := applyOAuthModelAlias(cfg, "codex", "oauth", models) if len(out) != 1 { 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{ - OAuthModelMappings: map[string][]config.ModelNameMapping{ + OAuthModelAlias: map[string][]config.OAuthModelAlias{ "codex": { {Name: "gpt-5", Alias: "g5", Fork: true}, }, @@ -42,7 +42,7 @@ func TestApplyOAuthModelMappings_ForkAddsAlias(t *testing.T) { {ID: "gpt-5", Name: "models/gpt-5"}, } - out := applyOAuthModelMappings(cfg, "codex", "oauth", models) + out := applyOAuthModelAlias(cfg, "codex", "oauth", models) if len(out) != 2 { 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{ - OAuthModelMappings: map[string][]config.ModelNameMapping{ + OAuthModelAlias: map[string][]config.OAuthModelAlias{ "codex": { {Name: "gpt-5", Alias: "g5", 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"}, } - out := applyOAuthModelMappings(cfg, "codex", "oauth", models) + out := applyOAuthModelAlias(cfg, "codex", "oauth", models) if len(out) != 3 { t.Fatalf("expected 3 models, got %d", len(out)) } diff --git a/sdk/config/config.go b/sdk/config/config.go index 1ae7ba20..304ccdd8 100644 --- a/sdk/config/config.go +++ b/sdk/config/config.go @@ -16,7 +16,7 @@ type StreamingConfig = internalconfig.StreamingConfig type TLSConfig = internalconfig.TLSConfig type RemoteManagement = internalconfig.RemoteManagement type AmpCode = internalconfig.AmpCode -type ModelNameMapping = internalconfig.ModelNameMapping +type OAuthModelAlias = internalconfig.OAuthModelAlias type PayloadConfig = internalconfig.PayloadConfig type PayloadRule = internalconfig.PayloadRule type PayloadModelRule = internalconfig.PayloadModelRule