diff --git a/config.example.yaml b/config.example.yaml index f6390d2ff..7178975a8 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -198,6 +198,15 @@ ws-auth: false # iflow: # - "tstars2.0" +# Claude OAuth Model Aliases +# Define model aliases for Claude Code (OAuth) accounts. This allows you to use shorter +# or more convenient model names when making API requests with authenticated Claude accounts. +# Format: alias: upstream-model-name +# claude-oauth-model-aliases: +# claude-haiku-4-5: "claude-haiku-4-5-20251001" # Use "claude-haiku-4-5" as alias for the full model name +# claude-sonnet-latest: "claude-sonnet-4-5-20250929" +# claude-opus-latest: "claude-opus-4-20250514" + # Optional payload configuration # payload: # default: # Default rules only set parameters when they are missing in the payload. diff --git a/internal/api/handlers/management/config_lists.go b/internal/api/handlers/management/config_lists.go index 7e42b64be..6e5df5ca4 100644 --- a/internal/api/handlers/management/config_lists.go +++ b/internal/api/handlers/management/config_lists.go @@ -572,6 +572,32 @@ func (h *Handler) DeleteOAuthExcludedModels(c *gin.Context) { h.persist(c) } +// claude-oauth-model-aliases: map[string]string +func (h *Handler) GetClaudeOAuthModelAliases(c *gin.Context) { + c.JSON(200, gin.H{"claude-oauth-model-aliases": config.NormalizeClaudeOAuthModelAliases(h.cfg.ClaudeOAuthModelAliases)}) +} + +func (h *Handler) PutClaudeOAuthModelAliases(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]string + if err = json.Unmarshal(data, &entries); err != nil { + var wrapper struct { + Items map[string]string `json:"items"` + } + if err2 := json.Unmarshal(data, &wrapper); err2 != nil { + c.JSON(400, gin.H{"error": "invalid body"}) + return + } + entries = wrapper.Items + } + h.cfg.ClaudeOAuthModelAliases = config.NormalizeClaudeOAuthModelAliases(entries) + h.persist(c) +} + // codex-api-key: []CodexKey func (h *Handler) GetCodexKeys(c *gin.Context) { c.JSON(200, gin.H{"codex-api-key": h.cfg.CodexKey}) diff --git a/internal/api/server.go b/internal/api/server.go index 094da118e..d236d197a 100644 --- a/internal/api/server.go +++ b/internal/api/server.go @@ -572,6 +572,9 @@ func (s *Server) registerManagementRoutes() { mgmt.PATCH("/oauth-excluded-models", s.mgmt.PatchOAuthExcludedModels) mgmt.DELETE("/oauth-excluded-models", s.mgmt.DeleteOAuthExcludedModels) + mgmt.GET("/claude-oauth-model-aliases", s.mgmt.GetClaudeOAuthModelAliases) + mgmt.PUT("/claude-oauth-model-aliases", s.mgmt.PutClaudeOAuthModelAliases) + mgmt.GET("/auth-files", s.mgmt.ListAuthFiles) mgmt.GET("/auth-files/models", s.mgmt.GetAuthFileModels) mgmt.GET("/auth-files/download", s.mgmt.DownloadAuthFile) diff --git a/internal/config/config.go b/internal/config/config.go index 6bd74c037..3fae2b5c4 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -88,6 +88,10 @@ 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"` + // ClaudeOAuthModelAliases defines model aliases for Claude OAuth (Claude Code) accounts. + // Maps alias names to upstream model names for routing. + ClaudeOAuthModelAliases map[string]string `yaml:"claude-oauth-model-aliases,omitempty" json:"claude-oauth-model-aliases,omitempty"` + // Payload defines default and override rules for provider payload parameters. Payload PayloadConfig `yaml:"payload" json:"payload"` @@ -425,6 +429,9 @@ func LoadConfigOptional(configFile string, optional bool) (*Config, error) { // Normalize OAuth provider model exclusion map. cfg.OAuthExcludedModels = NormalizeOAuthExcludedModels(cfg.OAuthExcludedModels) + // Normalize Claude OAuth model aliases map for efficient lookups. + cfg.ClaudeOAuthModelAliases = NormalizeClaudeOAuthModelAliases(cfg.ClaudeOAuthModelAliases) + if cfg.legacyMigrationPending { fmt.Println("Detected legacy configuration keys, attempting to persist the normalized config...") if !optional && configFile != "" { @@ -624,6 +631,28 @@ func NormalizeOAuthExcludedModels(entries map[string][]string) map[string][]stri return out } +// NormalizeClaudeOAuthModelAliases normalizes the Claude OAuth model aliases map by trimming +// whitespace, lowercasing keys, and trimming whitespace from values. This enables efficient +// case-insensitive lookup during request processing. +func NormalizeClaudeOAuthModelAliases(entries map[string]string) map[string]string { + if len(entries) == 0 { + return nil + } + out := make(map[string]string, len(entries)) + for alias, upstream := range entries { + normalizedKey := strings.ToLower(strings.TrimSpace(alias)) + normalizedValue := strings.TrimSpace(upstream) + if normalizedKey == "" || normalizedValue == "" { + continue + } + out[normalizedKey] = normalizedValue + } + if len(out) == 0 { + return nil + } + return out +} + // hashSecret hashes the given secret using bcrypt. func hashSecret(secret string) (string, error) { // Use default cost for simplicity. diff --git a/internal/runtime/executor/claude_executor.go b/internal/runtime/executor/claude_executor.go index 0c31f424b..4db8e5b7f 100644 --- a/internal/runtime/executor/claude_executor.go +++ b/internal/runtime/executor/claude_executor.go @@ -505,6 +505,10 @@ func (e *ClaudeExecutor) resolveUpstreamModel(alias string, auth *cliproxyauth.A entry := e.resolveClaudeConfig(auth) if entry == nil { + // Check for OAuth model aliases (for Claude Code OAuth accounts) + if e.cfg != nil && auth != nil && strings.EqualFold(auth.Provider, "claude") { + return e.resolveOAuthModelAlias(trimmed) + } return "" } @@ -539,9 +543,28 @@ func (e *ClaudeExecutor) resolveUpstreamModel(alias string, auth *cliproxyauth.A } } } + + // If no match in API key config, check OAuth aliases as fallback + if e.cfg != nil { + return e.resolveOAuthModelAlias(trimmed) + } + return "" } +// resolveOAuthModelAlias resolves a model alias using the global Claude OAuth model aliases configuration. +// This supports aliasing for Claude Code (OAuth) accounts where API key config is not available. +// The map keys are normalized (lowercase and trimmed) during config loading for efficient lookup. +func (e *ClaudeExecutor) resolveOAuthModelAlias(alias string) string { + if e.cfg == nil || len(e.cfg.ClaudeOAuthModelAliases) == 0 { + return "" + } + + // Direct map lookup with normalized key - keys are normalized during config loading + normalizedAlias := strings.ToLower(strings.TrimSpace(alias)) + return e.cfg.ClaudeOAuthModelAliases[normalizedAlias] +} + func (e *ClaudeExecutor) resolveClaudeConfig(auth *cliproxyauth.Auth) *config.ClaudeKey { if auth == nil || e.cfg == nil { return nil diff --git a/sdk/cliproxy/service.go b/sdk/cliproxy/service.go index a699ca618..7a33b8b89 100644 --- a/sdk/cliproxy/service.go +++ b/sdk/cliproxy/service.go @@ -737,6 +737,10 @@ func (s *Service) registerModelsForAuth(a *coreauth.Auth) { excluded = entry.ExcludedModels } } + // Add OAuth model aliases for Claude Code accounts + if oauthAliases := s.buildClaudeOAuthAliasModels(); len(oauthAliases) > 0 { + models = append(models, oauthAliases...) + } models = applyExcludedModels(models, excluded) case "codex": models = registry.GetOpenAIModels() @@ -1179,3 +1183,35 @@ func buildClaudeConfigModels(entry *config.ClaudeKey) []*ModelInfo { } return out } + +// buildClaudeOAuthAliasModels builds model info entries from global Claude OAuth model aliases configuration. +// This allows aliasing models for Claude Code (OAuth) accounts where API key config is not available. +// Note: The map is already normalized (keys lower-cased, values trimmed, empty entries filtered) +// during config loading in config.NormalizeClaudeOAuthModelAliases(). +func (s *Service) buildClaudeOAuthAliasModels() []*ModelInfo { + if s.cfg == nil || len(s.cfg.ClaudeOAuthModelAliases) == 0 { + return nil + } + + now := time.Now().Unix() + out := make([]*ModelInfo, 0, len(s.cfg.ClaudeOAuthModelAliases)) + + for alias, upstream := range s.cfg.ClaudeOAuthModelAliases { + // Map is already normalized during config loading: + // - alias is already lowercased and trimmed + // - upstream is already trimmed + // - empty entries are already filtered + // - keys are unique (map property) + + out = append(out, &ModelInfo{ + ID: alias, + Object: "model", + Created: now, + OwnedBy: "claude", + Type: "claude", + DisplayName: upstream, + }) + } + + return out +}