diff --git a/internal/config/config.go b/internal/config/config.go index d6e2bdc8..5a6595f7 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -516,16 +516,6 @@ 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) { - // NOTE: Startup oauth-model-alias migration is intentionally disabled. - // Reason: avoid mutating config.yaml during server startup. - // Re-enable the block below if automatic startup migration is needed again. - // 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 { @@ -1560,9 +1550,6 @@ func pruneMappingToGeneratedKeys(dstRoot, srcRoot *yaml.Node, key string) { srcIdx := findMapKeyIndex(srcRoot, key) if srcIdx < 0 { // Keep an explicit empty mapping for oauth-model-alias when it was previously present. - // - // Rationale: LoadConfig runs MigrateOAuthModelAlias before unmarshalling. If the - // oauth-model-alias key is missing, migration will add the default antigravity aliases. // When users delete the last channel from oauth-model-alias via the management API, // we want that deletion to persist across hot reloads and restarts. if key == "oauth-model-alias" { diff --git a/internal/config/oauth_model_alias_migration.go b/internal/config/oauth_model_alias_migration.go deleted file mode 100644 index 71613d03..00000000 --- a/internal/config/oauth_model_alias_migration.go +++ /dev/null @@ -1,286 +0,0 @@ -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-3.1-pro-preview": "gemini-3.1-pro-high", - "gemini-claude-sonnet-4-5": "claude-sonnet-4-6", - "gemini-claude-sonnet-4-5-thinking": "claude-sonnet-4-6-thinking", - "gemini-claude-opus-4-5-thinking": "claude-opus-4-6-thinking", - "gemini-claude-opus-4-6-thinking": "claude-opus-4-6-thinking", - "gemini-claude-sonnet-4-6": "claude-sonnet-4-6", - "claude-sonnet-4-5": "claude-sonnet-4-6", - "claude-sonnet-4-5-thinking": "claude-sonnet-4-6-thinking", - "claude-opus-4-5-thinking": "claude-opus-4-6-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: "gemini-3.1-pro-high", Alias: "gemini-3.1-pro-preview"}, - {Name: "claude-sonnet-4-6", Alias: "gemini-claude-sonnet-4-5"}, - {Name: "claude-sonnet-4-6-thinking", Alias: "gemini-claude-sonnet-4-5-thinking"}, - {Name: "claude-sonnet-4-6", Alias: "claude-sonnet-4-5"}, - {Name: "claude-sonnet-4-6-thinking", Alias: "claude-sonnet-4-5-thinking"}, - {Name: "claude-opus-4-6-thinking", Alias: "gemini-claude-opus-4-5-thinking"}, - {Name: "claude-opus-4-6-thinking", Alias: "claude-opus-4-5-thinking"}, - {Name: "claude-opus-4-6-thinking", Alias: "gemini-claude-opus-4-6-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 - } - - // For antigravity channel, supplement missing default aliases - if antigravityEntries, exists := newAliases["antigravity"]; exists { - // Build a set of already configured model names (upstream names) - configuredModels := make(map[string]bool, len(antigravityEntries)) - for _, entry := range antigravityEntries { - configuredModels[entry.Name] = true - } - - // Add missing default aliases - for _, defaultAlias := range defaultAntigravityAliases() { - if !configuredModels[defaultAlias.Name] { - antigravityEntries = append(antigravityEntries, defaultAlias) - } - } - newAliases["antigravity"] = antigravityEntries - } - - // 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 deleted file mode 100644 index cd73b9d5..00000000 --- a/internal/config/oauth_model_alias_migration_test.go +++ /dev/null @@ -1,245 +0,0 @@ -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") - } - - // Verify missing default aliases were supplemented - if !strings.Contains(content, "gemini-3-pro-image") { - t.Fatal("expected missing default alias gemini-3-pro-image to be added") - } - if !strings.Contains(content, "gemini-3-flash") { - t.Fatal("expected missing default alias gemini-3-flash to be added") - } - if !strings.Contains(content, "claude-sonnet-4-5") { - t.Fatal("expected missing default alias claude-sonnet-4-5 to be added") - } - if !strings.Contains(content, "claude-sonnet-4-5-thinking") { - t.Fatal("expected missing default alias claude-sonnet-4-5-thinking to be added") - } - if !strings.Contains(content, "claude-opus-4-5-thinking") { - t.Fatal("expected missing default alias claude-opus-4-5-thinking to be added") - } - if !strings.Contains(content, "claude-opus-4-6-thinking") { - t.Fatal("expected missing default alias claude-opus-4-6-thinking to be added") - } -} - -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") - } -}