| |
| |
| |
| |
| package config |
|
|
| import ( |
| "bytes" |
| "encoding/json" |
| "errors" |
| "fmt" |
| "os" |
| "strings" |
| "syscall" |
|
|
| log "github.com/sirupsen/logrus" |
| "golang.org/x/crypto/bcrypt" |
| "gopkg.in/yaml.v3" |
| ) |
|
|
| const DefaultPanelGitHubRepository = "https://github.com/router-for-me/Cli-Proxy-API-Management-Center" |
|
|
| |
| type Config struct { |
| SDKConfig `yaml:",inline"` |
| |
| |
| Host string `yaml:"host" json:"-"` |
| |
| Port int `yaml:"port" json:"-"` |
|
|
| |
| TLS TLSConfig `yaml:"tls" json:"tls"` |
|
|
| |
| RemoteManagement RemoteManagement `yaml:"remote-management" json:"-"` |
|
|
| |
| AuthDir string `yaml:"auth-dir" json:"-"` |
|
|
| |
| Debug bool `yaml:"debug" json:"debug"` |
|
|
| |
| CommercialMode bool `yaml:"commercial-mode" json:"commercial-mode"` |
|
|
| |
| LoggingToFile bool `yaml:"logging-to-file" json:"logging-to-file"` |
|
|
| |
| |
| LogsMaxTotalSizeMB int `yaml:"logs-max-total-size-mb" json:"logs-max-total-size-mb"` |
|
|
| |
| UsageStatisticsEnabled bool `yaml:"usage-statistics-enabled" json:"usage-statistics-enabled"` |
|
|
| |
| DisableCooling bool `yaml:"disable-cooling" json:"disable-cooling"` |
|
|
| |
| RequestRetry int `yaml:"request-retry" json:"request-retry"` |
| |
| MaxRetryInterval int `yaml:"max-retry-interval" json:"max-retry-interval"` |
|
|
| |
| QuotaExceeded QuotaExceeded `yaml:"quota-exceeded" json:"quota-exceeded"` |
|
|
| |
| Routing RoutingConfig `yaml:"routing" json:"routing"` |
|
|
| |
| WebsocketAuth bool `yaml:"ws-auth" json:"ws-auth"` |
|
|
| |
| |
| |
| CodexInstructionsEnabled bool `yaml:"codex-instructions-enabled" json:"codex-instructions-enabled"` |
|
|
| |
| GeminiKey []GeminiKey `yaml:"gemini-api-key" json:"gemini-api-key"` |
|
|
| |
| CodexKey []CodexKey `yaml:"codex-api-key" json:"codex-api-key"` |
|
|
| |
| ClaudeKey []ClaudeKey `yaml:"claude-api-key" json:"claude-api-key"` |
|
|
| |
| OpenAICompatibility []OpenAICompatibility `yaml:"openai-compatibility" json:"openai-compatibility"` |
|
|
| |
| |
| VertexCompatAPIKey []VertexCompatKey `yaml:"vertex-api-key" json:"vertex-api-key"` |
|
|
| |
| AmpCode AmpCode `yaml:"ampcode" json:"ampcode"` |
|
|
| |
| OAuthExcludedModels map[string][]string `yaml:"oauth-excluded-models,omitempty" json:"oauth-excluded-models,omitempty"` |
|
|
| |
| |
| |
| |
| |
| |
| OAuthModelAlias map[string][]OAuthModelAlias `yaml:"oauth-model-alias,omitempty" json:"oauth-model-alias,omitempty"` |
|
|
| |
| Payload PayloadConfig `yaml:"payload" json:"payload"` |
|
|
| legacyMigrationPending bool `yaml:"-" json:"-"` |
| } |
|
|
| |
| type TLSConfig struct { |
| |
| Enable bool `yaml:"enable" json:"enable"` |
| |
| Cert string `yaml:"cert" json:"cert"` |
| |
| Key string `yaml:"key" json:"key"` |
| } |
|
|
| |
| type RemoteManagement struct { |
| |
| AllowRemote bool `yaml:"allow-remote"` |
| |
| SecretKey string `yaml:"secret-key"` |
| |
| DisableControlPanel bool `yaml:"disable-control-panel"` |
| |
| |
| PanelGitHubRepository string `yaml:"panel-github-repository"` |
| } |
|
|
| |
| |
| type QuotaExceeded struct { |
| |
| SwitchProject bool `yaml:"switch-project" json:"switch-project"` |
|
|
| |
| SwitchPreviewModel bool `yaml:"switch-preview-model" json:"switch-preview-model"` |
| } |
|
|
| |
| type RoutingConfig struct { |
| |
| |
| Strategy string `yaml:"strategy,omitempty" json:"strategy,omitempty"` |
| } |
|
|
| |
| |
| |
| |
| type OAuthModelAlias struct { |
| Name string `yaml:"name" json:"name"` |
| Alias string `yaml:"alias" json:"alias"` |
| Fork bool `yaml:"fork,omitempty" json:"fork,omitempty"` |
| } |
|
|
| |
| |
| |
| type AmpModelMapping struct { |
| |
| From string `yaml:"from" json:"from"` |
|
|
| |
| |
| To string `yaml:"to" json:"to"` |
|
|
| |
| |
| |
| Regex bool `yaml:"regex,omitempty" json:"regex,omitempty"` |
| } |
|
|
| |
| |
| type AmpCode struct { |
| |
| UpstreamURL string `yaml:"upstream-url" json:"upstream-url"` |
|
|
| |
| UpstreamAPIKey string `yaml:"upstream-api-key" json:"upstream-api-key"` |
|
|
| |
| |
| |
| UpstreamAPIKeys []AmpUpstreamAPIKeyEntry `yaml:"upstream-api-keys,omitempty" json:"upstream-api-keys,omitempty"` |
|
|
| |
| |
| |
| RestrictManagementToLocalhost bool `yaml:"restrict-management-to-localhost" json:"restrict-management-to-localhost"` |
|
|
| |
| |
| |
| ModelMappings []AmpModelMapping `yaml:"model-mappings" json:"model-mappings"` |
|
|
| |
| |
| ForceModelMappings bool `yaml:"force-model-mappings" json:"force-model-mappings"` |
| } |
|
|
| |
| |
| |
| type AmpUpstreamAPIKeyEntry struct { |
| |
| UpstreamAPIKey string `yaml:"upstream-api-key" json:"upstream-api-key"` |
|
|
| |
| APIKeys []string `yaml:"api-keys" json:"api-keys"` |
| } |
|
|
| |
| type PayloadConfig struct { |
| |
| Default []PayloadRule `yaml:"default" json:"default"` |
| |
| DefaultRaw []PayloadRule `yaml:"default-raw" json:"default-raw"` |
| |
| Override []PayloadRule `yaml:"override" json:"override"` |
| |
| OverrideRaw []PayloadRule `yaml:"override-raw" json:"override-raw"` |
| } |
|
|
| |
| type PayloadRule struct { |
| |
| Models []PayloadModelRule `yaml:"models" json:"models"` |
| |
| |
| Params map[string]any `yaml:"params" json:"params"` |
| } |
|
|
| |
| type PayloadModelRule struct { |
| |
| Name string `yaml:"name" json:"name"` |
| |
| Protocol string `yaml:"protocol" json:"protocol"` |
| } |
|
|
| |
| |
| type CloakConfig struct { |
| |
| |
| |
| |
| Mode string `yaml:"mode,omitempty" json:"mode,omitempty"` |
|
|
| |
| |
| |
| StrictMode bool `yaml:"strict-mode,omitempty" json:"strict-mode,omitempty"` |
|
|
| |
| |
| SensitiveWords []string `yaml:"sensitive-words,omitempty" json:"sensitive-words,omitempty"` |
| } |
|
|
| |
| |
| type ClaudeKey struct { |
| |
| APIKey string `yaml:"api-key" json:"api-key"` |
|
|
| |
| |
| Priority int `yaml:"priority,omitempty" json:"priority,omitempty"` |
|
|
| |
| Prefix string `yaml:"prefix,omitempty" json:"prefix,omitempty"` |
|
|
| |
| |
| BaseURL string `yaml:"base-url" json:"base-url"` |
|
|
| |
| ProxyURL string `yaml:"proxy-url" json:"proxy-url"` |
|
|
| |
| Models []ClaudeModel `yaml:"models" json:"models"` |
|
|
| |
| Headers map[string]string `yaml:"headers,omitempty" json:"headers,omitempty"` |
|
|
| |
| ExcludedModels []string `yaml:"excluded-models,omitempty" json:"excluded-models,omitempty"` |
|
|
| |
| Cloak *CloakConfig `yaml:"cloak,omitempty" json:"cloak,omitempty"` |
| } |
|
|
| func (k ClaudeKey) GetAPIKey() string { return k.APIKey } |
| func (k ClaudeKey) GetBaseURL() string { return k.BaseURL } |
|
|
| |
| type ClaudeModel struct { |
| |
| Name string `yaml:"name" json:"name"` |
|
|
| |
| Alias string `yaml:"alias" json:"alias"` |
| } |
|
|
| func (m ClaudeModel) GetName() string { return m.Name } |
| func (m ClaudeModel) GetAlias() string { return m.Alias } |
|
|
| |
| |
| type CodexKey struct { |
| |
| APIKey string `yaml:"api-key" json:"api-key"` |
|
|
| |
| |
| Priority int `yaml:"priority,omitempty" json:"priority,omitempty"` |
|
|
| |
| Prefix string `yaml:"prefix,omitempty" json:"prefix,omitempty"` |
|
|
| |
| |
| BaseURL string `yaml:"base-url" json:"base-url"` |
|
|
| |
| ProxyURL string `yaml:"proxy-url" json:"proxy-url"` |
|
|
| |
| Models []CodexModel `yaml:"models" json:"models"` |
|
|
| |
| Headers map[string]string `yaml:"headers,omitempty" json:"headers,omitempty"` |
|
|
| |
| ExcludedModels []string `yaml:"excluded-models,omitempty" json:"excluded-models,omitempty"` |
| } |
|
|
| func (k CodexKey) GetAPIKey() string { return k.APIKey } |
| func (k CodexKey) GetBaseURL() string { return k.BaseURL } |
|
|
| |
| type CodexModel struct { |
| |
| Name string `yaml:"name" json:"name"` |
|
|
| |
| Alias string `yaml:"alias" json:"alias"` |
| } |
|
|
| func (m CodexModel) GetName() string { return m.Name } |
| func (m CodexModel) GetAlias() string { return m.Alias } |
|
|
| |
| |
| type GeminiKey struct { |
| |
| APIKey string `yaml:"api-key" json:"api-key"` |
|
|
| |
| |
| Priority int `yaml:"priority,omitempty" json:"priority,omitempty"` |
|
|
| |
| Prefix string `yaml:"prefix,omitempty" json:"prefix,omitempty"` |
|
|
| |
| BaseURL string `yaml:"base-url,omitempty" json:"base-url,omitempty"` |
|
|
| |
| ProxyURL string `yaml:"proxy-url,omitempty" json:"proxy-url,omitempty"` |
|
|
| |
| Models []GeminiModel `yaml:"models,omitempty" json:"models,omitempty"` |
|
|
| |
| Headers map[string]string `yaml:"headers,omitempty" json:"headers,omitempty"` |
|
|
| |
| ExcludedModels []string `yaml:"excluded-models,omitempty" json:"excluded-models,omitempty"` |
| } |
|
|
| func (k GeminiKey) GetAPIKey() string { return k.APIKey } |
| func (k GeminiKey) GetBaseURL() string { return k.BaseURL } |
|
|
| |
| type GeminiModel struct { |
| |
| Name string `yaml:"name" json:"name"` |
|
|
| |
| Alias string `yaml:"alias" json:"alias"` |
| } |
|
|
| func (m GeminiModel) GetName() string { return m.Name } |
| func (m GeminiModel) GetAlias() string { return m.Alias } |
|
|
| |
| |
| type OpenAICompatibility struct { |
| |
| Name string `yaml:"name" json:"name"` |
|
|
| |
| |
| Priority int `yaml:"priority,omitempty" json:"priority,omitempty"` |
|
|
| |
| Prefix string `yaml:"prefix,omitempty" json:"prefix,omitempty"` |
|
|
| |
| BaseURL string `yaml:"base-url" json:"base-url"` |
|
|
| |
| APIKeyEntries []OpenAICompatibilityAPIKey `yaml:"api-key-entries,omitempty" json:"api-key-entries,omitempty"` |
|
|
| |
| Models []OpenAICompatibilityModel `yaml:"models" json:"models"` |
|
|
| |
| Headers map[string]string `yaml:"headers,omitempty" json:"headers,omitempty"` |
| } |
|
|
| |
| type OpenAICompatibilityAPIKey struct { |
| |
| APIKey string `yaml:"api-key" json:"api-key"` |
|
|
| |
| ProxyURL string `yaml:"proxy-url,omitempty" json:"proxy-url,omitempty"` |
| } |
|
|
| |
| |
| type OpenAICompatibilityModel struct { |
| |
| Name string `yaml:"name" json:"name"` |
|
|
| |
| Alias string `yaml:"alias" json:"alias"` |
| } |
|
|
| func (m OpenAICompatibilityModel) GetName() string { return m.Name } |
| func (m OpenAICompatibilityModel) GetAlias() string { return m.Alias } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| func LoadConfig(configFile string) (*Config, error) { |
| return LoadConfigOptional(configFile, false) |
| } |
|
|
| |
| |
| |
| func LoadConfigOptional(configFile string, optional bool) (*Config, error) { |
| |
| |
| if migrated, err := MigrateOAuthModelAlias(configFile); err != nil { |
| |
| fmt.Printf("Warning: oauth-model-alias migration failed: %v\n", err) |
| } else if migrated { |
| fmt.Println("Migrated oauth-model-mappings to oauth-model-alias") |
| } |
|
|
| |
| data, err := os.ReadFile(configFile) |
| if err != nil { |
| if optional { |
| if os.IsNotExist(err) || errors.Is(err, syscall.EISDIR) { |
| |
| return &Config{}, nil |
| } |
| } |
| return nil, fmt.Errorf("failed to read config file: %w", err) |
| } |
|
|
| |
| if optional && len(data) == 0 { |
| return &Config{}, nil |
| } |
|
|
| |
| var cfg Config |
| |
| cfg.Host = "" |
| cfg.LoggingToFile = false |
| cfg.LogsMaxTotalSizeMB = 0 |
| cfg.UsageStatisticsEnabled = false |
| cfg.DisableCooling = false |
| cfg.AmpCode.RestrictManagementToLocalhost = false |
| cfg.RemoteManagement.PanelGitHubRepository = DefaultPanelGitHubRepository |
| if err = yaml.Unmarshal(data, &cfg); err != nil { |
| if optional { |
| |
| return &Config{}, nil |
| } |
| return nil, fmt.Errorf("failed to parse config file: %w", err) |
| } |
|
|
| var legacy legacyConfigData |
| if errLegacy := yaml.Unmarshal(data, &legacy); errLegacy == nil { |
| if cfg.migrateLegacyGeminiKeys(legacy.LegacyGeminiKeys) { |
| cfg.legacyMigrationPending = true |
| } |
| if cfg.migrateLegacyOpenAICompatibilityKeys(legacy.OpenAICompat) { |
| cfg.legacyMigrationPending = true |
| } |
| if cfg.migrateLegacyAmpConfig(&legacy) { |
| cfg.legacyMigrationPending = true |
| } |
| } |
|
|
| |
| |
| if cfg.RemoteManagement.SecretKey != "" && !looksLikeBcrypt(cfg.RemoteManagement.SecretKey) { |
| hashed, errHash := hashSecret(cfg.RemoteManagement.SecretKey) |
| if errHash != nil { |
| return nil, fmt.Errorf("failed to hash remote management key: %w", errHash) |
| } |
| cfg.RemoteManagement.SecretKey = hashed |
|
|
| |
| |
| _ = SaveConfigPreserveCommentsUpdateNestedScalar(configFile, []string{"remote-management", "secret-key"}, hashed) |
| } |
|
|
| cfg.RemoteManagement.PanelGitHubRepository = strings.TrimSpace(cfg.RemoteManagement.PanelGitHubRepository) |
| if cfg.RemoteManagement.PanelGitHubRepository == "" { |
| cfg.RemoteManagement.PanelGitHubRepository = DefaultPanelGitHubRepository |
| } |
|
|
| if cfg.LogsMaxTotalSizeMB < 0 { |
| cfg.LogsMaxTotalSizeMB = 0 |
| } |
|
|
| |
| syncInlineAccessProvider(&cfg) |
|
|
| |
| cfg.SanitizeGeminiKeys() |
|
|
| |
| cfg.SanitizeVertexCompatKeys() |
|
|
| |
| cfg.SanitizeCodexKeys() |
|
|
| |
| cfg.SanitizeClaudeKeys() |
|
|
| |
| cfg.SanitizeOpenAICompatibility() |
|
|
| |
| cfg.OAuthExcludedModels = NormalizeOAuthExcludedModels(cfg.OAuthExcludedModels) |
|
|
| |
| cfg.SanitizeOAuthModelAlias() |
|
|
| |
| cfg.SanitizePayloadRules() |
|
|
| if cfg.legacyMigrationPending { |
| fmt.Println("Detected legacy configuration keys, attempting to persist the normalized config...") |
| if !optional && configFile != "" { |
| if err := SaveConfigPreserveComments(configFile, &cfg); err != nil { |
| return nil, fmt.Errorf("failed to persist migrated legacy config: %w", err) |
| } |
| fmt.Println("Legacy configuration normalized and persisted.") |
| } else { |
| fmt.Println("Legacy configuration normalized in memory; persistence skipped.") |
| } |
| } |
|
|
| |
| return &cfg, nil |
| } |
|
|
| |
| func (cfg *Config) SanitizePayloadRules() { |
| if cfg == nil { |
| return |
| } |
| cfg.Payload.DefaultRaw = sanitizePayloadRawRules(cfg.Payload.DefaultRaw, "default-raw") |
| cfg.Payload.OverrideRaw = sanitizePayloadRawRules(cfg.Payload.OverrideRaw, "override-raw") |
| } |
|
|
| func sanitizePayloadRawRules(rules []PayloadRule, section string) []PayloadRule { |
| if len(rules) == 0 { |
| return rules |
| } |
| out := make([]PayloadRule, 0, len(rules)) |
| for i := range rules { |
| rule := rules[i] |
| if len(rule.Params) == 0 { |
| continue |
| } |
| invalid := false |
| for path, value := range rule.Params { |
| raw, ok := payloadRawString(value) |
| if !ok { |
| continue |
| } |
| trimmed := bytes.TrimSpace(raw) |
| if len(trimmed) == 0 || !json.Valid(trimmed) { |
| log.WithFields(log.Fields{ |
| "section": section, |
| "rule_index": i + 1, |
| "param": path, |
| }).Warn("payload rule dropped: invalid raw JSON") |
| invalid = true |
| break |
| } |
| } |
| if invalid { |
| continue |
| } |
| out = append(out, rule) |
| } |
| return out |
| } |
|
|
| func payloadRawString(value any) ([]byte, bool) { |
| switch typed := value.(type) { |
| case string: |
| return []byte(typed), true |
| case []byte: |
| return typed, true |
| default: |
| return nil, false |
| } |
| } |
|
|
| |
| |
| |
| func (cfg *Config) SanitizeOAuthModelAlias() { |
| if cfg == nil || len(cfg.OAuthModelAlias) == 0 { |
| return |
| } |
| out := make(map[string][]OAuthModelAlias, len(cfg.OAuthModelAlias)) |
| for rawChannel, aliases := range cfg.OAuthModelAlias { |
| channel := strings.ToLower(strings.TrimSpace(rawChannel)) |
| if channel == "" || len(aliases) == 0 { |
| continue |
| } |
| 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 |
| } |
| if strings.EqualFold(name, alias) { |
| continue |
| } |
| aliasKey := strings.ToLower(alias) |
| if _, ok := seenAlias[aliasKey]; ok { |
| continue |
| } |
| seenAlias[aliasKey] = struct{}{} |
| clean = append(clean, OAuthModelAlias{Name: name, Alias: alias, Fork: entry.Fork}) |
| } |
| if len(clean) > 0 { |
| out[channel] = clean |
| } |
| } |
| cfg.OAuthModelAlias = out |
| } |
|
|
| |
| |
| |
| func (cfg *Config) SanitizeOpenAICompatibility() { |
| if cfg == nil || len(cfg.OpenAICompatibility) == 0 { |
| return |
| } |
| out := make([]OpenAICompatibility, 0, len(cfg.OpenAICompatibility)) |
| for i := range cfg.OpenAICompatibility { |
| e := cfg.OpenAICompatibility[i] |
| e.Name = strings.TrimSpace(e.Name) |
| e.Prefix = normalizeModelPrefix(e.Prefix) |
| e.BaseURL = strings.TrimSpace(e.BaseURL) |
| e.Headers = NormalizeHeaders(e.Headers) |
| if e.BaseURL == "" { |
| |
| continue |
| } |
| out = append(out, e) |
| } |
| cfg.OpenAICompatibility = out |
| } |
|
|
| |
| |
| func (cfg *Config) SanitizeCodexKeys() { |
| if cfg == nil || len(cfg.CodexKey) == 0 { |
| return |
| } |
| out := make([]CodexKey, 0, len(cfg.CodexKey)) |
| for i := range cfg.CodexKey { |
| e := cfg.CodexKey[i] |
| e.Prefix = normalizeModelPrefix(e.Prefix) |
| e.BaseURL = strings.TrimSpace(e.BaseURL) |
| e.Headers = NormalizeHeaders(e.Headers) |
| e.ExcludedModels = NormalizeExcludedModels(e.ExcludedModels) |
| if e.BaseURL == "" { |
| continue |
| } |
| out = append(out, e) |
| } |
| cfg.CodexKey = out |
| } |
|
|
| |
| func (cfg *Config) SanitizeClaudeKeys() { |
| if cfg == nil || len(cfg.ClaudeKey) == 0 { |
| return |
| } |
| for i := range cfg.ClaudeKey { |
| entry := &cfg.ClaudeKey[i] |
| entry.Prefix = normalizeModelPrefix(entry.Prefix) |
| entry.Headers = NormalizeHeaders(entry.Headers) |
| entry.ExcludedModels = NormalizeExcludedModels(entry.ExcludedModels) |
| } |
| } |
|
|
| |
| func (cfg *Config) SanitizeGeminiKeys() { |
| if cfg == nil { |
| return |
| } |
|
|
| seen := make(map[string]struct{}, len(cfg.GeminiKey)) |
| out := cfg.GeminiKey[:0] |
| for i := range cfg.GeminiKey { |
| entry := cfg.GeminiKey[i] |
| entry.APIKey = strings.TrimSpace(entry.APIKey) |
| if entry.APIKey == "" { |
| continue |
| } |
| entry.Prefix = normalizeModelPrefix(entry.Prefix) |
| entry.BaseURL = strings.TrimSpace(entry.BaseURL) |
| entry.ProxyURL = strings.TrimSpace(entry.ProxyURL) |
| entry.Headers = NormalizeHeaders(entry.Headers) |
| entry.ExcludedModels = NormalizeExcludedModels(entry.ExcludedModels) |
| if _, exists := seen[entry.APIKey]; exists { |
| continue |
| } |
| seen[entry.APIKey] = struct{}{} |
| out = append(out, entry) |
| } |
| cfg.GeminiKey = out |
| } |
|
|
| func normalizeModelPrefix(prefix string) string { |
| trimmed := strings.TrimSpace(prefix) |
| trimmed = strings.Trim(trimmed, "/") |
| if trimmed == "" { |
| return "" |
| } |
| if strings.Contains(trimmed, "/") { |
| return "" |
| } |
| return trimmed |
| } |
|
|
| func syncInlineAccessProvider(cfg *Config) { |
| if cfg == nil { |
| return |
| } |
| if len(cfg.APIKeys) == 0 { |
| if provider := cfg.ConfigAPIKeyProvider(); provider != nil && len(provider.APIKeys) > 0 { |
| cfg.APIKeys = append([]string(nil), provider.APIKeys...) |
| } |
| } |
| cfg.Access.Providers = nil |
| } |
|
|
| |
| func looksLikeBcrypt(s string) bool { |
| return len(s) > 4 && (s[:4] == "$2a$" || s[:4] == "$2b$" || s[:4] == "$2y$") |
| } |
|
|
| |
| func NormalizeHeaders(headers map[string]string) map[string]string { |
| if len(headers) == 0 { |
| return nil |
| } |
| clean := make(map[string]string, len(headers)) |
| for k, v := range headers { |
| key := strings.TrimSpace(k) |
| val := strings.TrimSpace(v) |
| if key == "" || val == "" { |
| continue |
| } |
| clean[key] = val |
| } |
| if len(clean) == 0 { |
| return nil |
| } |
| return clean |
| } |
|
|
| |
| |
| func NormalizeExcludedModels(models []string) []string { |
| if len(models) == 0 { |
| return nil |
| } |
| seen := make(map[string]struct{}, len(models)) |
| out := make([]string, 0, len(models)) |
| for _, raw := range models { |
| trimmed := strings.ToLower(strings.TrimSpace(raw)) |
| if trimmed == "" { |
| continue |
| } |
| if _, exists := seen[trimmed]; exists { |
| continue |
| } |
| seen[trimmed] = struct{}{} |
| out = append(out, trimmed) |
| } |
| if len(out) == 0 { |
| return nil |
| } |
| return out |
| } |
|
|
| |
| |
| func NormalizeOAuthExcludedModels(entries map[string][]string) map[string][]string { |
| if len(entries) == 0 { |
| return nil |
| } |
| out := make(map[string][]string, len(entries)) |
| for provider, models := range entries { |
| key := strings.ToLower(strings.TrimSpace(provider)) |
| if key == "" { |
| continue |
| } |
| normalized := NormalizeExcludedModels(models) |
| if len(normalized) == 0 { |
| continue |
| } |
| out[key] = normalized |
| } |
| if len(out) == 0 { |
| return nil |
| } |
| return out |
| } |
|
|
| |
| func hashSecret(secret string) (string, error) { |
| |
| hashedBytes, err := bcrypt.GenerateFromPassword([]byte(secret), bcrypt.DefaultCost) |
| if err != nil { |
| return "", err |
| } |
| return string(hashedBytes), nil |
| } |
|
|
| |
| |
| func SaveConfigPreserveComments(configFile string, cfg *Config) error { |
| persistCfg := sanitizeConfigForPersist(cfg) |
| |
| data, err := os.ReadFile(configFile) |
| if err != nil { |
| return err |
| } |
|
|
| var original yaml.Node |
| if err = yaml.Unmarshal(data, &original); err != nil { |
| return err |
| } |
| if original.Kind != yaml.DocumentNode || len(original.Content) == 0 { |
| return fmt.Errorf("invalid yaml document structure") |
| } |
| if original.Content[0] == nil || original.Content[0].Kind != yaml.MappingNode { |
| return fmt.Errorf("expected root mapping node") |
| } |
|
|
| |
| rendered, err := yaml.Marshal(persistCfg) |
| if err != nil { |
| return err |
| } |
| var generated yaml.Node |
| if err = yaml.Unmarshal(rendered, &generated); err != nil { |
| return err |
| } |
| if generated.Kind != yaml.DocumentNode || len(generated.Content) == 0 || generated.Content[0] == nil { |
| return fmt.Errorf("invalid generated yaml structure") |
| } |
| if generated.Content[0].Kind != yaml.MappingNode { |
| return fmt.Errorf("expected generated root mapping node") |
| } |
|
|
| |
| removeLegacyAuthBlock(original.Content[0]) |
| removeLegacyOpenAICompatAPIKeys(original.Content[0]) |
| removeLegacyAmpKeys(original.Content[0]) |
| removeLegacyGenerativeLanguageKeys(original.Content[0]) |
|
|
| pruneMappingToGeneratedKeys(original.Content[0], generated.Content[0], "oauth-excluded-models") |
|
|
| |
| mergeMappingPreserve(original.Content[0], generated.Content[0]) |
| normalizeCollectionNodeStyles(original.Content[0]) |
|
|
| |
| f, err := os.Create(configFile) |
| if err != nil { |
| return err |
| } |
| defer func() { _ = f.Close() }() |
| var buf bytes.Buffer |
| enc := yaml.NewEncoder(&buf) |
| enc.SetIndent(2) |
| if err = enc.Encode(&original); err != nil { |
| _ = enc.Close() |
| return err |
| } |
| if err = enc.Close(); err != nil { |
| return err |
| } |
| data = NormalizeCommentIndentation(buf.Bytes()) |
| _, err = f.Write(data) |
| return err |
| } |
|
|
| func sanitizeConfigForPersist(cfg *Config) *Config { |
| if cfg == nil { |
| return nil |
| } |
| clone := *cfg |
| clone.SDKConfig = cfg.SDKConfig |
| clone.SDKConfig.Access = AccessConfig{} |
| return &clone |
| } |
|
|
| |
| |
| func SaveConfigPreserveCommentsUpdateNestedScalar(configFile string, path []string, value string) error { |
| data, err := os.ReadFile(configFile) |
| if err != nil { |
| return err |
| } |
| var root yaml.Node |
| if err = yaml.Unmarshal(data, &root); err != nil { |
| return err |
| } |
| if root.Kind != yaml.DocumentNode || len(root.Content) == 0 { |
| return fmt.Errorf("invalid yaml document structure") |
| } |
| node := root.Content[0] |
| |
| for i, key := range path { |
| if i == len(path)-1 { |
| |
| v := getOrCreateMapValue(node, key) |
| v.Kind = yaml.ScalarNode |
| v.Tag = "!!str" |
| v.Value = value |
| } else { |
| next := getOrCreateMapValue(node, key) |
| if next.Kind != yaml.MappingNode { |
| next.Kind = yaml.MappingNode |
| next.Tag = "!!map" |
| } |
| node = next |
| } |
| } |
| f, err := os.Create(configFile) |
| if err != nil { |
| return err |
| } |
| defer func() { _ = f.Close() }() |
| var buf bytes.Buffer |
| enc := yaml.NewEncoder(&buf) |
| enc.SetIndent(2) |
| if err = enc.Encode(&root); err != nil { |
| _ = enc.Close() |
| return err |
| } |
| if err = enc.Close(); err != nil { |
| return err |
| } |
| data = NormalizeCommentIndentation(buf.Bytes()) |
| _, err = f.Write(data) |
| return err |
| } |
|
|
| |
| func NormalizeCommentIndentation(data []byte) []byte { |
| lines := bytes.Split(data, []byte("\n")) |
| changed := false |
| for i, line := range lines { |
| trimmed := bytes.TrimLeft(line, " \t") |
| if len(trimmed) == 0 || trimmed[0] != '#' { |
| continue |
| } |
| if len(trimmed) == len(line) { |
| continue |
| } |
| lines[i] = append([]byte(nil), trimmed...) |
| changed = true |
| } |
| if !changed { |
| return data |
| } |
| return bytes.Join(lines, []byte("\n")) |
| } |
|
|
| |
| |
| func getOrCreateMapValue(mapNode *yaml.Node, key string) *yaml.Node { |
| if mapNode.Kind != yaml.MappingNode { |
| mapNode.Kind = yaml.MappingNode |
| mapNode.Tag = "!!map" |
| mapNode.Content = nil |
| } |
| for i := 0; i+1 < len(mapNode.Content); i += 2 { |
| k := mapNode.Content[i] |
| if k.Value == key { |
| return mapNode.Content[i+1] |
| } |
| } |
| |
| mapNode.Content = append(mapNode.Content, &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!str", Value: key}) |
| val := &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!str", Value: ""} |
| mapNode.Content = append(mapNode.Content, val) |
| return val |
| } |
|
|
| |
| |
| |
| func mergeMappingPreserve(dst, src *yaml.Node) { |
| if dst == nil || src == nil { |
| return |
| } |
| if dst.Kind != yaml.MappingNode || src.Kind != yaml.MappingNode { |
| |
| |
| copyNodeShallow(dst, src) |
| return |
| } |
| for i := 0; i+1 < len(src.Content); i += 2 { |
| sk := src.Content[i] |
| sv := src.Content[i+1] |
| idx := findMapKeyIndex(dst, sk.Value) |
| if idx >= 0 { |
| |
| dv := dst.Content[idx+1] |
| mergeNodePreserve(dv, sv) |
| } else { |
| |
| if isZeroValueNode(sv) { |
| continue |
| } |
| dst.Content = append(dst.Content, deepCopyNode(sk), deepCopyNode(sv)) |
| } |
| } |
| } |
|
|
| |
| |
| |
| func mergeNodePreserve(dst, src *yaml.Node) { |
| if dst == nil || src == nil { |
| return |
| } |
| switch src.Kind { |
| case yaml.MappingNode: |
| if dst.Kind != yaml.MappingNode { |
| copyNodeShallow(dst, src) |
| } |
| mergeMappingPreserve(dst, src) |
| case yaml.SequenceNode: |
| |
| if dst.Kind == yaml.ScalarNode && dst.Tag == "!!null" && len(src.Content) == 0 { |
| |
| return |
| } |
| if dst.Kind != yaml.SequenceNode { |
| dst.Kind = yaml.SequenceNode |
| dst.Tag = "!!seq" |
| dst.Content = nil |
| } |
| reorderSequenceForMerge(dst, src) |
| |
| minContent := len(dst.Content) |
| if len(src.Content) < minContent { |
| minContent = len(src.Content) |
| } |
| for i := 0; i < minContent; i++ { |
| if dst.Content[i] == nil { |
| dst.Content[i] = deepCopyNode(src.Content[i]) |
| continue |
| } |
| mergeNodePreserve(dst.Content[i], src.Content[i]) |
| if dst.Content[i] != nil && src.Content[i] != nil && |
| dst.Content[i].Kind == yaml.MappingNode && src.Content[i].Kind == yaml.MappingNode { |
| pruneMissingMapKeys(dst.Content[i], src.Content[i]) |
| } |
| } |
| |
| for i := len(dst.Content); i < len(src.Content); i++ { |
| dst.Content = append(dst.Content, deepCopyNode(src.Content[i])) |
| } |
| |
| if len(src.Content) < len(dst.Content) { |
| dst.Content = dst.Content[:len(src.Content)] |
| } |
| case yaml.ScalarNode, yaml.AliasNode: |
| |
| dst.Kind = src.Kind |
| dst.Tag = src.Tag |
| dst.Value = src.Value |
| |
| case 0: |
| |
| default: |
| |
| copyNodeShallow(dst, src) |
| } |
| } |
|
|
| |
| |
| func findMapKeyIndex(mapNode *yaml.Node, key string) int { |
| if mapNode == nil || mapNode.Kind != yaml.MappingNode { |
| return -1 |
| } |
| for i := 0; i+1 < len(mapNode.Content); i += 2 { |
| if mapNode.Content[i] != nil && mapNode.Content[i].Value == key { |
| return i |
| } |
| } |
| return -1 |
| } |
|
|
| |
| |
| |
| func isZeroValueNode(node *yaml.Node) bool { |
| if node == nil { |
| return true |
| } |
| switch node.Kind { |
| case yaml.ScalarNode: |
| switch node.Tag { |
| case "!!bool": |
| return node.Value == "false" |
| case "!!int", "!!float": |
| return node.Value == "0" || node.Value == "0.0" |
| case "!!str": |
| return node.Value == "" |
| case "!!null": |
| return true |
| } |
| case yaml.SequenceNode: |
| if len(node.Content) == 0 { |
| return true |
| } |
| |
| for _, child := range node.Content { |
| if !isZeroValueNode(child) { |
| return false |
| } |
| } |
| return true |
| case yaml.MappingNode: |
| if len(node.Content) == 0 { |
| return true |
| } |
| |
| for i := 1; i < len(node.Content); i += 2 { |
| if !isZeroValueNode(node.Content[i]) { |
| return false |
| } |
| } |
| return true |
| } |
| return false |
| } |
|
|
| |
| func deepCopyNode(n *yaml.Node) *yaml.Node { |
| if n == nil { |
| return nil |
| } |
| cp := *n |
| if len(n.Content) > 0 { |
| cp.Content = make([]*yaml.Node, len(n.Content)) |
| for i := range n.Content { |
| cp.Content[i] = deepCopyNode(n.Content[i]) |
| } |
| } |
| return &cp |
| } |
|
|
| |
| |
| func copyNodeShallow(dst, src *yaml.Node) { |
| if dst == nil || src == nil { |
| return |
| } |
| dst.Kind = src.Kind |
| dst.Tag = src.Tag |
| dst.Value = src.Value |
| |
| if len(src.Content) > 0 { |
| dst.Content = make([]*yaml.Node, len(src.Content)) |
| for i := range src.Content { |
| dst.Content[i] = deepCopyNode(src.Content[i]) |
| } |
| } else { |
| dst.Content = nil |
| } |
| } |
|
|
| func reorderSequenceForMerge(dst, src *yaml.Node) { |
| if dst == nil || src == nil { |
| return |
| } |
| if len(dst.Content) == 0 { |
| return |
| } |
| if len(src.Content) == 0 { |
| return |
| } |
| original := append([]*yaml.Node(nil), dst.Content...) |
| used := make([]bool, len(original)) |
| ordered := make([]*yaml.Node, len(src.Content)) |
| for i := range src.Content { |
| if idx := matchSequenceElement(original, used, src.Content[i]); idx >= 0 { |
| ordered[i] = original[idx] |
| used[idx] = true |
| } |
| } |
| dst.Content = ordered |
| } |
|
|
| func matchSequenceElement(original []*yaml.Node, used []bool, target *yaml.Node) int { |
| if target == nil { |
| return -1 |
| } |
| switch target.Kind { |
| case yaml.MappingNode: |
| id := sequenceElementIdentity(target) |
| if id != "" { |
| for i := range original { |
| if used[i] || original[i] == nil || original[i].Kind != yaml.MappingNode { |
| continue |
| } |
| if sequenceElementIdentity(original[i]) == id { |
| return i |
| } |
| } |
| } |
| case yaml.ScalarNode: |
| val := strings.TrimSpace(target.Value) |
| if val != "" { |
| for i := range original { |
| if used[i] || original[i] == nil || original[i].Kind != yaml.ScalarNode { |
| continue |
| } |
| if strings.TrimSpace(original[i].Value) == val { |
| return i |
| } |
| } |
| } |
| default: |
| } |
| |
| for i := range original { |
| if used[i] || original[i] == nil { |
| continue |
| } |
| if nodesStructurallyEqual(original[i], target) { |
| return i |
| } |
| } |
| return -1 |
| } |
|
|
| func sequenceElementIdentity(node *yaml.Node) string { |
| if node == nil || node.Kind != yaml.MappingNode { |
| return "" |
| } |
| identityKeys := []string{"id", "name", "alias", "api-key", "api_key", "apikey", "key", "provider", "model"} |
| for _, k := range identityKeys { |
| if v := mappingScalarValue(node, k); v != "" { |
| return k + "=" + v |
| } |
| } |
| for i := 0; i+1 < len(node.Content); i += 2 { |
| keyNode := node.Content[i] |
| valNode := node.Content[i+1] |
| if keyNode == nil || valNode == nil || valNode.Kind != yaml.ScalarNode { |
| continue |
| } |
| val := strings.TrimSpace(valNode.Value) |
| if val != "" { |
| return strings.ToLower(strings.TrimSpace(keyNode.Value)) + "=" + val |
| } |
| } |
| return "" |
| } |
|
|
| func mappingScalarValue(node *yaml.Node, key string) string { |
| if node == nil || node.Kind != yaml.MappingNode { |
| return "" |
| } |
| lowerKey := strings.ToLower(key) |
| for i := 0; i+1 < len(node.Content); i += 2 { |
| keyNode := node.Content[i] |
| valNode := node.Content[i+1] |
| if keyNode == nil || valNode == nil || valNode.Kind != yaml.ScalarNode { |
| continue |
| } |
| if strings.ToLower(strings.TrimSpace(keyNode.Value)) == lowerKey { |
| return strings.TrimSpace(valNode.Value) |
| } |
| } |
| return "" |
| } |
|
|
| func nodesStructurallyEqual(a, b *yaml.Node) bool { |
| if a == nil || b == nil { |
| return a == b |
| } |
| if a.Kind != b.Kind { |
| return false |
| } |
| switch a.Kind { |
| case yaml.MappingNode: |
| if len(a.Content) != len(b.Content) { |
| return false |
| } |
| for i := 0; i+1 < len(a.Content); i += 2 { |
| if !nodesStructurallyEqual(a.Content[i], b.Content[i]) { |
| return false |
| } |
| if !nodesStructurallyEqual(a.Content[i+1], b.Content[i+1]) { |
| return false |
| } |
| } |
| return true |
| case yaml.SequenceNode: |
| if len(a.Content) != len(b.Content) { |
| return false |
| } |
| for i := range a.Content { |
| if !nodesStructurallyEqual(a.Content[i], b.Content[i]) { |
| return false |
| } |
| } |
| return true |
| case yaml.ScalarNode: |
| return strings.TrimSpace(a.Value) == strings.TrimSpace(b.Value) |
| case yaml.AliasNode: |
| return nodesStructurallyEqual(a.Alias, b.Alias) |
| default: |
| return strings.TrimSpace(a.Value) == strings.TrimSpace(b.Value) |
| } |
| } |
|
|
| func removeMapKey(mapNode *yaml.Node, key string) { |
| if mapNode == nil || mapNode.Kind != yaml.MappingNode || key == "" { |
| return |
| } |
| for i := 0; i+1 < len(mapNode.Content); i += 2 { |
| if mapNode.Content[i] != nil && mapNode.Content[i].Value == key { |
| mapNode.Content = append(mapNode.Content[:i], mapNode.Content[i+2:]...) |
| return |
| } |
| } |
| } |
|
|
| func pruneMappingToGeneratedKeys(dstRoot, srcRoot *yaml.Node, key string) { |
| if key == "" || dstRoot == nil || srcRoot == nil { |
| return |
| } |
| if dstRoot.Kind != yaml.MappingNode || srcRoot.Kind != yaml.MappingNode { |
| return |
| } |
| dstIdx := findMapKeyIndex(dstRoot, key) |
| if dstIdx < 0 || dstIdx+1 >= len(dstRoot.Content) { |
| return |
| } |
| srcIdx := findMapKeyIndex(srcRoot, key) |
| if srcIdx < 0 { |
| removeMapKey(dstRoot, key) |
| return |
| } |
| if srcIdx+1 >= len(srcRoot.Content) { |
| return |
| } |
| srcVal := srcRoot.Content[srcIdx+1] |
| dstVal := dstRoot.Content[dstIdx+1] |
| if srcVal == nil { |
| dstRoot.Content[dstIdx+1] = nil |
| return |
| } |
| if srcVal.Kind != yaml.MappingNode { |
| dstRoot.Content[dstIdx+1] = deepCopyNode(srcVal) |
| return |
| } |
| if dstVal == nil || dstVal.Kind != yaml.MappingNode { |
| dstRoot.Content[dstIdx+1] = deepCopyNode(srcVal) |
| return |
| } |
| pruneMissingMapKeys(dstVal, srcVal) |
| } |
|
|
| func pruneMissingMapKeys(dstMap, srcMap *yaml.Node) { |
| if dstMap == nil || srcMap == nil || dstMap.Kind != yaml.MappingNode || srcMap.Kind != yaml.MappingNode { |
| return |
| } |
| keep := make(map[string]struct{}, len(srcMap.Content)/2) |
| for i := 0; i+1 < len(srcMap.Content); i += 2 { |
| keyNode := srcMap.Content[i] |
| if keyNode == nil { |
| continue |
| } |
| key := strings.TrimSpace(keyNode.Value) |
| if key == "" { |
| continue |
| } |
| keep[key] = struct{}{} |
| } |
| for i := 0; i+1 < len(dstMap.Content); { |
| keyNode := dstMap.Content[i] |
| if keyNode == nil { |
| i += 2 |
| continue |
| } |
| key := strings.TrimSpace(keyNode.Value) |
| if _, ok := keep[key]; !ok { |
| dstMap.Content = append(dstMap.Content[:i], dstMap.Content[i+2:]...) |
| continue |
| } |
| i += 2 |
| } |
| } |
|
|
| |
| |
| |
| func normalizeCollectionNodeStyles(node *yaml.Node) { |
| if node == nil { |
| return |
| } |
| switch node.Kind { |
| case yaml.MappingNode: |
| node.Style = 0 |
| for i := range node.Content { |
| normalizeCollectionNodeStyles(node.Content[i]) |
| } |
| case yaml.SequenceNode: |
| if len(node.Content) == 0 { |
| node.Style = yaml.FlowStyle |
| } else { |
| node.Style = 0 |
| } |
| for i := range node.Content { |
| normalizeCollectionNodeStyles(node.Content[i]) |
| } |
| default: |
| |
| } |
| } |
|
|
| |
| type legacyConfigData struct { |
| LegacyGeminiKeys []string `yaml:"generative-language-api-key"` |
| OpenAICompat []legacyOpenAICompatibility `yaml:"openai-compatibility"` |
| AmpUpstreamURL string `yaml:"amp-upstream-url"` |
| AmpUpstreamAPIKey string `yaml:"amp-upstream-api-key"` |
| AmpRestrictManagement *bool `yaml:"amp-restrict-management-to-localhost"` |
| AmpModelMappings []AmpModelMapping `yaml:"amp-model-mappings"` |
| } |
|
|
| type legacyOpenAICompatibility struct { |
| Name string `yaml:"name"` |
| BaseURL string `yaml:"base-url"` |
| APIKeys []string `yaml:"api-keys"` |
| } |
|
|
| func (cfg *Config) migrateLegacyGeminiKeys(legacy []string) bool { |
| if cfg == nil || len(legacy) == 0 { |
| return false |
| } |
| changed := false |
| seen := make(map[string]struct{}, len(cfg.GeminiKey)) |
| for i := range cfg.GeminiKey { |
| key := strings.TrimSpace(cfg.GeminiKey[i].APIKey) |
| if key == "" { |
| continue |
| } |
| seen[key] = struct{}{} |
| } |
| for _, raw := range legacy { |
| key := strings.TrimSpace(raw) |
| if key == "" { |
| continue |
| } |
| if _, exists := seen[key]; exists { |
| continue |
| } |
| cfg.GeminiKey = append(cfg.GeminiKey, GeminiKey{APIKey: key}) |
| seen[key] = struct{}{} |
| changed = true |
| } |
| return changed |
| } |
|
|
| func (cfg *Config) migrateLegacyOpenAICompatibilityKeys(legacy []legacyOpenAICompatibility) bool { |
| if cfg == nil || len(cfg.OpenAICompatibility) == 0 || len(legacy) == 0 { |
| return false |
| } |
| changed := false |
| for _, legacyEntry := range legacy { |
| if len(legacyEntry.APIKeys) == 0 { |
| continue |
| } |
| target := findOpenAICompatTarget(cfg.OpenAICompatibility, legacyEntry.Name, legacyEntry.BaseURL) |
| if target == nil { |
| continue |
| } |
| if mergeLegacyOpenAICompatAPIKeys(target, legacyEntry.APIKeys) { |
| changed = true |
| } |
| } |
| return changed |
| } |
|
|
| func mergeLegacyOpenAICompatAPIKeys(entry *OpenAICompatibility, keys []string) bool { |
| if entry == nil || len(keys) == 0 { |
| return false |
| } |
| changed := false |
| existing := make(map[string]struct{}, len(entry.APIKeyEntries)) |
| for i := range entry.APIKeyEntries { |
| key := strings.TrimSpace(entry.APIKeyEntries[i].APIKey) |
| if key == "" { |
| continue |
| } |
| existing[key] = struct{}{} |
| } |
| for _, raw := range keys { |
| key := strings.TrimSpace(raw) |
| if key == "" { |
| continue |
| } |
| if _, ok := existing[key]; ok { |
| continue |
| } |
| entry.APIKeyEntries = append(entry.APIKeyEntries, OpenAICompatibilityAPIKey{APIKey: key}) |
| existing[key] = struct{}{} |
| changed = true |
| } |
| return changed |
| } |
|
|
| func findOpenAICompatTarget(entries []OpenAICompatibility, legacyName, legacyBase string) *OpenAICompatibility { |
| nameKey := strings.ToLower(strings.TrimSpace(legacyName)) |
| baseKey := strings.ToLower(strings.TrimSpace(legacyBase)) |
| if nameKey != "" && baseKey != "" { |
| for i := range entries { |
| if strings.ToLower(strings.TrimSpace(entries[i].Name)) == nameKey && |
| strings.ToLower(strings.TrimSpace(entries[i].BaseURL)) == baseKey { |
| return &entries[i] |
| } |
| } |
| } |
| if baseKey != "" { |
| for i := range entries { |
| if strings.ToLower(strings.TrimSpace(entries[i].BaseURL)) == baseKey { |
| return &entries[i] |
| } |
| } |
| } |
| if nameKey != "" { |
| for i := range entries { |
| if strings.ToLower(strings.TrimSpace(entries[i].Name)) == nameKey { |
| return &entries[i] |
| } |
| } |
| } |
| return nil |
| } |
|
|
| func (cfg *Config) migrateLegacyAmpConfig(legacy *legacyConfigData) bool { |
| if cfg == nil || legacy == nil { |
| return false |
| } |
| changed := false |
| if cfg.AmpCode.UpstreamURL == "" { |
| if val := strings.TrimSpace(legacy.AmpUpstreamURL); val != "" { |
| cfg.AmpCode.UpstreamURL = val |
| changed = true |
| } |
| } |
| if cfg.AmpCode.UpstreamAPIKey == "" { |
| if val := strings.TrimSpace(legacy.AmpUpstreamAPIKey); val != "" { |
| cfg.AmpCode.UpstreamAPIKey = val |
| changed = true |
| } |
| } |
| if legacy.AmpRestrictManagement != nil { |
| cfg.AmpCode.RestrictManagementToLocalhost = *legacy.AmpRestrictManagement |
| changed = true |
| } |
| if len(cfg.AmpCode.ModelMappings) == 0 && len(legacy.AmpModelMappings) > 0 { |
| cfg.AmpCode.ModelMappings = append([]AmpModelMapping(nil), legacy.AmpModelMappings...) |
| changed = true |
| } |
| return changed |
| } |
|
|
| func removeLegacyOpenAICompatAPIKeys(root *yaml.Node) { |
| if root == nil || root.Kind != yaml.MappingNode { |
| return |
| } |
| idx := findMapKeyIndex(root, "openai-compatibility") |
| if idx < 0 || idx+1 >= len(root.Content) { |
| return |
| } |
| seq := root.Content[idx+1] |
| if seq == nil || seq.Kind != yaml.SequenceNode { |
| return |
| } |
| for i := range seq.Content { |
| if seq.Content[i] != nil && seq.Content[i].Kind == yaml.MappingNode { |
| removeMapKey(seq.Content[i], "api-keys") |
| } |
| } |
| } |
|
|
| func removeLegacyAmpKeys(root *yaml.Node) { |
| if root == nil || root.Kind != yaml.MappingNode { |
| return |
| } |
| removeMapKey(root, "amp-upstream-url") |
| removeMapKey(root, "amp-upstream-api-key") |
| removeMapKey(root, "amp-restrict-management-to-localhost") |
| removeMapKey(root, "amp-model-mappings") |
| } |
|
|
| func removeLegacyGenerativeLanguageKeys(root *yaml.Node) { |
| if root == nil || root.Kind != yaml.MappingNode { |
| return |
| } |
| removeMapKey(root, "generative-language-api-key") |
| } |
|
|
| func removeLegacyAuthBlock(root *yaml.Node) { |
| if root == nil || root.Kind != yaml.MappingNode { |
| return |
| } |
| removeMapKey(root, "auth") |
| } |
|
|