├── auths └── .gitkeep ├── .github ├── FUNDING.yml ├── pull.yml ├── workflows │ ├── pr-path-guard.yml │ ├── release.yaml │ └── docker-image.yml └── ISSUE_TEMPLATE │ └── bug_report.md ├── internal ├── misc │ ├── claude_code_instructions.txt │ ├── oauth.go │ ├── claude_code_instructions.go │ ├── credentials.go │ ├── copy-example-config.go │ ├── header_utils.go │ └── codex_instructions.go ├── runtime │ └── executor │ │ └── cache_helpers.go ├── buildinfo │ └── buildinfo.go ├── translator │ ├── codex │ │ ├── openai │ │ │ ├── chat-completions │ │ │ │ └── init.go │ │ │ └── responses │ │ │ │ ├── init.go │ │ │ │ └── codex_openai-responses_response.go │ │ ├── claude │ │ │ └── init.go │ │ ├── gemini │ │ │ └── init.go │ │ └── gemini-cli │ │ │ ├── init.go │ │ │ ├── codex_gemini-cli_request.go │ │ │ └── codex_gemini-cli_response.go │ ├── claude │ │ ├── openai │ │ │ ├── chat-completions │ │ │ │ └── init.go │ │ │ └── responses │ │ │ │ └── init.go │ │ ├── gemini │ │ │ └── init.go │ │ └── gemini-cli │ │ │ ├── init.go │ │ │ ├── claude_gemini-cli_request.go │ │ │ └── claude_gemini-cli_response.go │ ├── gemini │ │ ├── openai │ │ │ ├── chat-completions │ │ │ │ └── init.go │ │ │ └── responses │ │ │ │ └── init.go │ │ ├── claude │ │ │ └── init.go │ │ ├── gemini-cli │ │ │ ├── init.go │ │ │ ├── gemini_gemini-cli_response.go │ │ │ └── gemini_gemini-cli_request.go │ │ ├── gemini │ │ │ ├── init.go │ │ │ └── gemini_gemini_response.go │ │ └── common │ │ │ └── safety.go │ ├── openai │ │ ├── openai │ │ │ ├── chat-completions │ │ │ │ ├── init.go │ │ │ │ ├── openai_openai_request.go │ │ │ │ └── openai_openai_response.go │ │ │ └── responses │ │ │ │ └── init.go │ │ ├── claude │ │ │ └── init.go │ │ ├── gemini │ │ │ └── init.go │ │ └── gemini-cli │ │ │ ├── init.go │ │ │ ├── openai_gemini_request.go │ │ │ └── openai_gemini_response.go │ ├── gemini-cli │ │ ├── openai │ │ │ ├── chat-completions │ │ │ │ └── init.go │ │ │ └── responses │ │ │ │ ├── init.go │ │ │ │ ├── gemini-cli_openai-responses_request.go │ │ │ │ └── gemini-cli_openai-responses_response.go │ │ ├── claude │ │ │ └── init.go │ │ └── gemini │ │ │ └── init.go │ ├── antigravity │ │ ├── openai │ │ │ ├── chat-completions │ │ │ │ └── init.go │ │ │ └── responses │ │ │ │ ├── init.go │ │ │ │ ├── antigravity_openai-responses_request.go │ │ │ │ └── antigravity_openai-responses_response.go │ │ ├── claude │ │ │ └── init.go │ │ └── gemini │ │ │ └── init.go │ └── init.go ├── api │ ├── handlers │ │ └── management │ │ │ ├── usage.go │ │ │ └── quota.go │ ├── modules │ │ └── amp │ │ │ ├── gemini_bridge.go │ │ │ ├── gemini_bridge_test.go │ │ │ └── response_rewriter.go │ └── server_test.go ├── interfaces │ ├── types.go │ ├── error_message.go │ └── api_handler.go ├── auth │ ├── models.go │ ├── empty │ │ └── token.go │ ├── claude │ │ ├── anthropic.go │ │ ├── pkce.go │ │ └── token.go │ ├── iflow │ │ ├── iflow_token.go │ │ └── cookie_helpers.go │ ├── codex │ │ ├── openai.go │ │ ├── pkce.go │ │ └── token.go │ ├── qwen │ │ └── qwen_token.go │ ├── vertex │ │ └── vertex_credentials.go │ └── gemini │ │ └── gemini_token.go ├── cmd │ ├── auth_manager.go │ ├── antigravity_login.go │ ├── iflow_login.go │ ├── anthropic_login.go │ ├── qwen_login.go │ ├── openai_login.go │ ├── run.go │ └── iflow_cookie.go ├── constant │ └── constant.go ├── wsrelay │ └── message.go ├── util │ ├── image.go │ ├── header_helpers.go │ ├── claude_thinking.go │ ├── proxy.go │ └── util.go ├── access │ └── config_access │ │ └── provider.go ├── watcher │ └── diff │ │ └── model_hash.go └── logging │ ├── gin_logger.go │ └── global_logger.go ├── sdk ├── translator │ ├── format.go │ ├── formats.go │ ├── builtin │ │ └── builtin.go │ ├── helpers.go │ └── types.go ├── access │ ├── errors.go │ ├── manager.go │ └── registry.go ├── cliproxy │ ├── auth │ │ ├── store.go │ │ ├── status.go │ │ └── errors.go │ ├── model_registry.go │ ├── watcher.go │ ├── providers.go │ ├── executor │ │ └── types.go │ ├── pipeline │ │ └── context.go │ └── rtprovider.go ├── auth │ ├── store_registry.go │ ├── interfaces.go │ ├── errors.go │ ├── refresh_registry.go │ ├── gemini.go │ ├── manager.go │ └── qwen.go └── config │ └── config.go ├── .gitignore ├── .dockerignore ├── docker-compose.yml ├── Dockerfile ├── .goreleaser.yml ├── LICENSE ├── docs ├── sdk-watcher_CN.md └── sdk-watcher.md ├── .env.example ├── examples └── translator │ └── main.go ├── docker-build.sh ├── docker-build.ps1 ├── go.mod └── README_CN.md /auths/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [router-for-me] 2 | -------------------------------------------------------------------------------- /.github/pull.yml: -------------------------------------------------------------------------------- 1 | version: "1" 2 | rules: [] 3 | -------------------------------------------------------------------------------- /internal/misc/claude_code_instructions.txt: -------------------------------------------------------------------------------- 1 | [{"type":"text","text":"You are Claude Code, Anthropic's official CLI for Claude.","cache_control":{"type":"ephemeral"}}] -------------------------------------------------------------------------------- /internal/runtime/executor/cache_helpers.go: -------------------------------------------------------------------------------- 1 | package executor 2 | 3 | import "time" 4 | 5 | type codexCache struct { 6 | ID string 7 | Expire time.Time 8 | } 9 | 10 | var codexCacheMap = map[string]codexCache{} 11 | -------------------------------------------------------------------------------- /sdk/translator/format.go: -------------------------------------------------------------------------------- 1 | package translator 2 | 3 | // Format identifies a request/response schema used inside the proxy. 4 | type Format string 5 | 6 | // FromString converts an arbitrary identifier to a translator format. 7 | func FromString(v string) Format { 8 | return Format(v) 9 | } 10 | 11 | // String returns the raw schema identifier. 12 | func (f Format) String() string { 13 | return string(f) 14 | } 15 | -------------------------------------------------------------------------------- /sdk/translator/formats.go: -------------------------------------------------------------------------------- 1 | package translator 2 | 3 | // Common format identifiers exposed for SDK users. 4 | const ( 5 | FormatOpenAI Format = "openai" 6 | FormatOpenAIResponse Format = "openai-response" 7 | FormatClaude Format = "claude" 8 | FormatGemini Format = "gemini" 9 | FormatGeminiCLI Format = "gemini-cli" 10 | FormatCodex Format = "codex" 11 | FormatAntigravity Format = "antigravity" 12 | ) 13 | -------------------------------------------------------------------------------- /sdk/access/errors.go: -------------------------------------------------------------------------------- 1 | package access 2 | 3 | import "errors" 4 | 5 | var ( 6 | // ErrNoCredentials indicates no recognizable credentials were supplied. 7 | ErrNoCredentials = errors.New("access: no credentials provided") 8 | // ErrInvalidCredential signals that supplied credentials were rejected by a provider. 9 | ErrInvalidCredential = errors.New("access: invalid credential") 10 | // ErrNotHandled tells the manager to continue trying other providers. 11 | ErrNotHandled = errors.New("access: not handled") 12 | ) 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries 2 | cli-proxy-api 3 | *.exe 4 | 5 | # Configuration 6 | config.yaml 7 | .env 8 | 9 | # Generated content 10 | bin/* 11 | logs/* 12 | conv/* 13 | temp/* 14 | pgstore/* 15 | gitstore/* 16 | objectstore/* 17 | static/* 18 | refs/* 19 | 20 | # Authentication data 21 | auths/* 22 | !auths/.gitkeep 23 | 24 | # Documentation 25 | docs/* 26 | AGENTS.md 27 | CLAUDE.md 28 | GEMINI.md 29 | 30 | # Tooling metadata 31 | .vscode/* 32 | .claude/* 33 | .serena/* 34 | .bmad/* 35 | 36 | # macOS 37 | .DS_Store 38 | ._* 39 | -------------------------------------------------------------------------------- /sdk/cliproxy/auth/store.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import "context" 4 | 5 | // Store abstracts persistence of Auth state across restarts. 6 | type Store interface { 7 | // List returns all auth records stored in the backend. 8 | List(ctx context.Context) ([]*Auth, error) 9 | // Save persists the provided auth record, replacing any existing one with same ID. 10 | Save(ctx context.Context, auth *Auth) (string, error) 11 | // Delete removes the auth record identified by id. 12 | Delete(ctx context.Context, id string) error 13 | } 14 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # Git and GitHub folders 2 | .git/* 3 | .github/* 4 | 5 | # Docker and CI/CD related files 6 | docker-compose.yml 7 | .dockerignore 8 | .gitignore 9 | .goreleaser.yml 10 | Dockerfile 11 | 12 | # Documentation and license 13 | docs/* 14 | README.md 15 | README_CN.md 16 | MANAGEMENT_API.md 17 | MANAGEMENT_API_CN.md 18 | LICENSE 19 | 20 | # Runtime data folders (should be mounted as volumes) 21 | auths/* 22 | logs/* 23 | conv/* 24 | config.yaml 25 | 26 | # Development/editor 27 | bin/* 28 | .claude/* 29 | .vscode/* 30 | .serena/* 31 | .bmad/* -------------------------------------------------------------------------------- /internal/buildinfo/buildinfo.go: -------------------------------------------------------------------------------- 1 | // Package buildinfo exposes compile-time metadata shared across the server. 2 | package buildinfo 3 | 4 | // The following variables are overridden via ldflags during release builds. 5 | // Defaults cover local development builds. 6 | var ( 7 | // Version is the semantic version or git describe output of the binary. 8 | Version = "dev" 9 | 10 | // Commit is the git commit SHA baked into the binary. 11 | Commit = "none" 12 | 13 | // BuildDate records when the binary was built in UTC. 14 | BuildDate = "unknown" 15 | ) 16 | -------------------------------------------------------------------------------- /internal/translator/codex/openai/chat-completions/init.go: -------------------------------------------------------------------------------- 1 | package chat_completions 2 | 3 | import ( 4 | . "github.com/router-for-me/CLIProxyAPI/v6/internal/constant" 5 | "github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces" 6 | "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/translator" 7 | ) 8 | 9 | func init() { 10 | translator.Register( 11 | OpenAI, 12 | Codex, 13 | ConvertOpenAIRequestToCodex, 14 | interfaces.TranslateResponse{ 15 | Stream: ConvertCodexResponseToOpenAI, 16 | NonStream: ConvertCodexResponseToOpenAINonStream, 17 | }, 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /internal/translator/claude/openai/chat-completions/init.go: -------------------------------------------------------------------------------- 1 | package chat_completions 2 | 3 | import ( 4 | . "github.com/router-for-me/CLIProxyAPI/v6/internal/constant" 5 | "github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces" 6 | "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/translator" 7 | ) 8 | 9 | func init() { 10 | translator.Register( 11 | OpenAI, 12 | Claude, 13 | ConvertOpenAIRequestToClaude, 14 | interfaces.TranslateResponse{ 15 | Stream: ConvertClaudeResponseToOpenAI, 16 | NonStream: ConvertClaudeResponseToOpenAINonStream, 17 | }, 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /internal/translator/gemini/openai/chat-completions/init.go: -------------------------------------------------------------------------------- 1 | package chat_completions 2 | 3 | import ( 4 | . "github.com/router-for-me/CLIProxyAPI/v6/internal/constant" 5 | "github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces" 6 | "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/translator" 7 | ) 8 | 9 | func init() { 10 | translator.Register( 11 | OpenAI, 12 | Gemini, 13 | ConvertOpenAIRequestToGemini, 14 | interfaces.TranslateResponse{ 15 | Stream: ConvertGeminiResponseToOpenAI, 16 | NonStream: ConvertGeminiResponseToOpenAINonStream, 17 | }, 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /internal/translator/openai/openai/chat-completions/init.go: -------------------------------------------------------------------------------- 1 | package chat_completions 2 | 3 | import ( 4 | . "github.com/router-for-me/CLIProxyAPI/v6/internal/constant" 5 | "github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces" 6 | "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/translator" 7 | ) 8 | 9 | func init() { 10 | translator.Register( 11 | OpenAI, 12 | OpenAI, 13 | ConvertOpenAIRequestToOpenAI, 14 | interfaces.TranslateResponse{ 15 | Stream: ConvertOpenAIResponseToOpenAI, 16 | NonStream: ConvertOpenAIResponseToOpenAINonStream, 17 | }, 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /internal/translator/gemini-cli/openai/chat-completions/init.go: -------------------------------------------------------------------------------- 1 | package chat_completions 2 | 3 | import ( 4 | . "github.com/router-for-me/CLIProxyAPI/v6/internal/constant" 5 | "github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces" 6 | "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/translator" 7 | ) 8 | 9 | func init() { 10 | translator.Register( 11 | OpenAI, 12 | GeminiCLI, 13 | ConvertOpenAIRequestToGeminiCLI, 14 | interfaces.TranslateResponse{ 15 | Stream: ConvertCliResponseToOpenAI, 16 | NonStream: ConvertCliResponseToOpenAINonStream, 17 | }, 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /internal/api/handlers/management/usage.go: -------------------------------------------------------------------------------- 1 | package management 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gin-gonic/gin" 7 | "github.com/router-for-me/CLIProxyAPI/v6/internal/usage" 8 | ) 9 | 10 | // GetUsageStatistics returns the in-memory request statistics snapshot. 11 | func (h *Handler) GetUsageStatistics(c *gin.Context) { 12 | var snapshot usage.StatisticsSnapshot 13 | if h != nil && h.usageStats != nil { 14 | snapshot = h.usageStats.Snapshot() 15 | } 16 | c.JSON(http.StatusOK, gin.H{ 17 | "usage": snapshot, 18 | "failed_requests": snapshot.FailureCount, 19 | }) 20 | } 21 | -------------------------------------------------------------------------------- /internal/translator/codex/claude/init.go: -------------------------------------------------------------------------------- 1 | package claude 2 | 3 | import ( 4 | . "github.com/router-for-me/CLIProxyAPI/v6/internal/constant" 5 | "github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces" 6 | "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/translator" 7 | ) 8 | 9 | func init() { 10 | translator.Register( 11 | Claude, 12 | Codex, 13 | ConvertClaudeRequestToCodex, 14 | interfaces.TranslateResponse{ 15 | Stream: ConvertCodexResponseToClaude, 16 | NonStream: ConvertCodexResponseToClaudeNonStream, 17 | TokenCount: ClaudeTokenCount, 18 | }, 19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /internal/translator/codex/gemini/init.go: -------------------------------------------------------------------------------- 1 | package gemini 2 | 3 | import ( 4 | . "github.com/router-for-me/CLIProxyAPI/v6/internal/constant" 5 | "github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces" 6 | "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/translator" 7 | ) 8 | 9 | func init() { 10 | translator.Register( 11 | Gemini, 12 | Codex, 13 | ConvertGeminiRequestToCodex, 14 | interfaces.TranslateResponse{ 15 | Stream: ConvertCodexResponseToGemini, 16 | NonStream: ConvertCodexResponseToGeminiNonStream, 17 | TokenCount: GeminiTokenCount, 18 | }, 19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /internal/translator/claude/gemini/init.go: -------------------------------------------------------------------------------- 1 | package gemini 2 | 3 | import ( 4 | . "github.com/router-for-me/CLIProxyAPI/v6/internal/constant" 5 | "github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces" 6 | "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/translator" 7 | ) 8 | 9 | func init() { 10 | translator.Register( 11 | Gemini, 12 | Claude, 13 | ConvertGeminiRequestToClaude, 14 | interfaces.TranslateResponse{ 15 | Stream: ConvertClaudeResponseToGemini, 16 | NonStream: ConvertClaudeResponseToGeminiNonStream, 17 | TokenCount: GeminiTokenCount, 18 | }, 19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /internal/translator/codex/openai/responses/init.go: -------------------------------------------------------------------------------- 1 | package responses 2 | 3 | import ( 4 | . "github.com/router-for-me/CLIProxyAPI/v6/internal/constant" 5 | "github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces" 6 | "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/translator" 7 | ) 8 | 9 | func init() { 10 | translator.Register( 11 | OpenaiResponse, 12 | Codex, 13 | ConvertOpenAIResponsesRequestToCodex, 14 | interfaces.TranslateResponse{ 15 | Stream: ConvertCodexResponseToOpenAIResponses, 16 | NonStream: ConvertCodexResponseToOpenAIResponsesNonStream, 17 | }, 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /internal/translator/gemini/claude/init.go: -------------------------------------------------------------------------------- 1 | package claude 2 | 3 | import ( 4 | . "github.com/router-for-me/CLIProxyAPI/v6/internal/constant" 5 | "github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces" 6 | "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/translator" 7 | ) 8 | 9 | func init() { 10 | translator.Register( 11 | Claude, 12 | Gemini, 13 | ConvertClaudeRequestToGemini, 14 | interfaces.TranslateResponse{ 15 | Stream: ConvertGeminiResponseToClaude, 16 | NonStream: ConvertGeminiResponseToClaudeNonStream, 17 | TokenCount: ClaudeTokenCount, 18 | }, 19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /internal/translator/openai/claude/init.go: -------------------------------------------------------------------------------- 1 | package claude 2 | 3 | import ( 4 | . "github.com/router-for-me/CLIProxyAPI/v6/internal/constant" 5 | "github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces" 6 | "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/translator" 7 | ) 8 | 9 | func init() { 10 | translator.Register( 11 | Claude, 12 | OpenAI, 13 | ConvertClaudeRequestToOpenAI, 14 | interfaces.TranslateResponse{ 15 | Stream: ConvertOpenAIResponseToClaude, 16 | NonStream: ConvertOpenAIResponseToClaudeNonStream, 17 | TokenCount: ClaudeTokenCount, 18 | }, 19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /internal/translator/openai/gemini/init.go: -------------------------------------------------------------------------------- 1 | package gemini 2 | 3 | import ( 4 | . "github.com/router-for-me/CLIProxyAPI/v6/internal/constant" 5 | "github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces" 6 | "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/translator" 7 | ) 8 | 9 | func init() { 10 | translator.Register( 11 | Gemini, 12 | OpenAI, 13 | ConvertGeminiRequestToOpenAI, 14 | interfaces.TranslateResponse{ 15 | Stream: ConvertOpenAIResponseToGemini, 16 | NonStream: ConvertOpenAIResponseToGeminiNonStream, 17 | TokenCount: GeminiTokenCount, 18 | }, 19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /internal/translator/claude/openai/responses/init.go: -------------------------------------------------------------------------------- 1 | package responses 2 | 3 | import ( 4 | . "github.com/router-for-me/CLIProxyAPI/v6/internal/constant" 5 | "github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces" 6 | "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/translator" 7 | ) 8 | 9 | func init() { 10 | translator.Register( 11 | OpenaiResponse, 12 | Claude, 13 | ConvertOpenAIResponsesRequestToClaude, 14 | interfaces.TranslateResponse{ 15 | Stream: ConvertClaudeResponseToOpenAIResponses, 16 | NonStream: ConvertClaudeResponseToOpenAIResponsesNonStream, 17 | }, 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /internal/translator/gemini/openai/responses/init.go: -------------------------------------------------------------------------------- 1 | package responses 2 | 3 | import ( 4 | . "github.com/router-for-me/CLIProxyAPI/v6/internal/constant" 5 | "github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces" 6 | "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/translator" 7 | ) 8 | 9 | func init() { 10 | translator.Register( 11 | OpenaiResponse, 12 | Gemini, 13 | ConvertOpenAIResponsesRequestToGemini, 14 | interfaces.TranslateResponse{ 15 | Stream: ConvertGeminiResponseToOpenAIResponses, 16 | NonStream: ConvertGeminiResponseToOpenAIResponsesNonStream, 17 | }, 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /internal/translator/antigravity/openai/chat-completions/init.go: -------------------------------------------------------------------------------- 1 | package chat_completions 2 | 3 | import ( 4 | . "github.com/router-for-me/CLIProxyAPI/v6/internal/constant" 5 | "github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces" 6 | "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/translator" 7 | ) 8 | 9 | func init() { 10 | translator.Register( 11 | OpenAI, 12 | Antigravity, 13 | ConvertOpenAIRequestToAntigravity, 14 | interfaces.TranslateResponse{ 15 | Stream: ConvertAntigravityResponseToOpenAI, 16 | NonStream: ConvertAntigravityResponseToOpenAINonStream, 17 | }, 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /internal/translator/gemini-cli/claude/init.go: -------------------------------------------------------------------------------- 1 | package claude 2 | 3 | import ( 4 | . "github.com/router-for-me/CLIProxyAPI/v6/internal/constant" 5 | "github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces" 6 | "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/translator" 7 | ) 8 | 9 | func init() { 10 | translator.Register( 11 | Claude, 12 | GeminiCLI, 13 | ConvertClaudeRequestToCLI, 14 | interfaces.TranslateResponse{ 15 | Stream: ConvertGeminiCLIResponseToClaude, 16 | NonStream: ConvertGeminiCLIResponseToClaudeNonStream, 17 | TokenCount: ClaudeTokenCount, 18 | }, 19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /internal/translator/codex/gemini-cli/init.go: -------------------------------------------------------------------------------- 1 | package geminiCLI 2 | 3 | import ( 4 | . "github.com/router-for-me/CLIProxyAPI/v6/internal/constant" 5 | "github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces" 6 | "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/translator" 7 | ) 8 | 9 | func init() { 10 | translator.Register( 11 | GeminiCLI, 12 | Codex, 13 | ConvertGeminiCLIRequestToCodex, 14 | interfaces.TranslateResponse{ 15 | Stream: ConvertCodexResponseToGeminiCLI, 16 | NonStream: ConvertCodexResponseToGeminiCLINonStream, 17 | TokenCount: GeminiCLITokenCount, 18 | }, 19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /internal/translator/gemini-cli/gemini/init.go: -------------------------------------------------------------------------------- 1 | package gemini 2 | 3 | import ( 4 | . "github.com/router-for-me/CLIProxyAPI/v6/internal/constant" 5 | "github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces" 6 | "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/translator" 7 | ) 8 | 9 | func init() { 10 | translator.Register( 11 | Gemini, 12 | GeminiCLI, 13 | ConvertGeminiRequestToGeminiCLI, 14 | interfaces.TranslateResponse{ 15 | Stream: ConvertGeminiCliResponseToGemini, 16 | NonStream: ConvertGeminiCliResponseToGeminiNonStream, 17 | TokenCount: GeminiTokenCount, 18 | }, 19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /internal/translator/claude/gemini-cli/init.go: -------------------------------------------------------------------------------- 1 | package geminiCLI 2 | 3 | import ( 4 | . "github.com/router-for-me/CLIProxyAPI/v6/internal/constant" 5 | "github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces" 6 | "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/translator" 7 | ) 8 | 9 | func init() { 10 | translator.Register( 11 | GeminiCLI, 12 | Claude, 13 | ConvertGeminiCLIRequestToClaude, 14 | interfaces.TranslateResponse{ 15 | Stream: ConvertClaudeResponseToGeminiCLI, 16 | NonStream: ConvertClaudeResponseToGeminiCLINonStream, 17 | TokenCount: GeminiCLITokenCount, 18 | }, 19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /internal/translator/gemini-cli/openai/responses/init.go: -------------------------------------------------------------------------------- 1 | package responses 2 | 3 | import ( 4 | . "github.com/router-for-me/CLIProxyAPI/v6/internal/constant" 5 | "github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces" 6 | "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/translator" 7 | ) 8 | 9 | func init() { 10 | translator.Register( 11 | OpenaiResponse, 12 | GeminiCLI, 13 | ConvertOpenAIResponsesRequestToGeminiCLI, 14 | interfaces.TranslateResponse{ 15 | Stream: ConvertGeminiCLIResponseToOpenAIResponses, 16 | NonStream: ConvertGeminiCLIResponseToOpenAIResponsesNonStream, 17 | }, 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /internal/translator/gemini/gemini-cli/init.go: -------------------------------------------------------------------------------- 1 | package geminiCLI 2 | 3 | import ( 4 | . "github.com/router-for-me/CLIProxyAPI/v6/internal/constant" 5 | "github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces" 6 | "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/translator" 7 | ) 8 | 9 | func init() { 10 | translator.Register( 11 | GeminiCLI, 12 | Gemini, 13 | ConvertGeminiCLIRequestToGemini, 14 | interfaces.TranslateResponse{ 15 | Stream: ConvertGeminiResponseToGeminiCLI, 16 | NonStream: ConvertGeminiResponseToGeminiCLINonStream, 17 | TokenCount: GeminiCLITokenCount, 18 | }, 19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /internal/translator/openai/gemini-cli/init.go: -------------------------------------------------------------------------------- 1 | package geminiCLI 2 | 3 | import ( 4 | . "github.com/router-for-me/CLIProxyAPI/v6/internal/constant" 5 | "github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces" 6 | "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/translator" 7 | ) 8 | 9 | func init() { 10 | translator.Register( 11 | GeminiCLI, 12 | OpenAI, 13 | ConvertGeminiCLIRequestToOpenAI, 14 | interfaces.TranslateResponse{ 15 | Stream: ConvertOpenAIResponseToGeminiCLI, 16 | NonStream: ConvertOpenAIResponseToGeminiCLINonStream, 17 | TokenCount: GeminiCLITokenCount, 18 | }, 19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /internal/translator/antigravity/claude/init.go: -------------------------------------------------------------------------------- 1 | package claude 2 | 3 | import ( 4 | . "github.com/router-for-me/CLIProxyAPI/v6/internal/constant" 5 | "github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces" 6 | "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/translator" 7 | ) 8 | 9 | func init() { 10 | translator.Register( 11 | Claude, 12 | Antigravity, 13 | ConvertClaudeRequestToAntigravity, 14 | interfaces.TranslateResponse{ 15 | Stream: ConvertAntigravityResponseToClaude, 16 | NonStream: ConvertAntigravityResponseToClaudeNonStream, 17 | TokenCount: ClaudeTokenCount, 18 | }, 19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /internal/translator/antigravity/gemini/init.go: -------------------------------------------------------------------------------- 1 | package gemini 2 | 3 | import ( 4 | . "github.com/router-for-me/CLIProxyAPI/v6/internal/constant" 5 | "github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces" 6 | "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/translator" 7 | ) 8 | 9 | func init() { 10 | translator.Register( 11 | Gemini, 12 | Antigravity, 13 | ConvertGeminiRequestToAntigravity, 14 | interfaces.TranslateResponse{ 15 | Stream: ConvertAntigravityResponseToGemini, 16 | NonStream: ConvertAntigravityResponseToGeminiNonStream, 17 | TokenCount: GeminiTokenCount, 18 | }, 19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /internal/translator/gemini-cli/openai/responses/gemini-cli_openai-responses_request.go: -------------------------------------------------------------------------------- 1 | package responses 2 | 3 | import ( 4 | "bytes" 5 | 6 | . "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/gemini-cli/gemini" 7 | . "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/gemini/openai/responses" 8 | ) 9 | 10 | func ConvertOpenAIResponsesRequestToGeminiCLI(modelName string, inputRawJSON []byte, stream bool) []byte { 11 | rawJSON := bytes.Clone(inputRawJSON) 12 | rawJSON = ConvertOpenAIResponsesRequestToGemini(modelName, rawJSON, stream) 13 | return ConvertGeminiRequestToGeminiCLI(modelName, rawJSON, stream) 14 | } 15 | -------------------------------------------------------------------------------- /internal/translator/antigravity/openai/responses/init.go: -------------------------------------------------------------------------------- 1 | package responses 2 | 3 | import ( 4 | . "github.com/router-for-me/CLIProxyAPI/v6/internal/constant" 5 | "github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces" 6 | "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/translator" 7 | ) 8 | 9 | func init() { 10 | translator.Register( 11 | OpenaiResponse, 12 | Antigravity, 13 | ConvertOpenAIResponsesRequestToAntigravity, 14 | interfaces.TranslateResponse{ 15 | Stream: ConvertAntigravityResponseToOpenAIResponses, 16 | NonStream: ConvertAntigravityResponseToOpenAIResponsesNonStream, 17 | }, 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /internal/translator/antigravity/openai/responses/antigravity_openai-responses_request.go: -------------------------------------------------------------------------------- 1 | package responses 2 | 3 | import ( 4 | "bytes" 5 | 6 | . "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/antigravity/gemini" 7 | . "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/gemini/openai/responses" 8 | ) 9 | 10 | func ConvertOpenAIResponsesRequestToAntigravity(modelName string, inputRawJSON []byte, stream bool) []byte { 11 | rawJSON := bytes.Clone(inputRawJSON) 12 | rawJSON = ConvertOpenAIResponsesRequestToGemini(modelName, rawJSON, stream) 13 | return ConvertGeminiRequestToAntigravity(modelName, rawJSON, stream) 14 | } 15 | -------------------------------------------------------------------------------- /internal/translator/openai/openai/responses/init.go: -------------------------------------------------------------------------------- 1 | package responses 2 | 3 | import ( 4 | . "github.com/router-for-me/CLIProxyAPI/v6/internal/constant" 5 | "github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces" 6 | "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/translator" 7 | ) 8 | 9 | func init() { 10 | translator.Register( 11 | OpenaiResponse, 12 | OpenAI, 13 | ConvertOpenAIResponsesRequestToOpenAIChatCompletions, 14 | interfaces.TranslateResponse{ 15 | Stream: ConvertOpenAIChatCompletionsResponseToOpenAIResponses, 16 | NonStream: ConvertOpenAIChatCompletionsResponseToOpenAIResponsesNonStream, 17 | }, 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /internal/misc/oauth.go: -------------------------------------------------------------------------------- 1 | package misc 2 | 3 | import ( 4 | "crypto/rand" 5 | "encoding/hex" 6 | "fmt" 7 | ) 8 | 9 | // GenerateRandomState generates a cryptographically secure random state parameter 10 | // for OAuth2 flows to prevent CSRF attacks. 11 | // 12 | // Returns: 13 | // - string: A hexadecimal encoded random state string 14 | // - error: An error if the random generation fails, nil otherwise 15 | func GenerateRandomState() (string, error) { 16 | bytes := make([]byte, 16) 17 | if _, err := rand.Read(bytes); err != nil { 18 | return "", fmt.Errorf("failed to generate random bytes: %w", err) 19 | } 20 | return hex.EncodeToString(bytes), nil 21 | } 22 | -------------------------------------------------------------------------------- /sdk/translator/builtin/builtin.go: -------------------------------------------------------------------------------- 1 | // Package builtin exposes the built-in translator registrations for SDK users. 2 | package builtin 3 | 4 | import ( 5 | sdktranslator "github.com/router-for-me/CLIProxyAPI/v6/sdk/translator" 6 | 7 | _ "github.com/router-for-me/CLIProxyAPI/v6/internal/translator" 8 | ) 9 | 10 | // Registry exposes the default registry populated with all built-in translators. 11 | func Registry() *sdktranslator.Registry { 12 | return sdktranslator.Default() 13 | } 14 | 15 | // Pipeline returns a pipeline that already contains the built-in translators. 16 | func Pipeline() *sdktranslator.Pipeline { 17 | return sdktranslator.NewPipeline(sdktranslator.Default()) 18 | } 19 | -------------------------------------------------------------------------------- /internal/api/handlers/management/quota.go: -------------------------------------------------------------------------------- 1 | package management 2 | 3 | import "github.com/gin-gonic/gin" 4 | 5 | // Quota exceeded toggles 6 | func (h *Handler) GetSwitchProject(c *gin.Context) { 7 | c.JSON(200, gin.H{"switch-project": h.cfg.QuotaExceeded.SwitchProject}) 8 | } 9 | func (h *Handler) PutSwitchProject(c *gin.Context) { 10 | h.updateBoolField(c, func(v bool) { h.cfg.QuotaExceeded.SwitchProject = v }) 11 | } 12 | 13 | func (h *Handler) GetSwitchPreviewModel(c *gin.Context) { 14 | c.JSON(200, gin.H{"switch-preview-model": h.cfg.QuotaExceeded.SwitchPreviewModel}) 15 | } 16 | func (h *Handler) PutSwitchPreviewModel(c *gin.Context) { 17 | h.updateBoolField(c, func(v bool) { h.cfg.QuotaExceeded.SwitchPreviewModel = v }) 18 | } 19 | -------------------------------------------------------------------------------- /internal/misc/claude_code_instructions.go: -------------------------------------------------------------------------------- 1 | // Package misc provides miscellaneous utility functions and embedded data for the CLI Proxy API. 2 | // This package contains general-purpose helpers and embedded resources that do not fit into 3 | // more specific domain packages. It includes embedded instructional text for Claude Code-related operations. 4 | package misc 5 | 6 | import _ "embed" 7 | 8 | // ClaudeCodeInstructions holds the content of the claude_code_instructions.txt file, 9 | // which is embedded into the application binary at compile time. This variable 10 | // contains specific instructions for Claude Code model interactions and code generation guidance. 11 | // 12 | //go:embed claude_code_instructions.txt 13 | var ClaudeCodeInstructions string 14 | -------------------------------------------------------------------------------- /internal/interfaces/types.go: -------------------------------------------------------------------------------- 1 | // Package interfaces provides type aliases for backwards compatibility with translator functions. 2 | // It defines common interface types used throughout the CLI Proxy API for request and response 3 | // transformation operations, maintaining compatibility with the SDK translator package. 4 | package interfaces 5 | 6 | import sdktranslator "github.com/router-for-me/CLIProxyAPI/v6/sdk/translator" 7 | 8 | // Backwards compatible aliases for translator function types. 9 | type TranslateRequestFunc = sdktranslator.RequestTransform 10 | 11 | type TranslateResponseFunc = sdktranslator.ResponseStreamTransform 12 | 13 | type TranslateResponseNonStreamFunc = sdktranslator.ResponseNonStreamTransform 14 | 15 | type TranslateResponse = sdktranslator.ResponseTransform 16 | -------------------------------------------------------------------------------- /internal/translator/gemini/gemini/init.go: -------------------------------------------------------------------------------- 1 | package gemini 2 | 3 | import ( 4 | . "github.com/router-for-me/CLIProxyAPI/v6/internal/constant" 5 | "github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces" 6 | "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/translator" 7 | ) 8 | 9 | // Register a no-op response translator and a request normalizer for Gemini→Gemini. 10 | // The request converter ensures missing or invalid roles are normalized to valid values. 11 | func init() { 12 | translator.Register( 13 | Gemini, 14 | Gemini, 15 | ConvertGeminiRequestToGemini, 16 | interfaces.TranslateResponse{ 17 | Stream: PassthroughGeminiResponseStream, 18 | NonStream: PassthroughGeminiResponseNonStream, 19 | TokenCount: GeminiTokenCount, 20 | }, 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /internal/misc/credentials.go: -------------------------------------------------------------------------------- 1 | package misc 2 | 3 | import ( 4 | "fmt" 5 | "path/filepath" 6 | "strings" 7 | 8 | log "github.com/sirupsen/logrus" 9 | ) 10 | 11 | // Separator used to visually group related log lines. 12 | var credentialSeparator = strings.Repeat("-", 67) 13 | 14 | // LogSavingCredentials emits a consistent log message when persisting auth material. 15 | func LogSavingCredentials(path string) { 16 | if path == "" { 17 | return 18 | } 19 | // Use filepath.Clean so logs remain stable even if callers pass redundant separators. 20 | fmt.Printf("Saving credentials to %s\n", filepath.Clean(path)) 21 | } 22 | 23 | // LogCredentialSeparator adds a visual separator to group auth/key processing logs. 24 | func LogCredentialSeparator() { 25 | log.Debug(credentialSeparator) 26 | } 27 | -------------------------------------------------------------------------------- /internal/auth/models.go: -------------------------------------------------------------------------------- 1 | // Package auth provides authentication functionality for various AI service providers. 2 | // It includes interfaces and implementations for token storage and authentication methods. 3 | package auth 4 | 5 | // TokenStorage defines the interface for storing authentication tokens. 6 | // Implementations of this interface should provide methods to persist 7 | // authentication tokens to a file system location. 8 | type TokenStorage interface { 9 | // SaveTokenToFile persists authentication tokens to the specified file path. 10 | // 11 | // Parameters: 12 | // - authFilePath: The file path where the authentication tokens should be saved 13 | // 14 | // Returns: 15 | // - error: An error if the save operation fails, nil otherwise 16 | SaveTokenToFile(authFilePath string) error 17 | } 18 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | cli-proxy-api: 3 | image: ${CLI_PROXY_IMAGE:-eceasy/cli-proxy-api:latest} 4 | pull_policy: always 5 | build: 6 | context: . 7 | dockerfile: Dockerfile 8 | args: 9 | VERSION: ${VERSION:-dev} 10 | COMMIT: ${COMMIT:-none} 11 | BUILD_DATE: ${BUILD_DATE:-unknown} 12 | container_name: cli-proxy-api 13 | # env_file: 14 | # - .env 15 | environment: 16 | DEPLOY: ${DEPLOY:-} 17 | ports: 18 | - "8317:8317" 19 | - "8085:8085" 20 | - "1455:1455" 21 | - "54545:54545" 22 | - "51121:51121" 23 | - "11451:11451" 24 | volumes: 25 | - ./config.yaml:/CLIProxyAPI/config.yaml 26 | - ./auths:/root/.cli-proxy-api 27 | - ./logs:/CLIProxyAPI/logs 28 | restart: unless-stopped 29 | -------------------------------------------------------------------------------- /sdk/cliproxy/auth/status.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | // Status represents the lifecycle state of an Auth entry. 4 | type Status string 5 | 6 | const ( 7 | // StatusUnknown means the auth state could not be determined. 8 | StatusUnknown Status = "unknown" 9 | // StatusActive indicates the auth is valid and ready for execution. 10 | StatusActive Status = "active" 11 | // StatusPending indicates the auth is waiting for an external action, such as MFA. 12 | StatusPending Status = "pending" 13 | // StatusRefreshing indicates the auth is undergoing a refresh flow. 14 | StatusRefreshing Status = "refreshing" 15 | // StatusError indicates the auth is temporarily unavailable due to errors. 16 | StatusError Status = "error" 17 | // StatusDisabled marks the auth as intentionally disabled. 18 | StatusDisabled Status = "disabled" 19 | ) 20 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.24-alpine AS builder 2 | 3 | WORKDIR /app 4 | 5 | COPY go.mod go.sum ./ 6 | 7 | RUN go mod download 8 | 9 | COPY . . 10 | 11 | ARG VERSION=dev 12 | ARG COMMIT=none 13 | ARG BUILD_DATE=unknown 14 | 15 | RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w -X 'main.Version=${VERSION}' -X 'main.Commit=${COMMIT}' -X 'main.BuildDate=${BUILD_DATE}'" -o ./CLIProxyAPI ./cmd/server/ 16 | 17 | FROM alpine:3.22.0 18 | 19 | RUN apk add --no-cache tzdata 20 | 21 | RUN mkdir /CLIProxyAPI 22 | 23 | COPY --from=builder ./app/CLIProxyAPI /CLIProxyAPI/CLIProxyAPI 24 | 25 | COPY config.example.yaml /CLIProxyAPI/config.example.yaml 26 | 27 | WORKDIR /CLIProxyAPI 28 | 29 | EXPOSE 8317 30 | 31 | ENV TZ=Asia/Shanghai 32 | 33 | RUN cp /usr/share/zoneinfo/${TZ} /etc/localtime && echo "${TZ}" > /etc/timezone 34 | 35 | CMD ["./CLIProxyAPI"] -------------------------------------------------------------------------------- /internal/cmd/auth_manager.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | sdkAuth "github.com/router-for-me/CLIProxyAPI/v6/sdk/auth" 5 | ) 6 | 7 | // newAuthManager creates a new authentication manager instance with all supported 8 | // authenticators and a file-based token store. It initializes authenticators for 9 | // Gemini, Codex, Claude, and Qwen providers. 10 | // 11 | // Returns: 12 | // - *sdkAuth.Manager: A configured authentication manager instance 13 | func newAuthManager() *sdkAuth.Manager { 14 | store := sdkAuth.GetTokenStore() 15 | manager := sdkAuth.NewManager(store, 16 | sdkAuth.NewGeminiAuthenticator(), 17 | sdkAuth.NewCodexAuthenticator(), 18 | sdkAuth.NewClaudeAuthenticator(), 19 | sdkAuth.NewQwenAuthenticator(), 20 | sdkAuth.NewIFlowAuthenticator(), 21 | sdkAuth.NewAntigravityAuthenticator(), 22 | ) 23 | return manager 24 | } 25 | -------------------------------------------------------------------------------- /internal/interfaces/error_message.go: -------------------------------------------------------------------------------- 1 | // Package interfaces defines the core interfaces and shared structures for the CLI Proxy API server. 2 | // These interfaces provide a common contract for different components of the application, 3 | // such as AI service clients, API handlers, and data models. 4 | package interfaces 5 | 6 | import "net/http" 7 | 8 | // ErrorMessage encapsulates an error with an associated HTTP status code. 9 | // This structure is used to provide detailed error information including 10 | // both the HTTP status and the underlying error. 11 | type ErrorMessage struct { 12 | // StatusCode is the HTTP status code returned by the API. 13 | StatusCode int 14 | 15 | // Error is the underlying error that occurred. 16 | Error error 17 | 18 | // Addon contains additional headers to be added to the response. 19 | Addon http.Header 20 | } 21 | -------------------------------------------------------------------------------- /sdk/cliproxy/model_registry.go: -------------------------------------------------------------------------------- 1 | package cliproxy 2 | 3 | import "github.com/router-for-me/CLIProxyAPI/v6/internal/registry" 4 | 5 | // ModelInfo re-exports the registry model info structure. 6 | type ModelInfo = registry.ModelInfo 7 | 8 | // ModelRegistry describes registry operations consumed by external callers. 9 | type ModelRegistry interface { 10 | RegisterClient(clientID, clientProvider string, models []*ModelInfo) 11 | UnregisterClient(clientID string) 12 | SetModelQuotaExceeded(clientID, modelID string) 13 | ClearModelQuotaExceeded(clientID, modelID string) 14 | ClientSupportsModel(clientID, modelID string) bool 15 | GetAvailableModels(handlerType string) []map[string]any 16 | } 17 | 18 | // GlobalModelRegistry returns the shared registry instance. 19 | func GlobalModelRegistry() ModelRegistry { 20 | return registry.GetGlobalRegistry() 21 | } 22 | -------------------------------------------------------------------------------- /sdk/auth/store_registry.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "sync" 5 | 6 | coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" 7 | ) 8 | 9 | var ( 10 | storeMu sync.RWMutex 11 | registeredStore coreauth.Store 12 | ) 13 | 14 | // RegisterTokenStore sets the global token store used by the authentication helpers. 15 | func RegisterTokenStore(store coreauth.Store) { 16 | storeMu.Lock() 17 | registeredStore = store 18 | storeMu.Unlock() 19 | } 20 | 21 | // GetTokenStore returns the globally registered token store. 22 | func GetTokenStore() coreauth.Store { 23 | storeMu.RLock() 24 | s := registeredStore 25 | storeMu.RUnlock() 26 | if s != nil { 27 | return s 28 | } 29 | storeMu.Lock() 30 | defer storeMu.Unlock() 31 | if registeredStore == nil { 32 | registeredStore = NewFileTokenStore() 33 | } 34 | return registeredStore 35 | } 36 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | builds: 2 | - id: "cli-proxy-api" 3 | env: 4 | - CGO_ENABLED=0 5 | goos: 6 | - linux 7 | - windows 8 | - darwin 9 | goarch: 10 | - amd64 11 | - arm64 12 | main: ./cmd/server/ 13 | binary: cli-proxy-api 14 | ldflags: 15 | - -s -w -X 'main.Version={{.Version}}' -X 'main.Commit={{.ShortCommit}}' -X 'main.BuildDate={{.Date}}' 16 | archives: 17 | - id: "cli-proxy-api" 18 | format: tar.gz 19 | format_overrides: 20 | - goos: windows 21 | format: zip 22 | files: 23 | - LICENSE 24 | - README.md 25 | - README_CN.md 26 | - config.example.yaml 27 | 28 | checksum: 29 | name_template: 'checksums.txt' 30 | 31 | snapshot: 32 | name_template: "{{ incpatch .Version }}-next" 33 | 34 | changelog: 35 | sort: asc 36 | filters: 37 | exclude: 38 | - '^docs:' 39 | - '^test:' 40 | -------------------------------------------------------------------------------- /.github/workflows/pr-path-guard.yml: -------------------------------------------------------------------------------- 1 | name: translator-path-guard 2 | 3 | on: 4 | pull_request: 5 | types: 6 | - opened 7 | - synchronize 8 | - reopened 9 | 10 | jobs: 11 | ensure-no-translator-changes: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | with: 16 | fetch-depth: 0 17 | - name: Detect internal/translator changes 18 | id: changed-files 19 | uses: tj-actions/changed-files@v45 20 | with: 21 | files: | 22 | internal/translator/** 23 | - name: Fail when restricted paths change 24 | if: steps.changed-files.outputs.any_changed == 'true' 25 | run: | 26 | echo "Changes under internal/translator are not allowed in pull requests." 27 | echo "You need to create an issue for our maintenance team to make the necessary changes." 28 | exit 1 29 | -------------------------------------------------------------------------------- /internal/interfaces/api_handler.go: -------------------------------------------------------------------------------- 1 | // Package interfaces defines the core interfaces and shared structures for the CLI Proxy API server. 2 | // These interfaces provide a common contract for different components of the application, 3 | // such as AI service clients, API handlers, and data models. 4 | package interfaces 5 | 6 | // APIHandler defines the interface that all API handlers must implement. 7 | // This interface provides methods for identifying handler types and retrieving 8 | // supported models for different AI service endpoints. 9 | type APIHandler interface { 10 | // HandlerType returns the type identifier for this API handler. 11 | // This is used to determine which request/response translators to use. 12 | HandlerType() string 13 | 14 | // Models returns a list of supported models for this API handler. 15 | // Each model is represented as a map containing model metadata. 16 | Models() []map[string]any 17 | } 18 | -------------------------------------------------------------------------------- /sdk/auth/interfaces.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "time" 7 | 8 | "github.com/router-for-me/CLIProxyAPI/v6/internal/config" 9 | coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" 10 | ) 11 | 12 | var ErrRefreshNotSupported = errors.New("cliproxy auth: refresh not supported") 13 | 14 | // LoginOptions captures generic knobs shared across authenticators. 15 | // Provider-specific logic can inspect Metadata for extra parameters. 16 | type LoginOptions struct { 17 | NoBrowser bool 18 | ProjectID string 19 | Metadata map[string]string 20 | Prompt func(prompt string) (string, error) 21 | } 22 | 23 | // Authenticator manages login and optional refresh flows for a provider. 24 | type Authenticator interface { 25 | Provider() string 26 | Login(ctx context.Context, cfg *config.Config, opts *LoginOptions) (*coreauth.Auth, error) 27 | RefreshLead() *time.Duration 28 | } 29 | -------------------------------------------------------------------------------- /internal/misc/copy-example-config.go: -------------------------------------------------------------------------------- 1 | package misc 2 | 3 | import ( 4 | "io" 5 | "os" 6 | "path/filepath" 7 | 8 | log "github.com/sirupsen/logrus" 9 | ) 10 | 11 | func CopyConfigTemplate(src, dst string) error { 12 | in, err := os.Open(src) 13 | if err != nil { 14 | return err 15 | } 16 | defer func() { 17 | if errClose := in.Close(); errClose != nil { 18 | log.WithError(errClose).Warn("failed to close source config file") 19 | } 20 | }() 21 | 22 | if err = os.MkdirAll(filepath.Dir(dst), 0o700); err != nil { 23 | return err 24 | } 25 | 26 | out, err := os.OpenFile(dst, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o600) 27 | if err != nil { 28 | return err 29 | } 30 | defer func() { 31 | if errClose := out.Close(); errClose != nil { 32 | log.WithError(errClose).Warn("failed to close destination config file") 33 | } 34 | }() 35 | 36 | if _, err = io.Copy(out, in); err != nil { 37 | return err 38 | } 39 | return out.Sync() 40 | } 41 | -------------------------------------------------------------------------------- /internal/constant/constant.go: -------------------------------------------------------------------------------- 1 | // Package constant defines provider name constants used throughout the CLI Proxy API. 2 | // These constants identify different AI service providers and their variants, 3 | // ensuring consistent naming across the application. 4 | package constant 5 | 6 | const ( 7 | // Gemini represents the Google Gemini provider identifier. 8 | Gemini = "gemini" 9 | 10 | // GeminiCLI represents the Google Gemini CLI provider identifier. 11 | GeminiCLI = "gemini-cli" 12 | 13 | // Codex represents the OpenAI Codex provider identifier. 14 | Codex = "codex" 15 | 16 | // Claude represents the Anthropic Claude provider identifier. 17 | Claude = "claude" 18 | 19 | // OpenAI represents the OpenAI provider identifier. 20 | OpenAI = "openai" 21 | 22 | // OpenaiResponse represents the OpenAI response format identifier. 23 | OpenaiResponse = "openai-response" 24 | 25 | // Antigravity represents the Antigravity response format identifier. 26 | Antigravity = "antigravity" 27 | ) 28 | -------------------------------------------------------------------------------- /sdk/cliproxy/auth/errors.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | // Error describes an authentication related failure in a provider agnostic format. 4 | type Error struct { 5 | // Code is a short machine readable identifier. 6 | Code string `json:"code,omitempty"` 7 | // Message is a human readable description of the failure. 8 | Message string `json:"message"` 9 | // Retryable indicates whether a retry might fix the issue automatically. 10 | Retryable bool `json:"retryable"` 11 | // HTTPStatus optionally records an HTTP-like status code for the error. 12 | HTTPStatus int `json:"http_status,omitempty"` 13 | } 14 | 15 | // Error implements the error interface. 16 | func (e *Error) Error() string { 17 | if e == nil { 18 | return "" 19 | } 20 | if e.Code == "" { 21 | return e.Message 22 | } 23 | return e.Code + ": " + e.Message 24 | } 25 | 26 | // StatusCode implements optional status accessor for manager decision making. 27 | func (e *Error) StatusCode() int { 28 | if e == nil { 29 | return 0 30 | } 31 | return e.HTTPStatus 32 | } 33 | -------------------------------------------------------------------------------- /internal/translator/gemini/gemini/gemini_gemini_response.go: -------------------------------------------------------------------------------- 1 | package gemini 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "fmt" 7 | ) 8 | 9 | // PassthroughGeminiResponseStream forwards Gemini responses unchanged. 10 | func PassthroughGeminiResponseStream(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, _ *any) []string { 11 | if bytes.HasPrefix(rawJSON, []byte("data:")) { 12 | rawJSON = bytes.TrimSpace(rawJSON[5:]) 13 | } 14 | 15 | if bytes.Equal(rawJSON, []byte("[DONE]")) { 16 | return []string{} 17 | } 18 | 19 | return []string{string(rawJSON)} 20 | } 21 | 22 | // PassthroughGeminiResponseNonStream forwards Gemini responses unchanged. 23 | func PassthroughGeminiResponseNonStream(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, _ *any) string { 24 | return string(rawJSON) 25 | } 26 | 27 | func GeminiTokenCount(ctx context.Context, count int64) string { 28 | return fmt.Sprintf(`{"totalTokens":%d,"promptTokensDetails":[{"modality":"TEXT","tokenCount":%d}]}`, count, count) 29 | } 30 | -------------------------------------------------------------------------------- /sdk/cliproxy/watcher.go: -------------------------------------------------------------------------------- 1 | package cliproxy 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/router-for-me/CLIProxyAPI/v6/internal/config" 7 | "github.com/router-for-me/CLIProxyAPI/v6/internal/watcher" 8 | coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" 9 | ) 10 | 11 | func defaultWatcherFactory(configPath, authDir string, reload func(*config.Config)) (*WatcherWrapper, error) { 12 | w, err := watcher.NewWatcher(configPath, authDir, reload) 13 | if err != nil { 14 | return nil, err 15 | } 16 | 17 | return &WatcherWrapper{ 18 | start: func(ctx context.Context) error { 19 | return w.Start(ctx) 20 | }, 21 | stop: func() error { 22 | return w.Stop() 23 | }, 24 | setConfig: func(cfg *config.Config) { 25 | w.SetConfig(cfg) 26 | }, 27 | snapshotAuths: func() []*coreauth.Auth { return w.SnapshotCoreAuths() }, 28 | setUpdateQueue: func(queue chan<- watcher.AuthUpdate) { 29 | w.SetAuthUpdateQueue(queue) 30 | }, 31 | dispatchRuntimeUpdate: func(update watcher.AuthUpdate) bool { 32 | return w.DispatchRuntimeAuthUpdate(update) 33 | }, 34 | }, nil 35 | } 36 | -------------------------------------------------------------------------------- /sdk/auth/errors.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces" 7 | ) 8 | 9 | // ProjectSelectionError indicates that the user must choose a specific project ID. 10 | type ProjectSelectionError struct { 11 | Email string 12 | Projects []interfaces.GCPProjectProjects 13 | } 14 | 15 | func (e *ProjectSelectionError) Error() string { 16 | if e == nil { 17 | return "cliproxy auth: project selection required" 18 | } 19 | return fmt.Sprintf("cliproxy auth: project selection required for %s", e.Email) 20 | } 21 | 22 | // ProjectsDisplay returns the projects list for caller presentation. 23 | func (e *ProjectSelectionError) ProjectsDisplay() []interfaces.GCPProjectProjects { 24 | if e == nil { 25 | return nil 26 | } 27 | return e.Projects 28 | } 29 | 30 | // EmailRequiredError indicates that the calling context must provide an email or alias. 31 | type EmailRequiredError struct { 32 | Prompt string 33 | } 34 | 35 | func (e *EmailRequiredError) Error() string { 36 | if e == nil || e.Prompt == "" { 37 | return "cliproxy auth: email is required" 38 | } 39 | return e.Prompt 40 | } 41 | -------------------------------------------------------------------------------- /internal/wsrelay/message.go: -------------------------------------------------------------------------------- 1 | package wsrelay 2 | 3 | // Message represents the JSON payload exchanged with websocket clients. 4 | type Message struct { 5 | ID string `json:"id"` 6 | Type string `json:"type"` 7 | Payload map[string]any `json:"payload,omitempty"` 8 | } 9 | 10 | const ( 11 | // MessageTypeHTTPReq identifies an HTTP-style request envelope. 12 | MessageTypeHTTPReq = "http_request" 13 | // MessageTypeHTTPResp identifies a non-streaming HTTP response envelope. 14 | MessageTypeHTTPResp = "http_response" 15 | // MessageTypeStreamStart marks the beginning of a streaming response. 16 | MessageTypeStreamStart = "stream_start" 17 | // MessageTypeStreamChunk carries a streaming response chunk. 18 | MessageTypeStreamChunk = "stream_chunk" 19 | // MessageTypeStreamEnd marks the completion of a streaming response. 20 | MessageTypeStreamEnd = "stream_end" 21 | // MessageTypeError carries an error response. 22 | MessageTypeError = "error" 23 | // MessageTypePing represents ping messages from clients. 24 | MessageTypePing = "ping" 25 | // MessageTypePong represents pong responses back to clients. 26 | MessageTypePong = "pong" 27 | ) 28 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: goreleaser 2 | 3 | on: 4 | push: 5 | # run only against tags 6 | tags: 7 | - '*' 8 | 9 | permissions: 10 | contents: write 11 | 12 | jobs: 13 | goreleaser: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | with: 18 | fetch-depth: 0 19 | - run: git fetch --force --tags 20 | - uses: actions/setup-go@v4 21 | with: 22 | go-version: '>=1.24.0' 23 | cache: true 24 | - name: Generate Build Metadata 25 | run: | 26 | echo VERSION=`git describe --tags --always --dirty` >> $GITHUB_ENV 27 | echo COMMIT=`git rev-parse --short HEAD` >> $GITHUB_ENV 28 | echo BUILD_DATE=`date -u +%Y-%m-%dT%H:%M:%SZ` >> $GITHUB_ENV 29 | - uses: goreleaser/goreleaser-action@v4 30 | with: 31 | distribution: goreleaser 32 | version: latest 33 | args: release --clean 34 | env: 35 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 36 | VERSION: ${{ env.VERSION }} 37 | COMMIT: ${{ env.COMMIT }} 38 | BUILD_DATE: ${{ env.BUILD_DATE }} 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **CLI Type** 14 | What type of CLI account do you use? (gemini-cli, gemini, codex, claude code or openai-compatibility) 15 | 16 | **Model Name** 17 | What model are you using? (example: gemini-2.5-pro, claude-sonnet-4-20250514, gpt-5, etc.) 18 | 19 | **LLM Client** 20 | What LLM Client are you using? (example: roo-code, cline, claude code, etc.) 21 | 22 | **Request Information** 23 | The best way is to paste the cURL command of the HTTP request here. 24 | Alternatively, you can set `request-log: true` in the `config.yaml` file and then upload the detailed log file. 25 | 26 | **Expected behavior** 27 | A clear and concise description of what you expected to happen. 28 | 29 | **Screenshots** 30 | If applicable, add screenshots to help explain your problem. 31 | 32 | **OS Type** 33 | - OS: [e.g. macOS] 34 | - Version [e.g. 15.6.0] 35 | 36 | **Additional context** 37 | Add any other context about the problem here. 38 | -------------------------------------------------------------------------------- /internal/cmd/antigravity_login.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/router-for-me/CLIProxyAPI/v6/internal/config" 8 | sdkAuth "github.com/router-for-me/CLIProxyAPI/v6/sdk/auth" 9 | log "github.com/sirupsen/logrus" 10 | ) 11 | 12 | // DoAntigravityLogin triggers the OAuth flow for the antigravity provider and saves tokens. 13 | func DoAntigravityLogin(cfg *config.Config, options *LoginOptions) { 14 | if options == nil { 15 | options = &LoginOptions{} 16 | } 17 | 18 | manager := newAuthManager() 19 | authOpts := &sdkAuth.LoginOptions{ 20 | NoBrowser: options.NoBrowser, 21 | Metadata: map[string]string{}, 22 | Prompt: options.Prompt, 23 | } 24 | 25 | record, savedPath, err := manager.Login(context.Background(), "antigravity", cfg, authOpts) 26 | if err != nil { 27 | log.Errorf("Antigravity authentication failed: %v", err) 28 | return 29 | } 30 | 31 | if savedPath != "" { 32 | fmt.Printf("Authentication saved to %s\n", savedPath) 33 | } 34 | if record != nil && record.Label != "" { 35 | fmt.Printf("Authenticated as %s\n", record.Label) 36 | } 37 | fmt.Println("Antigravity authentication successful!") 38 | } 39 | -------------------------------------------------------------------------------- /sdk/auth/refresh_registry.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "time" 5 | 6 | cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" 7 | ) 8 | 9 | func init() { 10 | registerRefreshLead("codex", func() Authenticator { return NewCodexAuthenticator() }) 11 | registerRefreshLead("claude", func() Authenticator { return NewClaudeAuthenticator() }) 12 | registerRefreshLead("qwen", func() Authenticator { return NewQwenAuthenticator() }) 13 | registerRefreshLead("iflow", func() Authenticator { return NewIFlowAuthenticator() }) 14 | registerRefreshLead("gemini", func() Authenticator { return NewGeminiAuthenticator() }) 15 | registerRefreshLead("gemini-cli", func() Authenticator { return NewGeminiAuthenticator() }) 16 | registerRefreshLead("antigravity", func() Authenticator { return NewAntigravityAuthenticator() }) 17 | } 18 | 19 | func registerRefreshLead(provider string, factory func() Authenticator) { 20 | cliproxyauth.RegisterRefreshLeadProvider(provider, func() *time.Duration { 21 | if factory == nil { 22 | return nil 23 | } 24 | auth := factory() 25 | if auth == nil { 26 | return nil 27 | } 28 | return auth.RefreshLead() 29 | }) 30 | } 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025-2005.9 Luis Pater 4 | Copyright (c) 2025.9-present Router-For.ME 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. -------------------------------------------------------------------------------- /internal/auth/empty/token.go: -------------------------------------------------------------------------------- 1 | // Package empty provides a no-operation token storage implementation. 2 | // This package is used when authentication tokens are not required or when 3 | // using API key-based authentication instead of OAuth tokens for any provider. 4 | package empty 5 | 6 | // EmptyStorage is a no-operation implementation of the TokenStorage interface. 7 | // It provides empty implementations for scenarios where token storage is not needed, 8 | // such as when using API keys instead of OAuth tokens for authentication. 9 | type EmptyStorage struct { 10 | // Type indicates the authentication provider type, always "empty" for this implementation. 11 | Type string `json:"type"` 12 | } 13 | 14 | // SaveTokenToFile is a no-operation implementation that always succeeds. 15 | // This method satisfies the TokenStorage interface but performs no actual file operations 16 | // since empty storage doesn't require persistent token data. 17 | // 18 | // Parameters: 19 | // - _: The file path parameter is ignored in this implementation 20 | // 21 | // Returns: 22 | // - error: Always returns nil (no error) 23 | func (ts *EmptyStorage) SaveTokenToFile(_ string) error { 24 | ts.Type = "empty" 25 | return nil 26 | } 27 | -------------------------------------------------------------------------------- /internal/util/image.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "bytes" 5 | "encoding/base64" 6 | "image" 7 | "image/draw" 8 | "image/png" 9 | ) 10 | 11 | func CreateWhiteImageBase64(aspectRatio string) (string, error) { 12 | width := 1024 13 | height := 1024 14 | 15 | switch aspectRatio { 16 | case "1:1": 17 | width = 1024 18 | height = 1024 19 | case "2:3": 20 | width = 832 21 | height = 1248 22 | case "3:2": 23 | width = 1248 24 | height = 832 25 | case "3:4": 26 | width = 864 27 | height = 1184 28 | case "4:3": 29 | width = 1184 30 | height = 864 31 | case "4:5": 32 | width = 896 33 | height = 1152 34 | case "5:4": 35 | width = 1152 36 | height = 896 37 | case "9:16": 38 | width = 768 39 | height = 1344 40 | case "16:9": 41 | width = 1344 42 | height = 768 43 | case "21:9": 44 | width = 1536 45 | height = 672 46 | } 47 | 48 | img := image.NewRGBA(image.Rect(0, 0, width, height)) 49 | draw.Draw(img, img.Bounds(), image.White, image.Point{}, draw.Src) 50 | 51 | var buf bytes.Buffer 52 | 53 | if err := png.Encode(&buf, img); err != nil { 54 | return "", err 55 | } 56 | 57 | base64String := base64.StdEncoding.EncodeToString(buf.Bytes()) 58 | return base64String, nil 59 | } 60 | -------------------------------------------------------------------------------- /docs/sdk-watcher_CN.md: -------------------------------------------------------------------------------- 1 | # SDK Watcher集成说明 2 | 3 | 本文档介绍SDK服务与文件监控器之间的增量更新队列,包括接口契约、高频变更下的处理策略以及接入步骤。 4 | 5 | ## 更新队列契约 6 | 7 | - `watcher.AuthUpdate`描述单条凭据变更,`Action`可能为`add`、`modify`或`delete`,`ID`是凭据标识。对于`add`/`modify`会携带完整的`Auth`克隆,`delete`可以省略`Auth`。 8 | - `WatcherWrapper.SetAuthUpdateQueue(chan<- watcher.AuthUpdate)`用于将服务侧创建的队列注入watcher,必须在watcher启动前完成。 9 | - 服务通过`ensureAuthUpdateQueue`创建容量为256的缓冲通道,并在`consumeAuthUpdates`中使用专职goroutine消费;消费侧会主动“抽干”积压事件,降低切换开销。 10 | 11 | ## Watcher行为 12 | 13 | - `internal/watcher/watcher.go`维护`currentAuths`快照,文件或配置事件触发后会重建快照并与旧快照对比,生成最小化的`AuthUpdate`列表。 14 | - 以凭据ID为维度对更新进行合并,同一凭据在短时间内的多次变更只会保留最新状态(例如先写后删只会下发`delete`)。 15 | - watcher内部运行异步分发循环:生产者只向内存缓冲追加事件并唤醒分发协程,即使通道暂时写满也不会阻塞文件事件线程。watcher停止时会取消分发循环,确保协程正常退出。 16 | 17 | ## 高频变更处理 18 | 19 | - 分发循环与服务消费协程相互独立,因此即便短时间内出现大量变更也不会阻塞watcher事件处理。 20 | - 背压通过两级缓冲吸收: 21 | - 分发缓冲(map + 顺序切片)会合并同一凭据的重复事件,直到消费者完成处理。 22 | - 服务端通道的256容量加上消费侧的“抽干”逻辑,可平稳处理多个突发批次。 23 | - 当通道长时间处于高压状态时,缓冲仍持续合并事件,从而在消费者恢复后一次性应用最新状态,避免重复处理无意义的中间状态。 24 | 25 | ## 接入步骤 26 | 27 | 1. 实例化SDK Service(构建器或手工创建)。 28 | 2. 在启动watcher之前调用`ensureAuthUpdateQueue`创建共享通道。 29 | 3. watcher通过工厂函数创建后立刻调用`SetAuthUpdateQueue`注入通道,然后再启动watcher。 30 | 4. Reload回调专注于配置更新;认证增量会通过队列送达,并由`handleAuthUpdate`自动应用。 31 | 32 | 遵循上述流程即可在避免全量重载的同时保持凭据变更的实时性。 33 | -------------------------------------------------------------------------------- /internal/util/header_helpers.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "net/http" 5 | "strings" 6 | ) 7 | 8 | // ApplyCustomHeadersFromAttrs applies user-defined headers stored in the provided attributes map. 9 | // Custom headers override built-in defaults when conflicts occur. 10 | func ApplyCustomHeadersFromAttrs(r *http.Request, attrs map[string]string) { 11 | if r == nil { 12 | return 13 | } 14 | applyCustomHeaders(r, extractCustomHeaders(attrs)) 15 | } 16 | 17 | func extractCustomHeaders(attrs map[string]string) map[string]string { 18 | if len(attrs) == 0 { 19 | return nil 20 | } 21 | headers := make(map[string]string) 22 | for k, v := range attrs { 23 | if !strings.HasPrefix(k, "header:") { 24 | continue 25 | } 26 | name := strings.TrimSpace(strings.TrimPrefix(k, "header:")) 27 | if name == "" { 28 | continue 29 | } 30 | val := strings.TrimSpace(v) 31 | if val == "" { 32 | continue 33 | } 34 | headers[name] = val 35 | } 36 | if len(headers) == 0 { 37 | return nil 38 | } 39 | return headers 40 | } 41 | 42 | func applyCustomHeaders(r *http.Request, headers map[string]string) { 43 | if r == nil || len(headers) == 0 { 44 | return 45 | } 46 | for k, v := range headers { 47 | if k == "" || v == "" { 48 | continue 49 | } 50 | r.Header.Set(k, v) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /internal/translator/gemini/common/safety.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "github.com/tidwall/gjson" 5 | "github.com/tidwall/sjson" 6 | ) 7 | 8 | // DefaultSafetySettings returns the default Gemini safety configuration we attach to requests. 9 | func DefaultSafetySettings() []map[string]string { 10 | return []map[string]string{ 11 | { 12 | "category": "HARM_CATEGORY_HARASSMENT", 13 | "threshold": "OFF", 14 | }, 15 | { 16 | "category": "HARM_CATEGORY_HATE_SPEECH", 17 | "threshold": "OFF", 18 | }, 19 | { 20 | "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", 21 | "threshold": "OFF", 22 | }, 23 | { 24 | "category": "HARM_CATEGORY_DANGEROUS_CONTENT", 25 | "threshold": "OFF", 26 | }, 27 | { 28 | "category": "HARM_CATEGORY_CIVIC_INTEGRITY", 29 | "threshold": "BLOCK_NONE", 30 | }, 31 | } 32 | } 33 | 34 | // AttachDefaultSafetySettings ensures the default safety settings are present when absent. 35 | // The caller must provide the target JSON path (e.g. "safetySettings" or "request.safetySettings"). 36 | func AttachDefaultSafetySettings(rawJSON []byte, path string) []byte { 37 | if gjson.GetBytes(rawJSON, path).Exists() { 38 | return rawJSON 39 | } 40 | 41 | out, err := sjson.SetBytes(rawJSON, path, DefaultSafetySettings()) 42 | if err != nil { 43 | return rawJSON 44 | } 45 | 46 | return out 47 | } 48 | -------------------------------------------------------------------------------- /internal/misc/header_utils.go: -------------------------------------------------------------------------------- 1 | // Package misc provides miscellaneous utility functions for the CLI Proxy API server. 2 | // It includes helper functions for HTTP header manipulation and other common operations 3 | // that don't fit into more specific packages. 4 | package misc 5 | 6 | import ( 7 | "net/http" 8 | "strings" 9 | ) 10 | 11 | // EnsureHeader ensures that a header exists in the target header map by checking 12 | // multiple sources in order of priority: source headers, existing target headers, 13 | // and finally the default value. It only sets the header if it's not already present 14 | // and the value is not empty after trimming whitespace. 15 | // 16 | // Parameters: 17 | // - target: The target header map to modify 18 | // - source: The source header map to check first (can be nil) 19 | // - key: The header key to ensure 20 | // - defaultValue: The default value to use if no other source provides a value 21 | func EnsureHeader(target http.Header, source http.Header, key, defaultValue string) { 22 | if target == nil { 23 | return 24 | } 25 | if source != nil { 26 | if val := strings.TrimSpace(source.Get(key)); val != "" { 27 | target.Set(key, val) 28 | return 29 | } 30 | } 31 | if strings.TrimSpace(target.Get(key)) != "" { 32 | return 33 | } 34 | if val := strings.TrimSpace(defaultValue); val != "" { 35 | target.Set(key, val) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /internal/auth/claude/anthropic.go: -------------------------------------------------------------------------------- 1 | package claude 2 | 3 | // PKCECodes holds PKCE verification codes for OAuth2 PKCE flow 4 | type PKCECodes struct { 5 | // CodeVerifier is the cryptographically random string used to correlate 6 | // the authorization request to the token request 7 | CodeVerifier string `json:"code_verifier"` 8 | // CodeChallenge is the SHA256 hash of the code verifier, base64url-encoded 9 | CodeChallenge string `json:"code_challenge"` 10 | } 11 | 12 | // ClaudeTokenData holds OAuth token information from Anthropic 13 | type ClaudeTokenData struct { 14 | // AccessToken is the OAuth2 access token for API access 15 | AccessToken string `json:"access_token"` 16 | // RefreshToken is used to obtain new access tokens 17 | RefreshToken string `json:"refresh_token"` 18 | // Email is the Anthropic account email 19 | Email string `json:"email"` 20 | // Expire is the timestamp of the token expire 21 | Expire string `json:"expired"` 22 | } 23 | 24 | // ClaudeAuthBundle aggregates authentication data after OAuth flow completion 25 | type ClaudeAuthBundle struct { 26 | // APIKey is the Anthropic API key obtained from token exchange 27 | APIKey string `json:"api_key"` 28 | // TokenData contains the OAuth tokens from the authentication flow 29 | TokenData ClaudeTokenData `json:"token_data"` 30 | // LastRefresh is the timestamp of the last token refresh 31 | LastRefresh string `json:"last_refresh"` 32 | } 33 | -------------------------------------------------------------------------------- /internal/cmd/iflow_login.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | 8 | "github.com/router-for-me/CLIProxyAPI/v6/internal/config" 9 | sdkAuth "github.com/router-for-me/CLIProxyAPI/v6/sdk/auth" 10 | log "github.com/sirupsen/logrus" 11 | ) 12 | 13 | // DoIFlowLogin performs the iFlow OAuth login via the shared authentication manager. 14 | func DoIFlowLogin(cfg *config.Config, options *LoginOptions) { 15 | if options == nil { 16 | options = &LoginOptions{} 17 | } 18 | 19 | manager := newAuthManager() 20 | 21 | promptFn := options.Prompt 22 | if promptFn == nil { 23 | promptFn = func(prompt string) (string, error) { 24 | fmt.Println() 25 | fmt.Println(prompt) 26 | var value string 27 | _, err := fmt.Scanln(&value) 28 | return value, err 29 | } 30 | } 31 | 32 | authOpts := &sdkAuth.LoginOptions{ 33 | NoBrowser: options.NoBrowser, 34 | Metadata: map[string]string{}, 35 | Prompt: promptFn, 36 | } 37 | 38 | _, savedPath, err := manager.Login(context.Background(), "iflow", cfg, authOpts) 39 | if err != nil { 40 | var emailErr *sdkAuth.EmailRequiredError 41 | if errors.As(err, &emailErr) { 42 | log.Error(emailErr.Error()) 43 | return 44 | } 45 | fmt.Printf("iFlow authentication failed: %v\n", err) 46 | return 47 | } 48 | 49 | if savedPath != "" { 50 | fmt.Printf("Authentication saved to %s\n", savedPath) 51 | } 52 | 53 | fmt.Println("iFlow authentication successful!") 54 | } 55 | -------------------------------------------------------------------------------- /internal/auth/iflow/iflow_token.go: -------------------------------------------------------------------------------- 1 | package iflow 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | 9 | "github.com/router-for-me/CLIProxyAPI/v6/internal/misc" 10 | ) 11 | 12 | // IFlowTokenStorage persists iFlow OAuth credentials alongside the derived API key. 13 | type IFlowTokenStorage struct { 14 | AccessToken string `json:"access_token"` 15 | RefreshToken string `json:"refresh_token"` 16 | LastRefresh string `json:"last_refresh"` 17 | Expire string `json:"expired"` 18 | APIKey string `json:"api_key"` 19 | Email string `json:"email"` 20 | TokenType string `json:"token_type"` 21 | Scope string `json:"scope"` 22 | Cookie string `json:"cookie"` 23 | Type string `json:"type"` 24 | } 25 | 26 | // SaveTokenToFile serialises the token storage to disk. 27 | func (ts *IFlowTokenStorage) SaveTokenToFile(authFilePath string) error { 28 | misc.LogSavingCredentials(authFilePath) 29 | ts.Type = "iflow" 30 | if err := os.MkdirAll(filepath.Dir(authFilePath), 0o700); err != nil { 31 | return fmt.Errorf("iflow token: create directory failed: %w", err) 32 | } 33 | 34 | f, err := os.Create(authFilePath) 35 | if err != nil { 36 | return fmt.Errorf("iflow token: create file failed: %w", err) 37 | } 38 | defer func() { _ = f.Close() }() 39 | 40 | if err = json.NewEncoder(f).Encode(ts); err != nil { 41 | return fmt.Errorf("iflow token: encode token failed: %w", err) 42 | } 43 | return nil 44 | } 45 | -------------------------------------------------------------------------------- /internal/translator/gemini-cli/openai/responses/gemini-cli_openai-responses_response.go: -------------------------------------------------------------------------------- 1 | package responses 2 | 3 | import ( 4 | "context" 5 | 6 | . "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/gemini/openai/responses" 7 | "github.com/tidwall/gjson" 8 | ) 9 | 10 | func ConvertGeminiCLIResponseToOpenAIResponses(ctx context.Context, modelName string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) []string { 11 | responseResult := gjson.GetBytes(rawJSON, "response") 12 | if responseResult.Exists() { 13 | rawJSON = []byte(responseResult.Raw) 14 | } 15 | return ConvertGeminiResponseToOpenAIResponses(ctx, modelName, originalRequestRawJSON, requestRawJSON, rawJSON, param) 16 | } 17 | 18 | func ConvertGeminiCLIResponseToOpenAIResponsesNonStream(ctx context.Context, modelName string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) string { 19 | responseResult := gjson.GetBytes(rawJSON, "response") 20 | if responseResult.Exists() { 21 | rawJSON = []byte(responseResult.Raw) 22 | } 23 | 24 | requestResult := gjson.GetBytes(originalRequestRawJSON, "request") 25 | if responseResult.Exists() { 26 | originalRequestRawJSON = []byte(requestResult.Raw) 27 | } 28 | 29 | requestResult = gjson.GetBytes(requestRawJSON, "request") 30 | if responseResult.Exists() { 31 | requestRawJSON = []byte(requestResult.Raw) 32 | } 33 | 34 | return ConvertGeminiResponseToOpenAIResponsesNonStream(ctx, modelName, originalRequestRawJSON, requestRawJSON, rawJSON, param) 35 | } 36 | -------------------------------------------------------------------------------- /.github/workflows/docker-image.yml: -------------------------------------------------------------------------------- 1 | name: docker-image 2 | 3 | on: 4 | push: 5 | tags: 6 | - v* 7 | 8 | env: 9 | APP_NAME: CLIProxyAPI 10 | DOCKERHUB_REPO: eceasy/cli-proxy-api 11 | 12 | jobs: 13 | docker: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v4 18 | - name: Set up QEMU 19 | uses: docker/setup-qemu-action@v3 20 | - name: Set up Docker Buildx 21 | uses: docker/setup-buildx-action@v3 22 | - name: Login to DockerHub 23 | uses: docker/login-action@v3 24 | with: 25 | username: ${{ secrets.DOCKERHUB_USERNAME }} 26 | password: ${{ secrets.DOCKERHUB_TOKEN }} 27 | - name: Generate Build Metadata 28 | run: | 29 | echo VERSION=`git describe --tags --always --dirty` >> $GITHUB_ENV 30 | echo COMMIT=`git rev-parse --short HEAD` >> $GITHUB_ENV 31 | echo BUILD_DATE=`date -u +%Y-%m-%dT%H:%M:%SZ` >> $GITHUB_ENV 32 | - name: Build and push 33 | uses: docker/build-push-action@v6 34 | with: 35 | context: . 36 | platforms: | 37 | linux/amd64 38 | linux/arm64 39 | push: true 40 | build-args: | 41 | VERSION=${{ env.VERSION }} 42 | COMMIT=${{ env.COMMIT }} 43 | BUILD_DATE=${{ env.BUILD_DATE }} 44 | tags: | 45 | ${{ env.DOCKERHUB_REPO }}:latest 46 | ${{ env.DOCKERHUB_REPO }}:${{ env.VERSION }} 47 | -------------------------------------------------------------------------------- /internal/translator/antigravity/openai/responses/antigravity_openai-responses_response.go: -------------------------------------------------------------------------------- 1 | package responses 2 | 3 | import ( 4 | "context" 5 | 6 | . "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/gemini/openai/responses" 7 | "github.com/tidwall/gjson" 8 | ) 9 | 10 | func ConvertAntigravityResponseToOpenAIResponses(ctx context.Context, modelName string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) []string { 11 | responseResult := gjson.GetBytes(rawJSON, "response") 12 | if responseResult.Exists() { 13 | rawJSON = []byte(responseResult.Raw) 14 | } 15 | return ConvertGeminiResponseToOpenAIResponses(ctx, modelName, originalRequestRawJSON, requestRawJSON, rawJSON, param) 16 | } 17 | 18 | func ConvertAntigravityResponseToOpenAIResponsesNonStream(ctx context.Context, modelName string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) string { 19 | responseResult := gjson.GetBytes(rawJSON, "response") 20 | if responseResult.Exists() { 21 | rawJSON = []byte(responseResult.Raw) 22 | } 23 | 24 | requestResult := gjson.GetBytes(originalRequestRawJSON, "request") 25 | if responseResult.Exists() { 26 | originalRequestRawJSON = []byte(requestResult.Raw) 27 | } 28 | 29 | requestResult = gjson.GetBytes(requestRawJSON, "request") 30 | if responseResult.Exists() { 31 | requestRawJSON = []byte(requestResult.Raw) 32 | } 33 | 34 | return ConvertGeminiResponseToOpenAIResponsesNonStream(ctx, modelName, originalRequestRawJSON, requestRawJSON, rawJSON, param) 35 | } 36 | -------------------------------------------------------------------------------- /sdk/cliproxy/providers.go: -------------------------------------------------------------------------------- 1 | package cliproxy 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/router-for-me/CLIProxyAPI/v6/internal/config" 7 | "github.com/router-for-me/CLIProxyAPI/v6/internal/watcher" 8 | ) 9 | 10 | // NewFileTokenClientProvider returns the default token-backed client loader. 11 | func NewFileTokenClientProvider() TokenClientProvider { 12 | return &fileTokenClientProvider{} 13 | } 14 | 15 | type fileTokenClientProvider struct{} 16 | 17 | func (p *fileTokenClientProvider) Load(ctx context.Context, cfg *config.Config) (*TokenClientResult, error) { 18 | // Stateless executors handle tokens 19 | _ = ctx 20 | _ = cfg 21 | return &TokenClientResult{SuccessfulAuthed: 0}, nil 22 | } 23 | 24 | // NewAPIKeyClientProvider returns the default API key client loader that reuses existing logic. 25 | func NewAPIKeyClientProvider() APIKeyClientProvider { 26 | return &apiKeyClientProvider{} 27 | } 28 | 29 | type apiKeyClientProvider struct{} 30 | 31 | func (p *apiKeyClientProvider) Load(ctx context.Context, cfg *config.Config) (*APIKeyClientResult, error) { 32 | geminiCount, vertexCompatCount, claudeCount, codexCount, openAICompat := watcher.BuildAPIKeyClients(cfg) 33 | if ctx != nil { 34 | select { 35 | case <-ctx.Done(): 36 | return nil, ctx.Err() 37 | default: 38 | } 39 | } 40 | return &APIKeyClientResult{ 41 | GeminiKeyCount: geminiCount, 42 | VertexCompatKeyCount: vertexCompatCount, 43 | ClaudeKeyCount: claudeCount, 44 | CodexKeyCount: codexCount, 45 | OpenAICompatCount: openAICompat, 46 | }, nil 47 | } 48 | -------------------------------------------------------------------------------- /internal/translator/openai/openai/chat-completions/openai_openai_request.go: -------------------------------------------------------------------------------- 1 | // Package openai provides request translation functionality for OpenAI to Gemini CLI API compatibility. 2 | // It converts OpenAI Chat Completions requests into Gemini CLI compatible JSON using gjson/sjson only. 3 | package chat_completions 4 | 5 | import ( 6 | "bytes" 7 | "github.com/tidwall/sjson" 8 | ) 9 | 10 | // ConvertOpenAIRequestToOpenAI converts an OpenAI Chat Completions request (raw JSON) 11 | // into a complete Gemini CLI request JSON. All JSON construction uses sjson and lookups use gjson. 12 | // 13 | // Parameters: 14 | // - modelName: The name of the model to use for the request 15 | // - rawJSON: The raw JSON request data from the OpenAI API 16 | // - stream: A boolean indicating if the request is for a streaming response (unused in current implementation) 17 | // 18 | // Returns: 19 | // - []byte: The transformed request data in Gemini CLI API format 20 | func ConvertOpenAIRequestToOpenAI(modelName string, inputRawJSON []byte, _ bool) []byte { 21 | // Update the "model" field in the JSON payload with the provided modelName 22 | // The sjson.SetBytes function returns a new byte slice with the updated JSON. 23 | updatedJSON, err := sjson.SetBytes(inputRawJSON, "model", modelName) 24 | if err != nil { 25 | // If there's an error, return the original JSON or handle the error appropriately. 26 | // For now, we'll return the original, but in a real scenario, logging or a more robust error 27 | // handling mechanism would be needed. 28 | return bytes.Clone(inputRawJSON) 29 | } 30 | return updatedJSON 31 | } 32 | -------------------------------------------------------------------------------- /internal/translator/openai/gemini-cli/openai_gemini_request.go: -------------------------------------------------------------------------------- 1 | // Package geminiCLI provides request translation functionality for Gemini to OpenAI API. 2 | // It handles parsing and transforming Gemini API requests into OpenAI Chat Completions API format, 3 | // extracting model information, generation config, message contents, and tool declarations. 4 | // The package performs JSON data transformation to ensure compatibility 5 | // between Gemini API format and OpenAI API's expected format. 6 | package geminiCLI 7 | 8 | import ( 9 | "bytes" 10 | 11 | . "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/openai/gemini" 12 | "github.com/tidwall/gjson" 13 | "github.com/tidwall/sjson" 14 | ) 15 | 16 | // ConvertGeminiCLIRequestToOpenAI parses and transforms a Gemini API request into OpenAI Chat Completions API format. 17 | // It extracts the model name, generation config, message contents, and tool declarations 18 | // from the raw JSON request and returns them in the format expected by the OpenAI API. 19 | func ConvertGeminiCLIRequestToOpenAI(modelName string, inputRawJSON []byte, stream bool) []byte { 20 | rawJSON := bytes.Clone(inputRawJSON) 21 | rawJSON = []byte(gjson.GetBytes(rawJSON, "request").Raw) 22 | rawJSON, _ = sjson.SetBytes(rawJSON, "model", modelName) 23 | if gjson.GetBytes(rawJSON, "systemInstruction").Exists() { 24 | rawJSON, _ = sjson.SetRawBytes(rawJSON, "system_instruction", []byte(gjson.GetBytes(rawJSON, "systemInstruction").Raw)) 25 | rawJSON, _ = sjson.DeleteBytes(rawJSON, "systemInstruction") 26 | } 27 | 28 | return ConvertGeminiRequestToOpenAI(modelName, rawJSON, stream) 29 | } 30 | -------------------------------------------------------------------------------- /sdk/translator/helpers.go: -------------------------------------------------------------------------------- 1 | package translator 2 | 3 | import "context" 4 | 5 | // TranslateRequestByFormatName converts a request payload between schemas by their string identifiers. 6 | func TranslateRequestByFormatName(from, to Format, model string, rawJSON []byte, stream bool) []byte { 7 | return TranslateRequest(from, to, model, rawJSON, stream) 8 | } 9 | 10 | // HasResponseTransformerByFormatName reports whether a response translator exists between two schemas. 11 | func HasResponseTransformerByFormatName(from, to Format) bool { 12 | return HasResponseTransformer(from, to) 13 | } 14 | 15 | // TranslateStreamByFormatName converts streaming responses between schemas by their string identifiers. 16 | func TranslateStreamByFormatName(ctx context.Context, from, to Format, model string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) []string { 17 | return TranslateStream(ctx, from, to, model, originalRequestRawJSON, requestRawJSON, rawJSON, param) 18 | } 19 | 20 | // TranslateNonStreamByFormatName converts non-streaming responses between schemas by their string identifiers. 21 | func TranslateNonStreamByFormatName(ctx context.Context, from, to Format, model string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) string { 22 | return TranslateNonStream(ctx, from, to, model, originalRequestRawJSON, requestRawJSON, rawJSON, param) 23 | } 24 | 25 | // TranslateTokenCountByFormatName converts token counts between schemas by their string identifiers. 26 | func TranslateTokenCountByFormatName(ctx context.Context, from, to Format, count int64, rawJSON []byte) string { 27 | return TranslateTokenCount(ctx, from, to, count, rawJSON) 28 | } 29 | -------------------------------------------------------------------------------- /internal/cmd/anthropic_login.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "os" 8 | 9 | "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/claude" 10 | "github.com/router-for-me/CLIProxyAPI/v6/internal/config" 11 | sdkAuth "github.com/router-for-me/CLIProxyAPI/v6/sdk/auth" 12 | log "github.com/sirupsen/logrus" 13 | ) 14 | 15 | // DoClaudeLogin triggers the Claude OAuth flow through the shared authentication manager. 16 | // It initiates the OAuth authentication process for Anthropic Claude services and saves 17 | // the authentication tokens to the configured auth directory. 18 | // 19 | // Parameters: 20 | // - cfg: The application configuration 21 | // - options: Login options including browser behavior and prompts 22 | func DoClaudeLogin(cfg *config.Config, options *LoginOptions) { 23 | if options == nil { 24 | options = &LoginOptions{} 25 | } 26 | 27 | manager := newAuthManager() 28 | 29 | authOpts := &sdkAuth.LoginOptions{ 30 | NoBrowser: options.NoBrowser, 31 | Metadata: map[string]string{}, 32 | Prompt: options.Prompt, 33 | } 34 | 35 | _, savedPath, err := manager.Login(context.Background(), "claude", cfg, authOpts) 36 | if err != nil { 37 | var authErr *claude.AuthenticationError 38 | if errors.As(err, &authErr) { 39 | log.Error(claude.GetUserFriendlyMessage(authErr)) 40 | if authErr.Type == claude.ErrPortInUse.Type { 41 | os.Exit(claude.ErrPortInUse.Code) 42 | } 43 | return 44 | } 45 | fmt.Printf("Claude authentication failed: %v\n", err) 46 | return 47 | } 48 | 49 | if savedPath != "" { 50 | fmt.Printf("Authentication saved to %s\n", savedPath) 51 | } 52 | 53 | fmt.Println("Claude authentication successful!") 54 | } 55 | -------------------------------------------------------------------------------- /internal/util/claude_thinking.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "github.com/tidwall/gjson" 5 | "github.com/tidwall/sjson" 6 | ) 7 | 8 | // ApplyClaudeThinkingConfig applies thinking configuration to a Claude API request payload. 9 | // It sets the thinking.type to "enabled" and thinking.budget_tokens to the specified budget. 10 | // If budget is nil or the payload already has thinking config, it returns the payload unchanged. 11 | func ApplyClaudeThinkingConfig(body []byte, budget *int) []byte { 12 | if budget == nil { 13 | return body 14 | } 15 | if gjson.GetBytes(body, "thinking").Exists() { 16 | return body 17 | } 18 | if *budget <= 0 { 19 | return body 20 | } 21 | updated := body 22 | updated, _ = sjson.SetBytes(updated, "thinking.type", "enabled") 23 | updated, _ = sjson.SetBytes(updated, "thinking.budget_tokens", *budget) 24 | return updated 25 | } 26 | 27 | // ResolveClaudeThinkingConfig resolves thinking configuration from metadata for Claude models. 28 | // It uses the unified ResolveThinkingConfigFromMetadata and normalizes the budget. 29 | // Returns the normalized budget (nil if thinking should not be enabled) and whether it matched. 30 | func ResolveClaudeThinkingConfig(modelName string, metadata map[string]any) (*int, bool) { 31 | if !ModelSupportsThinking(modelName) { 32 | return nil, false 33 | } 34 | budget, include, matched := ResolveThinkingConfigFromMetadata(modelName, metadata) 35 | if !matched { 36 | return nil, false 37 | } 38 | if include != nil && !*include { 39 | return nil, true 40 | } 41 | if budget == nil { 42 | return nil, true 43 | } 44 | normalized := NormalizeThinkingBudget(modelName, *budget) 45 | if normalized <= 0 { 46 | return nil, true 47 | } 48 | return &normalized, true 49 | } 50 | -------------------------------------------------------------------------------- /internal/cmd/qwen_login.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | 8 | "github.com/router-for-me/CLIProxyAPI/v6/internal/config" 9 | sdkAuth "github.com/router-for-me/CLIProxyAPI/v6/sdk/auth" 10 | log "github.com/sirupsen/logrus" 11 | ) 12 | 13 | // DoQwenLogin handles the Qwen device flow using the shared authentication manager. 14 | // It initiates the device-based authentication process for Qwen services and saves 15 | // the authentication tokens to the configured auth directory. 16 | // 17 | // Parameters: 18 | // - cfg: The application configuration 19 | // - options: Login options including browser behavior and prompts 20 | func DoQwenLogin(cfg *config.Config, options *LoginOptions) { 21 | if options == nil { 22 | options = &LoginOptions{} 23 | } 24 | 25 | manager := newAuthManager() 26 | 27 | promptFn := options.Prompt 28 | if promptFn == nil { 29 | promptFn = func(prompt string) (string, error) { 30 | fmt.Println() 31 | fmt.Println(prompt) 32 | var value string 33 | _, err := fmt.Scanln(&value) 34 | return value, err 35 | } 36 | } 37 | 38 | authOpts := &sdkAuth.LoginOptions{ 39 | NoBrowser: options.NoBrowser, 40 | Metadata: map[string]string{}, 41 | Prompt: promptFn, 42 | } 43 | 44 | _, savedPath, err := manager.Login(context.Background(), "qwen", cfg, authOpts) 45 | if err != nil { 46 | var emailErr *sdkAuth.EmailRequiredError 47 | if errors.As(err, &emailErr) { 48 | log.Error(emailErr.Error()) 49 | return 50 | } 51 | fmt.Printf("Qwen authentication failed: %v\n", err) 52 | return 53 | } 54 | 55 | if savedPath != "" { 56 | fmt.Printf("Authentication saved to %s\n", savedPath) 57 | } 58 | 59 | fmt.Println("Qwen authentication successful!") 60 | } 61 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # Example environment configuration for CLIProxyAPI. 2 | # Copy this file to `.env` and uncomment the variables you need. 3 | # 4 | # NOTE: Environment variables are only required when using remote storage options. 5 | # For local file-based storage (default), no environment variables need to be set. 6 | 7 | # ------------------------------------------------------------------------------ 8 | # Management Web UI 9 | # ------------------------------------------------------------------------------ 10 | # MANAGEMENT_PASSWORD=change-me-to-a-strong-password 11 | 12 | # ------------------------------------------------------------------------------ 13 | # Postgres Token Store (optional) 14 | # ------------------------------------------------------------------------------ 15 | # PGSTORE_DSN=postgresql://user:pass@localhost:5432/cliproxy 16 | # PGSTORE_SCHEMA=public 17 | # PGSTORE_LOCAL_PATH=/var/lib/cliproxy 18 | 19 | # ------------------------------------------------------------------------------ 20 | # Git-Backed Config Store (optional) 21 | # ------------------------------------------------------------------------------ 22 | # GITSTORE_GIT_URL=https://github.com/your-org/cli-proxy-config.git 23 | # GITSTORE_GIT_USERNAME=git-user 24 | # GITSTORE_GIT_TOKEN=ghp_your_personal_access_token 25 | # GITSTORE_LOCAL_PATH=/data/cliproxy/gitstore 26 | 27 | # ------------------------------------------------------------------------------ 28 | # Object Store Token Store (optional) 29 | # ------------------------------------------------------------------------------ 30 | # OBJECTSTORE_ENDPOINT=https://s3.your-cloud.example.com 31 | # OBJECTSTORE_BUCKET=cli-proxy-config 32 | # OBJECTSTORE_ACCESS_KEY=your_access_key 33 | # OBJECTSTORE_SECRET_KEY=your_secret_key 34 | # OBJECTSTORE_LOCAL_PATH=/data/cliproxy/objectstore 35 | -------------------------------------------------------------------------------- /internal/translator/codex/openai/responses/codex_openai-responses_response.go: -------------------------------------------------------------------------------- 1 | package responses 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "fmt" 7 | 8 | "github.com/tidwall/gjson" 9 | "github.com/tidwall/sjson" 10 | ) 11 | 12 | // ConvertCodexResponseToOpenAIResponses converts OpenAI Chat Completions streaming chunks 13 | // to OpenAI Responses SSE events (response.*). 14 | 15 | func ConvertCodexResponseToOpenAIResponses(ctx context.Context, modelName string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) []string { 16 | if bytes.HasPrefix(rawJSON, []byte("data:")) { 17 | rawJSON = bytes.TrimSpace(rawJSON[5:]) 18 | if typeResult := gjson.GetBytes(rawJSON, "type"); typeResult.Exists() { 19 | typeStr := typeResult.String() 20 | if typeStr == "response.created" || typeStr == "response.in_progress" || typeStr == "response.completed" { 21 | rawJSON, _ = sjson.SetBytes(rawJSON, "response.instructions", gjson.GetBytes(originalRequestRawJSON, "instructions").String()) 22 | } 23 | } 24 | out := fmt.Sprintf("data: %s", string(rawJSON)) 25 | return []string{out} 26 | } 27 | return []string{string(rawJSON)} 28 | } 29 | 30 | // ConvertCodexResponseToOpenAIResponsesNonStream builds a single Responses JSON 31 | // from a non-streaming OpenAI Chat Completions response. 32 | func ConvertCodexResponseToOpenAIResponsesNonStream(_ context.Context, modelName string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, _ *any) string { 33 | rootResult := gjson.ParseBytes(rawJSON) 34 | // Verify this is a response.completed event 35 | if rootResult.Get("type").String() != "response.completed" { 36 | return "" 37 | } 38 | responseResult := rootResult.Get("response") 39 | template := responseResult.Raw 40 | template, _ = sjson.Set(template, "instructions", gjson.GetBytes(originalRequestRawJSON, "instructions").String()) 41 | return template 42 | } 43 | -------------------------------------------------------------------------------- /internal/auth/codex/openai.go: -------------------------------------------------------------------------------- 1 | package codex 2 | 3 | // PKCECodes holds the verification codes for the OAuth2 PKCE (Proof Key for Code Exchange) flow. 4 | // PKCE is an extension to the Authorization Code flow to prevent CSRF and authorization code injection attacks. 5 | type PKCECodes struct { 6 | // CodeVerifier is the cryptographically random string used to correlate 7 | // the authorization request to the token request 8 | CodeVerifier string `json:"code_verifier"` 9 | // CodeChallenge is the SHA256 hash of the code verifier, base64url-encoded 10 | CodeChallenge string `json:"code_challenge"` 11 | } 12 | 13 | // CodexTokenData holds the OAuth token information obtained from OpenAI. 14 | // It includes the ID token, access token, refresh token, and associated user details. 15 | type CodexTokenData struct { 16 | // IDToken is the JWT ID token containing user claims 17 | IDToken string `json:"id_token"` 18 | // AccessToken is the OAuth2 access token for API access 19 | AccessToken string `json:"access_token"` 20 | // RefreshToken is used to obtain new access tokens 21 | RefreshToken string `json:"refresh_token"` 22 | // AccountID is the OpenAI account identifier 23 | AccountID string `json:"account_id"` 24 | // Email is the OpenAI account email 25 | Email string `json:"email"` 26 | // Expire is the timestamp of the token expire 27 | Expire string `json:"expired"` 28 | } 29 | 30 | // CodexAuthBundle aggregates all authentication-related data after the OAuth flow is complete. 31 | // This includes the API key, token data, and the timestamp of the last refresh. 32 | type CodexAuthBundle struct { 33 | // APIKey is the OpenAI API key obtained from token exchange 34 | APIKey string `json:"api_key"` 35 | // TokenData contains the OAuth tokens from the authentication flow 36 | TokenData CodexTokenData `json:"token_data"` 37 | // LastRefresh is the timestamp of the last token refresh 38 | LastRefresh string `json:"last_refresh"` 39 | } 40 | -------------------------------------------------------------------------------- /sdk/auth/gemini.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/gemini" 9 | // legacy client removed 10 | "github.com/router-for-me/CLIProxyAPI/v6/internal/config" 11 | coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" 12 | ) 13 | 14 | // GeminiAuthenticator implements the login flow for Google Gemini CLI accounts. 15 | type GeminiAuthenticator struct{} 16 | 17 | // NewGeminiAuthenticator constructs a Gemini authenticator. 18 | func NewGeminiAuthenticator() *GeminiAuthenticator { 19 | return &GeminiAuthenticator{} 20 | } 21 | 22 | func (a *GeminiAuthenticator) Provider() string { 23 | return "gemini" 24 | } 25 | 26 | func (a *GeminiAuthenticator) RefreshLead() *time.Duration { 27 | return nil 28 | } 29 | 30 | func (a *GeminiAuthenticator) Login(ctx context.Context, cfg *config.Config, opts *LoginOptions) (*coreauth.Auth, error) { 31 | if cfg == nil { 32 | return nil, fmt.Errorf("cliproxy auth: configuration is required") 33 | } 34 | if ctx == nil { 35 | ctx = context.Background() 36 | } 37 | if opts == nil { 38 | opts = &LoginOptions{} 39 | } 40 | 41 | var ts gemini.GeminiTokenStorage 42 | if opts.ProjectID != "" { 43 | ts.ProjectID = opts.ProjectID 44 | } 45 | 46 | geminiAuth := gemini.NewGeminiAuth() 47 | _, err := geminiAuth.GetAuthenticatedClient(ctx, &ts, cfg, opts.NoBrowser) 48 | if err != nil { 49 | return nil, fmt.Errorf("gemini authentication failed: %w", err) 50 | } 51 | 52 | // Skip onboarding here; rely on upstream configuration 53 | 54 | fileName := fmt.Sprintf("%s-%s.json", ts.Email, ts.ProjectID) 55 | metadata := map[string]any{ 56 | "email": ts.Email, 57 | "project_id": ts.ProjectID, 58 | } 59 | 60 | fmt.Println("Gemini authentication successful") 61 | 62 | return &coreauth.Auth{ 63 | ID: fileName, 64 | Provider: a.Provider(), 65 | FileName: fileName, 66 | Storage: &ts, 67 | Metadata: metadata, 68 | }, nil 69 | } 70 | -------------------------------------------------------------------------------- /examples/translator/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/router-for-me/CLIProxyAPI/v6/sdk/translator" 8 | _ "github.com/router-for-me/CLIProxyAPI/v6/sdk/translator/builtin" 9 | ) 10 | 11 | func main() { 12 | rawRequest := []byte(`{"messages":[{"content":[{"text":"Hello! Gemini","type":"text"}],"role":"user"}],"model":"gemini-2.5-pro","stream":false}`) 13 | fmt.Println("Has gemini->openai response translator:", translator.HasResponseTransformerByFormatName( 14 | translator.FormatGemini, 15 | translator.FormatOpenAI, 16 | )) 17 | 18 | translatedRequest := translator.TranslateRequestByFormatName( 19 | translator.FormatOpenAI, 20 | translator.FormatGemini, 21 | "gemini-2.5-pro", 22 | rawRequest, 23 | false, 24 | ) 25 | 26 | fmt.Printf("Translated request to Gemini format:\n%s\n\n", translatedRequest) 27 | 28 | claudeResponse := []byte(`{"candidates":[{"content":{"role":"model","parts":[{"thought":true,"text":"Okay, here's what's going through my mind. I need to schedule a meeting"},{"thoughtSignature":"","functionCall":{"name":"schedule_meeting","args":{"topic":"Q3 planning","attendees":["Bob","Alice"],"time":"10:00","date":"2025-03-27"}}}]},"finishReason":"STOP","avgLogprobs":-0.50018133435930523}],"usageMetadata":{"promptTokenCount":117,"candidatesTokenCount":28,"totalTokenCount":474,"trafficType":"PROVISIONED_THROUGHPUT","promptTokensDetails":[{"modality":"TEXT","tokenCount":117}],"candidatesTokensDetails":[{"modality":"TEXT","tokenCount":28}],"thoughtsTokenCount":329},"modelVersion":"gemini-2.5-pro","createTime":"2025-08-15T04:12:55.249090Z","responseId":"x7OeaIKaD6CU48APvNXDyA4"}`) 29 | 30 | convertedResponse := translator.TranslateNonStreamByFormatName( 31 | context.Background(), 32 | translator.FormatGemini, 33 | translator.FormatOpenAI, 34 | "gemini-2.5-pro", 35 | rawRequest, 36 | translatedRequest, 37 | claudeResponse, 38 | nil, 39 | ) 40 | 41 | fmt.Printf("Converted response for OpenAI clients:\n%s\n", convertedResponse) 42 | } 43 | -------------------------------------------------------------------------------- /sdk/access/manager.go: -------------------------------------------------------------------------------- 1 | package access 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "net/http" 7 | "sync" 8 | ) 9 | 10 | // Manager coordinates authentication providers. 11 | type Manager struct { 12 | mu sync.RWMutex 13 | providers []Provider 14 | } 15 | 16 | // NewManager constructs an empty manager. 17 | func NewManager() *Manager { 18 | return &Manager{} 19 | } 20 | 21 | // SetProviders replaces the active provider list. 22 | func (m *Manager) SetProviders(providers []Provider) { 23 | if m == nil { 24 | return 25 | } 26 | cloned := make([]Provider, len(providers)) 27 | copy(cloned, providers) 28 | m.mu.Lock() 29 | m.providers = cloned 30 | m.mu.Unlock() 31 | } 32 | 33 | // Providers returns a snapshot of the active providers. 34 | func (m *Manager) Providers() []Provider { 35 | if m == nil { 36 | return nil 37 | } 38 | m.mu.RLock() 39 | defer m.mu.RUnlock() 40 | snapshot := make([]Provider, len(m.providers)) 41 | copy(snapshot, m.providers) 42 | return snapshot 43 | } 44 | 45 | // Authenticate evaluates providers until one succeeds. 46 | func (m *Manager) Authenticate(ctx context.Context, r *http.Request) (*Result, error) { 47 | if m == nil { 48 | return nil, nil 49 | } 50 | providers := m.Providers() 51 | if len(providers) == 0 { 52 | return nil, nil 53 | } 54 | 55 | var ( 56 | missing bool 57 | invalid bool 58 | ) 59 | 60 | for _, provider := range providers { 61 | if provider == nil { 62 | continue 63 | } 64 | res, err := provider.Authenticate(ctx, r) 65 | if err == nil { 66 | return res, nil 67 | } 68 | if errors.Is(err, ErrNotHandled) { 69 | continue 70 | } 71 | if errors.Is(err, ErrNoCredentials) { 72 | missing = true 73 | continue 74 | } 75 | if errors.Is(err, ErrInvalidCredential) { 76 | invalid = true 77 | continue 78 | } 79 | return nil, err 80 | } 81 | 82 | if invalid { 83 | return nil, ErrInvalidCredential 84 | } 85 | if missing { 86 | return nil, ErrNoCredentials 87 | } 88 | return nil, ErrNoCredentials 89 | } 90 | -------------------------------------------------------------------------------- /docker-build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # build.sh - Linux/macOS Build Script 4 | # 5 | # This script automates the process of building and running the Docker container 6 | # with version information dynamically injected at build time. 7 | 8 | # Exit immediately if a command exits with a non-zero status. 9 | set -euo pipefail 10 | 11 | # --- Step 1: Choose Environment --- 12 | echo "Please select an option:" 13 | echo "1) Run using Pre-built Image (Recommended)" 14 | echo "2) Build from Source and Run (For Developers)" 15 | read -r -p "Enter choice [1-2]: " choice 16 | 17 | # --- Step 2: Execute based on choice --- 18 | case "$choice" in 19 | 1) 20 | echo "--- Running with Pre-built Image ---" 21 | docker compose up -d --remove-orphans --no-build 22 | echo "Services are starting from remote image." 23 | echo "Run 'docker compose logs -f' to see the logs." 24 | ;; 25 | 2) 26 | echo "--- Building from Source and Running ---" 27 | 28 | # Get Version Information 29 | VERSION="$(git describe --tags --always --dirty)" 30 | COMMIT="$(git rev-parse --short HEAD)" 31 | BUILD_DATE="$(date -u +%Y-%m-%dT%H:%M:%SZ)" 32 | 33 | echo "Building with the following info:" 34 | echo " Version: ${VERSION}" 35 | echo " Commit: ${COMMIT}" 36 | echo " Build Date: ${BUILD_DATE}" 37 | echo "----------------------------------------" 38 | 39 | # Build and start the services with a local-only image tag 40 | export CLI_PROXY_IMAGE="cli-proxy-api:local" 41 | 42 | echo "Building the Docker image..." 43 | docker compose build \ 44 | --build-arg VERSION="${VERSION}" \ 45 | --build-arg COMMIT="${COMMIT}" \ 46 | --build-arg BUILD_DATE="${BUILD_DATE}" 47 | 48 | echo "Starting the services..." 49 | docker compose up -d --remove-orphans --pull never 50 | 51 | echo "Build complete. Services are starting." 52 | echo "Run 'docker compose logs -f' to see the logs." 53 | ;; 54 | *) 55 | echo "Invalid choice. Please enter 1 or 2." 56 | exit 1 57 | ;; 58 | esac -------------------------------------------------------------------------------- /internal/misc/codex_instructions.go: -------------------------------------------------------------------------------- 1 | // Package misc provides miscellaneous utility functions and embedded data for the CLI Proxy API. 2 | // This package contains general-purpose helpers and embedded resources that do not fit into 3 | // more specific domain packages. It includes embedded instructional text for Codex-related operations. 4 | package misc 5 | 6 | import ( 7 | "embed" 8 | _ "embed" 9 | "strings" 10 | ) 11 | 12 | //go:embed codex_instructions 13 | var codexInstructionsDir embed.FS 14 | 15 | func CodexInstructionsForModel(modelName, systemInstructions string) (bool, string) { 16 | entries, _ := codexInstructionsDir.ReadDir("codex_instructions") 17 | 18 | lastPrompt := "" 19 | lastCodexPrompt := "" 20 | lastCodexMaxPrompt := "" 21 | last51Prompt := "" 22 | last52Prompt := "" 23 | // lastReviewPrompt := "" 24 | for _, entry := range entries { 25 | content, _ := codexInstructionsDir.ReadFile("codex_instructions/" + entry.Name()) 26 | if strings.HasPrefix(systemInstructions, string(content)) { 27 | return true, "" 28 | } 29 | if strings.HasPrefix(entry.Name(), "gpt_5_codex_prompt.md") { 30 | lastCodexPrompt = string(content) 31 | } else if strings.HasPrefix(entry.Name(), "gpt-5.1-codex-max_prompt.md") { 32 | lastCodexMaxPrompt = string(content) 33 | } else if strings.HasPrefix(entry.Name(), "prompt.md") { 34 | lastPrompt = string(content) 35 | } else if strings.HasPrefix(entry.Name(), "gpt_5_1_prompt.md") { 36 | last51Prompt = string(content) 37 | } else if strings.HasPrefix(entry.Name(), "gpt_5_2_prompt.md") { 38 | last52Prompt = string(content) 39 | } else if strings.HasPrefix(entry.Name(), "review_prompt.md") { 40 | // lastReviewPrompt = string(content) 41 | } 42 | } 43 | if strings.Contains(modelName, "codex-max") { 44 | return false, lastCodexMaxPrompt 45 | } else if strings.Contains(modelName, "codex") { 46 | return false, lastCodexPrompt 47 | } else if strings.Contains(modelName, "5.1") { 48 | return false, last51Prompt 49 | } else if strings.Contains(modelName, "5.2") { 50 | return false, last52Prompt 51 | } else { 52 | return false, lastPrompt 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /internal/cmd/openai_login.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "os" 8 | 9 | "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/codex" 10 | "github.com/router-for-me/CLIProxyAPI/v6/internal/config" 11 | sdkAuth "github.com/router-for-me/CLIProxyAPI/v6/sdk/auth" 12 | log "github.com/sirupsen/logrus" 13 | ) 14 | 15 | // LoginOptions contains options for the login processes. 16 | // It provides configuration for authentication flows including browser behavior 17 | // and interactive prompting capabilities. 18 | type LoginOptions struct { 19 | // NoBrowser indicates whether to skip opening the browser automatically. 20 | NoBrowser bool 21 | 22 | // Prompt allows the caller to provide interactive input when needed. 23 | Prompt func(prompt string) (string, error) 24 | } 25 | 26 | // DoCodexLogin triggers the Codex OAuth flow through the shared authentication manager. 27 | // It initiates the OAuth authentication process for OpenAI Codex services and saves 28 | // the authentication tokens to the configured auth directory. 29 | // 30 | // Parameters: 31 | // - cfg: The application configuration 32 | // - options: Login options including browser behavior and prompts 33 | func DoCodexLogin(cfg *config.Config, options *LoginOptions) { 34 | if options == nil { 35 | options = &LoginOptions{} 36 | } 37 | 38 | manager := newAuthManager() 39 | 40 | authOpts := &sdkAuth.LoginOptions{ 41 | NoBrowser: options.NoBrowser, 42 | Metadata: map[string]string{}, 43 | Prompt: options.Prompt, 44 | } 45 | 46 | _, savedPath, err := manager.Login(context.Background(), "codex", cfg, authOpts) 47 | if err != nil { 48 | var authErr *codex.AuthenticationError 49 | if errors.As(err, &authErr) { 50 | log.Error(codex.GetUserFriendlyMessage(authErr)) 51 | if authErr.Type == codex.ErrPortInUse.Type { 52 | os.Exit(codex.ErrPortInUse.Code) 53 | } 54 | return 55 | } 56 | fmt.Printf("Codex authentication failed: %v\n", err) 57 | return 58 | } 59 | 60 | if savedPath != "" { 61 | fmt.Printf("Authentication saved to %s\n", savedPath) 62 | } 63 | fmt.Println("Codex authentication successful!") 64 | } 65 | -------------------------------------------------------------------------------- /internal/api/modules/amp/gemini_bridge.go: -------------------------------------------------------------------------------- 1 | package amp 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/gin-gonic/gin" 7 | ) 8 | 9 | // createGeminiBridgeHandler creates a handler that bridges AMP CLI's non-standard Gemini paths 10 | // to our standard Gemini handler by rewriting the request context. 11 | // 12 | // AMP CLI format: /publishers/google/models/gemini-3-pro-preview:streamGenerateContent 13 | // Standard format: /models/gemini-3-pro-preview:streamGenerateContent 14 | // 15 | // This extracts the model+method from the AMP path and sets it as the :action parameter 16 | // so the standard Gemini handler can process it. 17 | // 18 | // The handler parameter should be a Gemini-compatible handler that expects the :action param. 19 | func createGeminiBridgeHandler(handler gin.HandlerFunc) gin.HandlerFunc { 20 | return func(c *gin.Context) { 21 | // Get the full path from the catch-all parameter 22 | path := c.Param("path") 23 | 24 | // Extract model:method from AMP CLI path format 25 | // Example: /publishers/google/models/gemini-3-pro-preview:streamGenerateContent 26 | const modelsPrefix = "/models/" 27 | if idx := strings.Index(path, modelsPrefix); idx >= 0 { 28 | // Extract everything after modelsPrefix 29 | actionPart := path[idx+len(modelsPrefix):] 30 | 31 | // Check if model was mapped by FallbackHandler 32 | if mappedModel, exists := c.Get(MappedModelContextKey); exists { 33 | if strModel, ok := mappedModel.(string); ok && strModel != "" { 34 | // Replace the model part in the action 35 | // actionPart is like "model-name:method" 36 | if colonIdx := strings.Index(actionPart, ":"); colonIdx > 0 { 37 | method := actionPart[colonIdx:] // ":method" 38 | actionPart = strModel + method 39 | } 40 | } 41 | } 42 | 43 | // Set this as the :action parameter that the Gemini handler expects 44 | c.Params = append(c.Params, gin.Param{ 45 | Key: "action", 46 | Value: actionPart, 47 | }) 48 | 49 | // Call the handler 50 | handler(c) 51 | return 52 | } 53 | 54 | // If we can't parse the path, return 400 55 | c.JSON(400, gin.H{ 56 | "error": "Invalid Gemini API path format", 57 | }) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /docker-build.ps1: -------------------------------------------------------------------------------- 1 | # build.ps1 - Windows PowerShell Build Script 2 | # 3 | # This script automates the process of building and running the Docker container 4 | # with version information dynamically injected at build time. 5 | 6 | # Stop script execution on any error 7 | $ErrorActionPreference = "Stop" 8 | 9 | # --- Step 1: Choose Environment --- 10 | Write-Host "Please select an option:" 11 | Write-Host "1) Run using Pre-built Image (Recommended)" 12 | Write-Host "2) Build from Source and Run (For Developers)" 13 | $choice = Read-Host -Prompt "Enter choice [1-2]" 14 | 15 | # --- Step 2: Execute based on choice --- 16 | switch ($choice) { 17 | "1" { 18 | Write-Host "--- Running with Pre-built Image ---" 19 | docker compose up -d --remove-orphans --no-build 20 | Write-Host "Services are starting from remote image." 21 | Write-Host "Run 'docker compose logs -f' to see the logs." 22 | } 23 | "2" { 24 | Write-Host "--- Building from Source and Running ---" 25 | 26 | # Get Version Information 27 | $VERSION = (git describe --tags --always --dirty) 28 | $COMMIT = (git rev-parse --short HEAD) 29 | $BUILD_DATE = (Get-Date).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ") 30 | 31 | Write-Host "Building with the following info:" 32 | Write-Host " Version: $VERSION" 33 | Write-Host " Commit: $COMMIT" 34 | Write-Host " Build Date: $BUILD_DATE" 35 | Write-Host "----------------------------------------" 36 | 37 | # Build and start the services with a local-only image tag 38 | $env:CLI_PROXY_IMAGE = "cli-proxy-api:local" 39 | 40 | Write-Host "Building the Docker image..." 41 | docker compose build --build-arg VERSION=$VERSION --build-arg COMMIT=$COMMIT --build-arg BUILD_DATE=$BUILD_DATE 42 | 43 | Write-Host "Starting the services..." 44 | docker compose up -d --remove-orphans --pull never 45 | 46 | Write-Host "Build complete. Services are starting." 47 | Write-Host "Run 'docker compose logs -f' to see the logs." 48 | } 49 | default { 50 | Write-Host "Invalid choice. Please enter 1 or 2." 51 | exit 1 52 | } 53 | } -------------------------------------------------------------------------------- /internal/util/proxy.go: -------------------------------------------------------------------------------- 1 | // Package util provides utility functions for the CLI Proxy API server. 2 | // It includes helper functions for proxy configuration, HTTP client setup, 3 | // log level management, and other common operations used across the application. 4 | package util 5 | 6 | import ( 7 | "context" 8 | "net" 9 | "net/http" 10 | "net/url" 11 | 12 | "github.com/router-for-me/CLIProxyAPI/v6/sdk/config" 13 | log "github.com/sirupsen/logrus" 14 | "golang.org/x/net/proxy" 15 | ) 16 | 17 | // SetProxy configures the provided HTTP client with proxy settings from the configuration. 18 | // It supports SOCKS5, HTTP, and HTTPS proxies. The function modifies the client's transport 19 | // to route requests through the configured proxy server. 20 | func SetProxy(cfg *config.SDKConfig, httpClient *http.Client) *http.Client { 21 | var transport *http.Transport 22 | // Attempt to parse the proxy URL from the configuration. 23 | proxyURL, errParse := url.Parse(cfg.ProxyURL) 24 | if errParse == nil { 25 | // Handle different proxy schemes. 26 | if proxyURL.Scheme == "socks5" { 27 | // Configure SOCKS5 proxy with optional authentication. 28 | var proxyAuth *proxy.Auth 29 | if proxyURL.User != nil { 30 | username := proxyURL.User.Username() 31 | password, _ := proxyURL.User.Password() 32 | proxyAuth = &proxy.Auth{User: username, Password: password} 33 | } 34 | dialer, errSOCKS5 := proxy.SOCKS5("tcp", proxyURL.Host, proxyAuth, proxy.Direct) 35 | if errSOCKS5 != nil { 36 | log.Errorf("create SOCKS5 dialer failed: %v", errSOCKS5) 37 | return httpClient 38 | } 39 | // Set up a custom transport using the SOCKS5 dialer. 40 | transport = &http.Transport{ 41 | DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { 42 | return dialer.Dial(network, addr) 43 | }, 44 | } 45 | } else if proxyURL.Scheme == "http" || proxyURL.Scheme == "https" { 46 | // Configure HTTP or HTTPS proxy. 47 | transport = &http.Transport{Proxy: http.ProxyURL(proxyURL)} 48 | } 49 | } 50 | // If a new transport was created, apply it to the HTTP client. 51 | if transport != nil { 52 | httpClient.Transport = transport 53 | } 54 | return httpClient 55 | } 56 | -------------------------------------------------------------------------------- /internal/auth/claude/pkce.go: -------------------------------------------------------------------------------- 1 | // Package claude provides authentication and token management functionality 2 | // for Anthropic's Claude AI services. It handles OAuth2 token storage, serialization, 3 | // and retrieval for maintaining authenticated sessions with the Claude API. 4 | package claude 5 | 6 | import ( 7 | "crypto/rand" 8 | "crypto/sha256" 9 | "encoding/base64" 10 | "fmt" 11 | ) 12 | 13 | // GeneratePKCECodes generates a PKCE code verifier and challenge pair 14 | // following RFC 7636 specifications for OAuth 2.0 PKCE extension. 15 | // This provides additional security for the OAuth flow by ensuring that 16 | // only the client that initiated the request can exchange the authorization code. 17 | // 18 | // Returns: 19 | // - *PKCECodes: A struct containing the code verifier and challenge 20 | // - error: An error if the generation fails, nil otherwise 21 | func GeneratePKCECodes() (*PKCECodes, error) { 22 | // Generate code verifier: 43-128 characters, URL-safe 23 | codeVerifier, err := generateCodeVerifier() 24 | if err != nil { 25 | return nil, fmt.Errorf("failed to generate code verifier: %w", err) 26 | } 27 | 28 | // Generate code challenge using S256 method 29 | codeChallenge := generateCodeChallenge(codeVerifier) 30 | 31 | return &PKCECodes{ 32 | CodeVerifier: codeVerifier, 33 | CodeChallenge: codeChallenge, 34 | }, nil 35 | } 36 | 37 | // generateCodeVerifier creates a cryptographically random string 38 | // of 128 characters using URL-safe base64 encoding 39 | func generateCodeVerifier() (string, error) { 40 | // Generate 96 random bytes (will result in 128 base64 characters) 41 | bytes := make([]byte, 96) 42 | _, err := rand.Read(bytes) 43 | if err != nil { 44 | return "", fmt.Errorf("failed to generate random bytes: %w", err) 45 | } 46 | 47 | // Encode to URL-safe base64 without padding 48 | return base64.URLEncoding.WithPadding(base64.NoPadding).EncodeToString(bytes), nil 49 | } 50 | 51 | // generateCodeChallenge creates a SHA256 hash of the code verifier 52 | // and encodes it using URL-safe base64 encoding without padding 53 | func generateCodeChallenge(codeVerifier string) string { 54 | hash := sha256.Sum256([]byte(codeVerifier)) 55 | return base64.URLEncoding.WithPadding(base64.NoPadding).EncodeToString(hash[:]) 56 | } 57 | -------------------------------------------------------------------------------- /sdk/cliproxy/executor/types.go: -------------------------------------------------------------------------------- 1 | package executor 2 | 3 | import ( 4 | "net/http" 5 | "net/url" 6 | 7 | sdktranslator "github.com/router-for-me/CLIProxyAPI/v6/sdk/translator" 8 | ) 9 | 10 | // Request encapsulates the translated payload that will be sent to a provider executor. 11 | type Request struct { 12 | // Model is the upstream model identifier after translation. 13 | Model string 14 | // Payload is the provider specific JSON payload. 15 | Payload []byte 16 | // Format represents the provider payload schema. 17 | Format sdktranslator.Format 18 | // Metadata carries optional provider specific execution hints. 19 | Metadata map[string]any 20 | } 21 | 22 | // Options controls execution behavior for both streaming and non-streaming calls. 23 | type Options struct { 24 | // Stream toggles streaming mode. 25 | Stream bool 26 | // Alt carries optional alternate format hint (e.g. SSE JSON key). 27 | Alt string 28 | // Headers are forwarded to the provider request builder. 29 | Headers http.Header 30 | // Query contains optional query string parameters. 31 | Query url.Values 32 | // OriginalRequest preserves the inbound request bytes prior to translation. 33 | OriginalRequest []byte 34 | // SourceFormat identifies the inbound schema. 35 | SourceFormat sdktranslator.Format 36 | // Metadata carries extra execution hints shared across selection and executors. 37 | Metadata map[string]any 38 | } 39 | 40 | // Response wraps either a full provider response or metadata for streaming flows. 41 | type Response struct { 42 | // Payload is the provider response in the executor format. 43 | Payload []byte 44 | // Metadata exposes optional structured data for translators. 45 | Metadata map[string]any 46 | } 47 | 48 | // StreamChunk represents a single streaming payload unit emitted by provider executors. 49 | type StreamChunk struct { 50 | // Payload is the raw provider chunk payload. 51 | Payload []byte 52 | // Err reports any terminal error encountered while producing chunks. 53 | Err error 54 | } 55 | 56 | // StatusError represents an error that carries an HTTP-like status code. 57 | // Provider executors should implement this when possible to enable 58 | // better auth state updates on failures (e.g., 401/402/429). 59 | type StatusError interface { 60 | error 61 | StatusCode() int 62 | } 63 | -------------------------------------------------------------------------------- /internal/translator/codex/gemini-cli/codex_gemini-cli_request.go: -------------------------------------------------------------------------------- 1 | // Package geminiCLI provides request translation functionality for Gemini CLI to Codex API compatibility. 2 | // It handles parsing and transforming Gemini CLI API requests into Codex API format, 3 | // extracting model information, system instructions, message contents, and tool declarations. 4 | // The package performs JSON data transformation to ensure compatibility 5 | // between Gemini CLI API format and Codex API's expected format. 6 | package geminiCLI 7 | 8 | import ( 9 | "bytes" 10 | 11 | . "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/codex/gemini" 12 | "github.com/tidwall/gjson" 13 | "github.com/tidwall/sjson" 14 | ) 15 | 16 | // ConvertGeminiCLIRequestToCodex parses and transforms a Gemini CLI API request into Codex API format. 17 | // It extracts the model name, system instruction, message contents, and tool declarations 18 | // from the raw JSON request and returns them in the format expected by the Codex API. 19 | // The function performs the following transformations: 20 | // 1. Extracts the inner request object and promotes it to the top level 21 | // 2. Restores the model information at the top level 22 | // 3. Converts systemInstruction field to system_instruction for Codex compatibility 23 | // 4. Delegates to the Gemini-to-Codex conversion function for further processing 24 | // 25 | // Parameters: 26 | // - modelName: The name of the model to use for the request 27 | // - rawJSON: The raw JSON request data from the Gemini CLI API 28 | // - stream: A boolean indicating if the request is for a streaming response 29 | // 30 | // Returns: 31 | // - []byte: The transformed request data in Codex API format 32 | func ConvertGeminiCLIRequestToCodex(modelName string, inputRawJSON []byte, stream bool) []byte { 33 | rawJSON := bytes.Clone(inputRawJSON) 34 | 35 | rawJSON = []byte(gjson.GetBytes(rawJSON, "request").Raw) 36 | rawJSON, _ = sjson.SetBytes(rawJSON, "model", modelName) 37 | if gjson.GetBytes(rawJSON, "systemInstruction").Exists() { 38 | rawJSON, _ = sjson.SetRawBytes(rawJSON, "system_instruction", []byte(gjson.GetBytes(rawJSON, "systemInstruction").Raw)) 39 | rawJSON, _ = sjson.DeleteBytes(rawJSON, "systemInstruction") 40 | } 41 | 42 | return ConvertGeminiRequestToCodex(modelName, rawJSON, stream) 43 | } 44 | -------------------------------------------------------------------------------- /sdk/auth/manager.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/router-for-me/CLIProxyAPI/v6/internal/config" 8 | coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" 9 | ) 10 | 11 | // Manager aggregates authenticators and coordinates persistence via a token store. 12 | type Manager struct { 13 | authenticators map[string]Authenticator 14 | store coreauth.Store 15 | } 16 | 17 | // NewManager constructs a manager with the provided token store and authenticators. 18 | // If store is nil, the caller must set it later using SetStore. 19 | func NewManager(store coreauth.Store, authenticators ...Authenticator) *Manager { 20 | mgr := &Manager{ 21 | authenticators: make(map[string]Authenticator), 22 | store: store, 23 | } 24 | for i := range authenticators { 25 | mgr.Register(authenticators[i]) 26 | } 27 | return mgr 28 | } 29 | 30 | // Register adds or replaces an authenticator keyed by its provider identifier. 31 | func (m *Manager) Register(a Authenticator) { 32 | if a == nil { 33 | return 34 | } 35 | if m.authenticators == nil { 36 | m.authenticators = make(map[string]Authenticator) 37 | } 38 | m.authenticators[a.Provider()] = a 39 | } 40 | 41 | // SetStore updates the token store used for persistence. 42 | func (m *Manager) SetStore(store coreauth.Store) { 43 | m.store = store 44 | } 45 | 46 | // Login executes the provider login flow and persists the resulting auth record. 47 | func (m *Manager) Login(ctx context.Context, provider string, cfg *config.Config, opts *LoginOptions) (*coreauth.Auth, string, error) { 48 | auth, ok := m.authenticators[provider] 49 | if !ok { 50 | return nil, "", fmt.Errorf("cliproxy auth: authenticator %s not registered", provider) 51 | } 52 | 53 | record, err := auth.Login(ctx, cfg, opts) 54 | if err != nil { 55 | return nil, "", err 56 | } 57 | if record == nil { 58 | return nil, "", fmt.Errorf("cliproxy auth: authenticator %s returned nil record", provider) 59 | } 60 | 61 | if m.store == nil { 62 | return record, "", nil 63 | } 64 | 65 | if cfg != nil { 66 | if dirSetter, ok := m.store.(interface{ SetBaseDir(string) }); ok { 67 | dirSetter.SetBaseDir(cfg.AuthDir) 68 | } 69 | } 70 | 71 | savedPath, err := m.store.Save(ctx, record) 72 | if err != nil { 73 | return record, "", err 74 | } 75 | return record, savedPath, nil 76 | } 77 | -------------------------------------------------------------------------------- /internal/auth/codex/pkce.go: -------------------------------------------------------------------------------- 1 | // Package codex provides authentication and token management functionality 2 | // for OpenAI's Codex AI services. It handles OAuth2 PKCE (Proof Key for Code Exchange) 3 | // code generation for secure authentication flows. 4 | package codex 5 | 6 | import ( 7 | "crypto/rand" 8 | "crypto/sha256" 9 | "encoding/base64" 10 | "fmt" 11 | ) 12 | 13 | // GeneratePKCECodes generates a new pair of PKCE (Proof Key for Code Exchange) codes. 14 | // It creates a cryptographically random code verifier and its corresponding 15 | // SHA256 code challenge, as specified in RFC 7636. This is a critical security 16 | // feature for the OAuth 2.0 authorization code flow. 17 | func GeneratePKCECodes() (*PKCECodes, error) { 18 | // Generate code verifier: 43-128 characters, URL-safe 19 | codeVerifier, err := generateCodeVerifier() 20 | if err != nil { 21 | return nil, fmt.Errorf("failed to generate code verifier: %w", err) 22 | } 23 | 24 | // Generate code challenge using S256 method 25 | codeChallenge := generateCodeChallenge(codeVerifier) 26 | 27 | return &PKCECodes{ 28 | CodeVerifier: codeVerifier, 29 | CodeChallenge: codeChallenge, 30 | }, nil 31 | } 32 | 33 | // generateCodeVerifier creates a cryptographically secure random string to be used 34 | // as the code verifier in the PKCE flow. The verifier is a high-entropy string 35 | // that is later used to prove possession of the client that initiated the 36 | // authorization request. 37 | func generateCodeVerifier() (string, error) { 38 | // Generate 96 random bytes (will result in 128 base64 characters) 39 | bytes := make([]byte, 96) 40 | _, err := rand.Read(bytes) 41 | if err != nil { 42 | return "", fmt.Errorf("failed to generate random bytes: %w", err) 43 | } 44 | 45 | // Encode to URL-safe base64 without padding 46 | return base64.URLEncoding.WithPadding(base64.NoPadding).EncodeToString(bytes), nil 47 | } 48 | 49 | // generateCodeChallenge creates a code challenge from a given code verifier. 50 | // The challenge is derived by taking the SHA256 hash of the verifier and then 51 | // Base64 URL-encoding the result. This is sent in the initial authorization 52 | // request and later verified against the verifier. 53 | func generateCodeChallenge(codeVerifier string) string { 54 | hash := sha256.Sum256([]byte(codeVerifier)) 55 | return base64.URLEncoding.WithPadding(base64.NoPadding).EncodeToString(hash[:]) 56 | } 57 | -------------------------------------------------------------------------------- /sdk/translator/types.go: -------------------------------------------------------------------------------- 1 | // Package translator provides types and functions for converting chat requests and responses between different schemas. 2 | package translator 3 | 4 | import "context" 5 | 6 | // RequestTransform is a function type that converts a request payload from a source schema to a target schema. 7 | // It takes the model name, the raw JSON payload of the request, and a boolean indicating if the request is for a streaming response. 8 | // It returns the converted request payload as a byte slice. 9 | type RequestTransform func(model string, rawJSON []byte, stream bool) []byte 10 | 11 | // ResponseStreamTransform is a function type that converts a streaming response from a source schema to a target schema. 12 | // It takes a context, the model name, the raw JSON of the original and converted requests, the raw JSON of the current response chunk, and an optional parameter. 13 | // It returns a slice of strings, where each string is a chunk of the converted streaming response. 14 | type ResponseStreamTransform func(ctx context.Context, model string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) []string 15 | 16 | // ResponseNonStreamTransform is a function type that converts a non-streaming response from a source schema to a target schema. 17 | // It takes a context, the model name, the raw JSON of the original and converted requests, the raw JSON of the response, and an optional parameter. 18 | // It returns the converted response as a single string. 19 | type ResponseNonStreamTransform func(ctx context.Context, model string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) string 20 | 21 | // ResponseTokenCountTransform is a function type that transforms a token count from a source format to a target format. 22 | // It takes a context and the token count as an int64, and returns the transformed token count as a string. 23 | type ResponseTokenCountTransform func(ctx context.Context, count int64) string 24 | 25 | // ResponseTransform is a struct that groups together the functions for transforming streaming and non-streaming responses, 26 | // as well as token counts. 27 | type ResponseTransform struct { 28 | // Stream is the function for transforming streaming responses. 29 | Stream ResponseStreamTransform 30 | // NonStream is the function for transforming non-streaming responses. 31 | NonStream ResponseNonStreamTransform 32 | // TokenCount is the function for transforming token counts. 33 | TokenCount ResponseTokenCountTransform 34 | } 35 | -------------------------------------------------------------------------------- /internal/translator/init.go: -------------------------------------------------------------------------------- 1 | package translator 2 | 3 | import ( 4 | _ "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/claude/gemini" 5 | _ "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/claude/gemini-cli" 6 | _ "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/claude/openai/chat-completions" 7 | _ "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/claude/openai/responses" 8 | 9 | _ "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/codex/claude" 10 | _ "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/codex/gemini" 11 | _ "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/codex/gemini-cli" 12 | _ "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/codex/openai/chat-completions" 13 | _ "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/codex/openai/responses" 14 | 15 | _ "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/gemini-cli/claude" 16 | _ "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/gemini-cli/gemini" 17 | _ "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/gemini-cli/openai/chat-completions" 18 | _ "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/gemini-cli/openai/responses" 19 | 20 | _ "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/gemini/claude" 21 | _ "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/gemini/gemini" 22 | _ "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/gemini/gemini-cli" 23 | _ "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/gemini/openai/chat-completions" 24 | _ "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/gemini/openai/responses" 25 | 26 | _ "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/openai/claude" 27 | _ "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/openai/gemini" 28 | _ "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/openai/gemini-cli" 29 | _ "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/openai/openai/chat-completions" 30 | _ "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/openai/openai/responses" 31 | 32 | _ "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/antigravity/claude" 33 | _ "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/antigravity/gemini" 34 | _ "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/antigravity/openai/chat-completions" 35 | _ "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/antigravity/openai/responses" 36 | ) 37 | -------------------------------------------------------------------------------- /sdk/cliproxy/pipeline/context.go: -------------------------------------------------------------------------------- 1 | package pipeline 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | 7 | cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" 8 | cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor" 9 | sdktranslator "github.com/router-for-me/CLIProxyAPI/v6/sdk/translator" 10 | ) 11 | 12 | // Context encapsulates execution state shared across middleware, translators, and executors. 13 | type Context struct { 14 | // Request encapsulates the provider facing request payload. 15 | Request cliproxyexecutor.Request 16 | // Options carries execution flags (streaming, headers, etc.). 17 | Options cliproxyexecutor.Options 18 | // Auth references the credential selected for execution. 19 | Auth *cliproxyauth.Auth 20 | // Translator represents the pipeline responsible for schema adaptation. 21 | Translator *sdktranslator.Pipeline 22 | // HTTPClient allows middleware to customise the outbound transport per request. 23 | HTTPClient *http.Client 24 | } 25 | 26 | // Hook captures middleware callbacks around execution. 27 | type Hook interface { 28 | BeforeExecute(ctx context.Context, execCtx *Context) 29 | AfterExecute(ctx context.Context, execCtx *Context, resp cliproxyexecutor.Response, err error) 30 | OnStreamChunk(ctx context.Context, execCtx *Context, chunk cliproxyexecutor.StreamChunk) 31 | } 32 | 33 | // HookFunc aggregates optional hook implementations. 34 | type HookFunc struct { 35 | Before func(context.Context, *Context) 36 | After func(context.Context, *Context, cliproxyexecutor.Response, error) 37 | Stream func(context.Context, *Context, cliproxyexecutor.StreamChunk) 38 | } 39 | 40 | // BeforeExecute implements Hook. 41 | func (h HookFunc) BeforeExecute(ctx context.Context, execCtx *Context) { 42 | if h.Before != nil { 43 | h.Before(ctx, execCtx) 44 | } 45 | } 46 | 47 | // AfterExecute implements Hook. 48 | func (h HookFunc) AfterExecute(ctx context.Context, execCtx *Context, resp cliproxyexecutor.Response, err error) { 49 | if h.After != nil { 50 | h.After(ctx, execCtx, resp, err) 51 | } 52 | } 53 | 54 | // OnStreamChunk implements Hook. 55 | func (h HookFunc) OnStreamChunk(ctx context.Context, execCtx *Context, chunk cliproxyexecutor.StreamChunk) { 56 | if h.Stream != nil { 57 | h.Stream(ctx, execCtx, chunk) 58 | } 59 | } 60 | 61 | // RoundTripperProvider allows injection of custom HTTP transports per auth entry. 62 | type RoundTripperProvider interface { 63 | RoundTripperFor(auth *cliproxyauth.Auth) http.RoundTripper 64 | } 65 | -------------------------------------------------------------------------------- /sdk/cliproxy/rtprovider.go: -------------------------------------------------------------------------------- 1 | package cliproxy 2 | 3 | import ( 4 | "context" 5 | "net" 6 | "net/http" 7 | "net/url" 8 | "strings" 9 | "sync" 10 | 11 | coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" 12 | log "github.com/sirupsen/logrus" 13 | "golang.org/x/net/proxy" 14 | ) 15 | 16 | // defaultRoundTripperProvider returns a per-auth HTTP RoundTripper based on 17 | // the Auth.ProxyURL value. It caches transports per proxy URL string. 18 | type defaultRoundTripperProvider struct { 19 | mu sync.RWMutex 20 | cache map[string]http.RoundTripper 21 | } 22 | 23 | func newDefaultRoundTripperProvider() *defaultRoundTripperProvider { 24 | return &defaultRoundTripperProvider{cache: make(map[string]http.RoundTripper)} 25 | } 26 | 27 | // RoundTripperFor implements coreauth.RoundTripperProvider. 28 | func (p *defaultRoundTripperProvider) RoundTripperFor(auth *coreauth.Auth) http.RoundTripper { 29 | if auth == nil { 30 | return nil 31 | } 32 | proxyStr := strings.TrimSpace(auth.ProxyURL) 33 | if proxyStr == "" { 34 | return nil 35 | } 36 | p.mu.RLock() 37 | rt := p.cache[proxyStr] 38 | p.mu.RUnlock() 39 | if rt != nil { 40 | return rt 41 | } 42 | // Parse the proxy URL to determine the scheme. 43 | proxyURL, errParse := url.Parse(proxyStr) 44 | if errParse != nil { 45 | log.Errorf("parse proxy URL failed: %v", errParse) 46 | return nil 47 | } 48 | var transport *http.Transport 49 | // Handle different proxy schemes. 50 | if proxyURL.Scheme == "socks5" { 51 | // Configure SOCKS5 proxy with optional authentication. 52 | username := proxyURL.User.Username() 53 | password, _ := proxyURL.User.Password() 54 | proxyAuth := &proxy.Auth{User: username, Password: password} 55 | dialer, errSOCKS5 := proxy.SOCKS5("tcp", proxyURL.Host, proxyAuth, proxy.Direct) 56 | if errSOCKS5 != nil { 57 | log.Errorf("create SOCKS5 dialer failed: %v", errSOCKS5) 58 | return nil 59 | } 60 | // Set up a custom transport using the SOCKS5 dialer. 61 | transport = &http.Transport{ 62 | DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { 63 | return dialer.Dial(network, addr) 64 | }, 65 | } 66 | } else if proxyURL.Scheme == "http" || proxyURL.Scheme == "https" { 67 | // Configure HTTP or HTTPS proxy. 68 | transport = &http.Transport{Proxy: http.ProxyURL(proxyURL)} 69 | } else { 70 | log.Errorf("unsupported proxy scheme: %s", proxyURL.Scheme) 71 | return nil 72 | } 73 | p.mu.Lock() 74 | p.cache[proxyStr] = transport 75 | p.mu.Unlock() 76 | return transport 77 | } 78 | -------------------------------------------------------------------------------- /sdk/access/registry.go: -------------------------------------------------------------------------------- 1 | package access 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | "sync" 8 | 9 | "github.com/router-for-me/CLIProxyAPI/v6/sdk/config" 10 | ) 11 | 12 | // Provider validates credentials for incoming requests. 13 | type Provider interface { 14 | Identifier() string 15 | Authenticate(ctx context.Context, r *http.Request) (*Result, error) 16 | } 17 | 18 | // Result conveys authentication outcome. 19 | type Result struct { 20 | Provider string 21 | Principal string 22 | Metadata map[string]string 23 | } 24 | 25 | // ProviderFactory builds a provider from configuration data. 26 | type ProviderFactory func(cfg *config.AccessProvider, root *config.SDKConfig) (Provider, error) 27 | 28 | var ( 29 | registryMu sync.RWMutex 30 | registry = make(map[string]ProviderFactory) 31 | ) 32 | 33 | // RegisterProvider registers a provider factory for a given type identifier. 34 | func RegisterProvider(typ string, factory ProviderFactory) { 35 | if typ == "" || factory == nil { 36 | return 37 | } 38 | registryMu.Lock() 39 | registry[typ] = factory 40 | registryMu.Unlock() 41 | } 42 | 43 | func BuildProvider(cfg *config.AccessProvider, root *config.SDKConfig) (Provider, error) { 44 | if cfg == nil { 45 | return nil, fmt.Errorf("access: nil provider config") 46 | } 47 | registryMu.RLock() 48 | factory, ok := registry[cfg.Type] 49 | registryMu.RUnlock() 50 | if !ok { 51 | return nil, fmt.Errorf("access: provider type %q is not registered", cfg.Type) 52 | } 53 | provider, err := factory(cfg, root) 54 | if err != nil { 55 | return nil, fmt.Errorf("access: failed to build provider %q: %w", cfg.Name, err) 56 | } 57 | return provider, nil 58 | } 59 | 60 | // BuildProviders constructs providers declared in configuration. 61 | func BuildProviders(root *config.SDKConfig) ([]Provider, error) { 62 | if root == nil { 63 | return nil, nil 64 | } 65 | providers := make([]Provider, 0, len(root.Access.Providers)) 66 | for i := range root.Access.Providers { 67 | providerCfg := &root.Access.Providers[i] 68 | if providerCfg.Type == "" { 69 | continue 70 | } 71 | provider, err := BuildProvider(providerCfg, root) 72 | if err != nil { 73 | return nil, err 74 | } 75 | providers = append(providers, provider) 76 | } 77 | if len(providers) == 0 { 78 | if inline := config.MakeInlineAPIKeyProvider(root.APIKeys); inline != nil { 79 | provider, err := BuildProvider(inline, root) 80 | if err != nil { 81 | return nil, err 82 | } 83 | providers = append(providers, provider) 84 | } 85 | } 86 | return providers, nil 87 | } 88 | -------------------------------------------------------------------------------- /internal/auth/qwen/qwen_token.go: -------------------------------------------------------------------------------- 1 | // Package qwen provides authentication and token management functionality 2 | // for Alibaba's Qwen AI services. It handles OAuth2 token storage, serialization, 3 | // and retrieval for maintaining authenticated sessions with the Qwen API. 4 | package qwen 5 | 6 | import ( 7 | "encoding/json" 8 | "fmt" 9 | "os" 10 | "path/filepath" 11 | 12 | "github.com/router-for-me/CLIProxyAPI/v6/internal/misc" 13 | ) 14 | 15 | // QwenTokenStorage stores OAuth2 token information for Alibaba Qwen API authentication. 16 | // It maintains compatibility with the existing auth system while adding Qwen-specific fields 17 | // for managing access tokens, refresh tokens, and user account information. 18 | type QwenTokenStorage struct { 19 | // AccessToken is the OAuth2 access token used for authenticating API requests. 20 | AccessToken string `json:"access_token"` 21 | // RefreshToken is used to obtain new access tokens when the current one expires. 22 | RefreshToken string `json:"refresh_token"` 23 | // LastRefresh is the timestamp of the last token refresh operation. 24 | LastRefresh string `json:"last_refresh"` 25 | // ResourceURL is the base URL for API requests. 26 | ResourceURL string `json:"resource_url"` 27 | // Email is the Qwen account email address associated with this token. 28 | Email string `json:"email"` 29 | // Type indicates the authentication provider type, always "qwen" for this storage. 30 | Type string `json:"type"` 31 | // Expire is the timestamp when the current access token expires. 32 | Expire string `json:"expired"` 33 | } 34 | 35 | // SaveTokenToFile serializes the Qwen token storage to a JSON file. 36 | // This method creates the necessary directory structure and writes the token 37 | // data in JSON format to the specified file path for persistent storage. 38 | // 39 | // Parameters: 40 | // - authFilePath: The full path where the token file should be saved 41 | // 42 | // Returns: 43 | // - error: An error if the operation fails, nil otherwise 44 | func (ts *QwenTokenStorage) SaveTokenToFile(authFilePath string) error { 45 | misc.LogSavingCredentials(authFilePath) 46 | ts.Type = "qwen" 47 | if err := os.MkdirAll(filepath.Dir(authFilePath), 0700); err != nil { 48 | return fmt.Errorf("failed to create directory: %v", err) 49 | } 50 | 51 | f, err := os.Create(authFilePath) 52 | if err != nil { 53 | return fmt.Errorf("failed to create token file: %w", err) 54 | } 55 | defer func() { 56 | _ = f.Close() 57 | }() 58 | 59 | if err = json.NewEncoder(f).Encode(ts); err != nil { 60 | return fmt.Errorf("failed to write token to file: %w", err) 61 | } 62 | return nil 63 | } 64 | -------------------------------------------------------------------------------- /internal/auth/vertex/vertex_credentials.go: -------------------------------------------------------------------------------- 1 | // Package vertex provides token storage for Google Vertex AI Gemini via service account credentials. 2 | // It serialises service account JSON into an auth file that is consumed by the runtime executor. 3 | package vertex 4 | 5 | import ( 6 | "encoding/json" 7 | "fmt" 8 | "os" 9 | "path/filepath" 10 | 11 | "github.com/router-for-me/CLIProxyAPI/v6/internal/misc" 12 | log "github.com/sirupsen/logrus" 13 | ) 14 | 15 | // VertexCredentialStorage stores the service account JSON for Vertex AI access. 16 | // The content is persisted verbatim under the "service_account" key, together with 17 | // helper fields for project, location and email to improve logging and discovery. 18 | type VertexCredentialStorage struct { 19 | // ServiceAccount holds the parsed service account JSON content. 20 | ServiceAccount map[string]any `json:"service_account"` 21 | 22 | // ProjectID is derived from the service account JSON (project_id). 23 | ProjectID string `json:"project_id"` 24 | 25 | // Email is the client_email from the service account JSON. 26 | Email string `json:"email"` 27 | 28 | // Location optionally sets a default region (e.g., us-central1) for Vertex endpoints. 29 | Location string `json:"location,omitempty"` 30 | 31 | // Type is the provider identifier stored alongside credentials. Always "vertex". 32 | Type string `json:"type"` 33 | } 34 | 35 | // SaveTokenToFile writes the credential payload to the given file path in JSON format. 36 | // It ensures the parent directory exists and logs the operation for transparency. 37 | func (s *VertexCredentialStorage) SaveTokenToFile(authFilePath string) error { 38 | misc.LogSavingCredentials(authFilePath) 39 | if s == nil { 40 | return fmt.Errorf("vertex credential: storage is nil") 41 | } 42 | if s.ServiceAccount == nil { 43 | return fmt.Errorf("vertex credential: service account content is empty") 44 | } 45 | // Ensure we tag the file with the provider type. 46 | s.Type = "vertex" 47 | 48 | if err := os.MkdirAll(filepath.Dir(authFilePath), 0o700); err != nil { 49 | return fmt.Errorf("vertex credential: create directory failed: %w", err) 50 | } 51 | f, err := os.Create(authFilePath) 52 | if err != nil { 53 | return fmt.Errorf("vertex credential: create file failed: %w", err) 54 | } 55 | defer func() { 56 | if errClose := f.Close(); errClose != nil { 57 | log.Errorf("vertex credential: failed to close file: %v", errClose) 58 | } 59 | }() 60 | enc := json.NewEncoder(f) 61 | enc.SetIndent("", " ") 62 | if err = enc.Encode(s); err != nil { 63 | return fmt.Errorf("vertex credential: encode failed: %w", err) 64 | } 65 | return nil 66 | } 67 | -------------------------------------------------------------------------------- /internal/translator/claude/gemini-cli/claude_gemini-cli_request.go: -------------------------------------------------------------------------------- 1 | // Package geminiCLI provides request translation functionality for Gemini CLI to Claude Code API compatibility. 2 | // It handles parsing and transforming Gemini CLI API requests into Claude Code API format, 3 | // extracting model information, system instructions, message contents, and tool declarations. 4 | // The package performs JSON data transformation to ensure compatibility 5 | // between Gemini CLI API format and Claude Code API's expected format. 6 | package geminiCLI 7 | 8 | import ( 9 | "bytes" 10 | 11 | . "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/claude/gemini" 12 | "github.com/tidwall/gjson" 13 | "github.com/tidwall/sjson" 14 | ) 15 | 16 | // ConvertGeminiCLIRequestToClaude parses and transforms a Gemini CLI API request into Claude Code API format. 17 | // It extracts the model name, system instruction, message contents, and tool declarations 18 | // from the raw JSON request and returns them in the format expected by the Claude Code API. 19 | // The function performs the following transformations: 20 | // 1. Extracts the model information from the request 21 | // 2. Restructures the JSON to match Claude Code API format 22 | // 3. Converts system instructions to the expected format 23 | // 4. Delegates to the Gemini-to-Claude conversion function for further processing 24 | // 25 | // Parameters: 26 | // - modelName: The name of the model to use for the request 27 | // - rawJSON: The raw JSON request data from the Gemini CLI API 28 | // - stream: A boolean indicating if the request is for a streaming response 29 | // 30 | // Returns: 31 | // - []byte: The transformed request data in Claude Code API format 32 | func ConvertGeminiCLIRequestToClaude(modelName string, inputRawJSON []byte, stream bool) []byte { 33 | rawJSON := bytes.Clone(inputRawJSON) 34 | 35 | modelResult := gjson.GetBytes(rawJSON, "model") 36 | // Extract the inner request object and promote it to the top level 37 | rawJSON = []byte(gjson.GetBytes(rawJSON, "request").Raw) 38 | // Restore the model information at the top level 39 | rawJSON, _ = sjson.SetBytes(rawJSON, "model", modelResult.String()) 40 | // Convert systemInstruction field to system_instruction for Claude Code compatibility 41 | if gjson.GetBytes(rawJSON, "systemInstruction").Exists() { 42 | rawJSON, _ = sjson.SetRawBytes(rawJSON, "system_instruction", []byte(gjson.GetBytes(rawJSON, "systemInstruction").Raw)) 43 | rawJSON, _ = sjson.DeleteBytes(rawJSON, "systemInstruction") 44 | } 45 | // Delegate to the Gemini-to-Claude conversion function for further processing 46 | return ConvertGeminiRequestToClaude(modelName, rawJSON, stream) 47 | } 48 | -------------------------------------------------------------------------------- /internal/cmd/run.go: -------------------------------------------------------------------------------- 1 | // Package cmd provides command-line interface functionality for the CLI Proxy API server. 2 | // It includes authentication flows for various AI service providers, service startup, 3 | // and other command-line operations. 4 | package cmd 5 | 6 | import ( 7 | "context" 8 | "errors" 9 | "os/signal" 10 | "syscall" 11 | "time" 12 | 13 | "github.com/router-for-me/CLIProxyAPI/v6/internal/api" 14 | "github.com/router-for-me/CLIProxyAPI/v6/internal/config" 15 | "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy" 16 | log "github.com/sirupsen/logrus" 17 | ) 18 | 19 | // StartService builds and runs the proxy service using the exported SDK. 20 | // It creates a new proxy service instance, sets up signal handling for graceful shutdown, 21 | // and starts the service with the provided configuration. 22 | // 23 | // Parameters: 24 | // - cfg: The application configuration 25 | // - configPath: The path to the configuration file 26 | // - localPassword: Optional password accepted for local management requests 27 | func StartService(cfg *config.Config, configPath string, localPassword string) { 28 | builder := cliproxy.NewBuilder(). 29 | WithConfig(cfg). 30 | WithConfigPath(configPath). 31 | WithLocalManagementPassword(localPassword) 32 | 33 | ctxSignal, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) 34 | defer cancel() 35 | 36 | runCtx := ctxSignal 37 | if localPassword != "" { 38 | var keepAliveCancel context.CancelFunc 39 | runCtx, keepAliveCancel = context.WithCancel(ctxSignal) 40 | builder = builder.WithServerOptions(api.WithKeepAliveEndpoint(10*time.Second, func() { 41 | log.Warn("keep-alive endpoint idle for 10s, shutting down") 42 | keepAliveCancel() 43 | })) 44 | } 45 | 46 | service, err := builder.Build() 47 | if err != nil { 48 | log.Errorf("failed to build proxy service: %v", err) 49 | return 50 | } 51 | 52 | err = service.Run(runCtx) 53 | if err != nil && !errors.Is(err, context.Canceled) { 54 | log.Errorf("proxy service exited with error: %v", err) 55 | } 56 | } 57 | 58 | // WaitForCloudDeploy waits indefinitely for shutdown signals in cloud deploy mode 59 | // when no configuration file is available. 60 | func WaitForCloudDeploy() { 61 | // Clarify that we are intentionally idle for configuration and not running the API server. 62 | log.Info("Cloud deploy mode: No config found; standing by for configuration. API server is not started. Press Ctrl+C to exit.") 63 | 64 | ctxSignal, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) 65 | defer cancel() 66 | 67 | // Block until shutdown signal is received 68 | <-ctxSignal.Done() 69 | log.Info("Cloud deploy mode: Shutdown signal received; exiting") 70 | } 71 | -------------------------------------------------------------------------------- /internal/auth/codex/token.go: -------------------------------------------------------------------------------- 1 | // Package codex provides authentication and token management functionality 2 | // for OpenAI's Codex AI services. It handles OAuth2 token storage, serialization, 3 | // and retrieval for maintaining authenticated sessions with the Codex API. 4 | package codex 5 | 6 | import ( 7 | "encoding/json" 8 | "fmt" 9 | "os" 10 | "path/filepath" 11 | 12 | "github.com/router-for-me/CLIProxyAPI/v6/internal/misc" 13 | ) 14 | 15 | // CodexTokenStorage stores OAuth2 token information for OpenAI Codex API authentication. 16 | // It maintains compatibility with the existing auth system while adding Codex-specific fields 17 | // for managing access tokens, refresh tokens, and user account information. 18 | type CodexTokenStorage struct { 19 | // IDToken is the JWT ID token containing user claims and identity information. 20 | IDToken string `json:"id_token"` 21 | // AccessToken is the OAuth2 access token used for authenticating API requests. 22 | AccessToken string `json:"access_token"` 23 | // RefreshToken is used to obtain new access tokens when the current one expires. 24 | RefreshToken string `json:"refresh_token"` 25 | // AccountID is the OpenAI account identifier associated with this token. 26 | AccountID string `json:"account_id"` 27 | // LastRefresh is the timestamp of the last token refresh operation. 28 | LastRefresh string `json:"last_refresh"` 29 | // Email is the OpenAI account email address associated with this token. 30 | Email string `json:"email"` 31 | // Type indicates the authentication provider type, always "codex" for this storage. 32 | Type string `json:"type"` 33 | // Expire is the timestamp when the current access token expires. 34 | Expire string `json:"expired"` 35 | } 36 | 37 | // SaveTokenToFile serializes the Codex token storage to a JSON file. 38 | // This method creates the necessary directory structure and writes the token 39 | // data in JSON format to the specified file path for persistent storage. 40 | // 41 | // Parameters: 42 | // - authFilePath: The full path where the token file should be saved 43 | // 44 | // Returns: 45 | // - error: An error if the operation fails, nil otherwise 46 | func (ts *CodexTokenStorage) SaveTokenToFile(authFilePath string) error { 47 | misc.LogSavingCredentials(authFilePath) 48 | ts.Type = "codex" 49 | if err := os.MkdirAll(filepath.Dir(authFilePath), 0700); err != nil { 50 | return fmt.Errorf("failed to create directory: %v", err) 51 | } 52 | 53 | f, err := os.Create(authFilePath) 54 | if err != nil { 55 | return fmt.Errorf("failed to create token file: %w", err) 56 | } 57 | defer func() { 58 | _ = f.Close() 59 | }() 60 | 61 | if err = json.NewEncoder(f).Encode(ts); err != nil { 62 | return fmt.Errorf("failed to write token to file: %w", err) 63 | } 64 | return nil 65 | 66 | } 67 | -------------------------------------------------------------------------------- /internal/translator/gemini/gemini-cli/gemini_gemini-cli_response.go: -------------------------------------------------------------------------------- 1 | // Package gemini_cli provides response translation functionality for Gemini API to Gemini CLI API. 2 | // This package handles the conversion of Gemini API responses into Gemini CLI-compatible 3 | // JSON format, transforming streaming events and non-streaming responses into the format 4 | // expected by Gemini CLI API clients. 5 | package geminiCLI 6 | 7 | import ( 8 | "bytes" 9 | "context" 10 | "fmt" 11 | 12 | "github.com/tidwall/sjson" 13 | ) 14 | 15 | var dataTag = []byte("data:") 16 | 17 | // ConvertGeminiResponseToGeminiCLI converts Gemini streaming response format to Gemini CLI single-line JSON format. 18 | // This function processes various Gemini event types and transforms them into Gemini CLI-compatible JSON responses. 19 | // It handles thinking content, regular text content, and function calls, outputting single-line JSON 20 | // that matches the Gemini CLI API response format. 21 | // 22 | // Parameters: 23 | // - ctx: The context for the request. 24 | // - modelName: The name of the model. 25 | // - rawJSON: The raw JSON response from the Gemini API. 26 | // - param: A pointer to a parameter object for the conversion (unused). 27 | // 28 | // Returns: 29 | // - []string: A slice of strings, each containing a Gemini CLI-compatible JSON response. 30 | func ConvertGeminiResponseToGeminiCLI(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, _ *any) []string { 31 | if !bytes.HasPrefix(rawJSON, dataTag) { 32 | return []string{} 33 | } 34 | rawJSON = bytes.TrimSpace(rawJSON[5:]) 35 | 36 | if bytes.Equal(rawJSON, []byte("[DONE]")) { 37 | return []string{} 38 | } 39 | json := `{"response": {}}` 40 | rawJSON, _ = sjson.SetRawBytes([]byte(json), "response", rawJSON) 41 | return []string{string(rawJSON)} 42 | } 43 | 44 | // ConvertGeminiResponseToGeminiCLINonStream converts a non-streaming Gemini response to a non-streaming Gemini CLI response. 45 | // 46 | // Parameters: 47 | // - ctx: The context for the request. 48 | // - modelName: The name of the model. 49 | // - rawJSON: The raw JSON response from the Gemini API. 50 | // - param: A pointer to a parameter object for the conversion (unused). 51 | // 52 | // Returns: 53 | // - string: A Gemini CLI-compatible JSON response. 54 | func ConvertGeminiResponseToGeminiCLINonStream(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, _ *any) string { 55 | json := `{"response": {}}` 56 | rawJSON, _ = sjson.SetRawBytes([]byte(json), "response", rawJSON) 57 | return string(rawJSON) 58 | } 59 | 60 | func GeminiCLITokenCount(ctx context.Context, count int64) string { 61 | return fmt.Sprintf(`{"totalTokens":%d,"promptTokensDetails":[{"modality":"TEXT","tokenCount":%d}]}`, count, count) 62 | } 63 | -------------------------------------------------------------------------------- /internal/auth/claude/token.go: -------------------------------------------------------------------------------- 1 | // Package claude provides authentication and token management functionality 2 | // for Anthropic's Claude AI services. It handles OAuth2 token storage, serialization, 3 | // and retrieval for maintaining authenticated sessions with the Claude API. 4 | package claude 5 | 6 | import ( 7 | "encoding/json" 8 | "fmt" 9 | "os" 10 | "path/filepath" 11 | 12 | "github.com/router-for-me/CLIProxyAPI/v6/internal/misc" 13 | ) 14 | 15 | // ClaudeTokenStorage stores OAuth2 token information for Anthropic Claude API authentication. 16 | // It maintains compatibility with the existing auth system while adding Claude-specific fields 17 | // for managing access tokens, refresh tokens, and user account information. 18 | type ClaudeTokenStorage struct { 19 | // IDToken is the JWT ID token containing user claims and identity information. 20 | IDToken string `json:"id_token"` 21 | 22 | // AccessToken is the OAuth2 access token used for authenticating API requests. 23 | AccessToken string `json:"access_token"` 24 | 25 | // RefreshToken is used to obtain new access tokens when the current one expires. 26 | RefreshToken string `json:"refresh_token"` 27 | 28 | // LastRefresh is the timestamp of the last token refresh operation. 29 | LastRefresh string `json:"last_refresh"` 30 | 31 | // Email is the Anthropic account email address associated with this token. 32 | Email string `json:"email"` 33 | 34 | // Type indicates the authentication provider type, always "claude" for this storage. 35 | Type string `json:"type"` 36 | 37 | // Expire is the timestamp when the current access token expires. 38 | Expire string `json:"expired"` 39 | } 40 | 41 | // SaveTokenToFile serializes the Claude token storage to a JSON file. 42 | // This method creates the necessary directory structure and writes the token 43 | // data in JSON format to the specified file path for persistent storage. 44 | // 45 | // Parameters: 46 | // - authFilePath: The full path where the token file should be saved 47 | // 48 | // Returns: 49 | // - error: An error if the operation fails, nil otherwise 50 | func (ts *ClaudeTokenStorage) SaveTokenToFile(authFilePath string) error { 51 | misc.LogSavingCredentials(authFilePath) 52 | ts.Type = "claude" 53 | 54 | // Create directory structure if it doesn't exist 55 | if err := os.MkdirAll(filepath.Dir(authFilePath), 0700); err != nil { 56 | return fmt.Errorf("failed to create directory: %v", err) 57 | } 58 | 59 | // Create the token file 60 | f, err := os.Create(authFilePath) 61 | if err != nil { 62 | return fmt.Errorf("failed to create token file: %w", err) 63 | } 64 | defer func() { 65 | _ = f.Close() 66 | }() 67 | 68 | // Encode and write the token data as JSON 69 | if err = json.NewEncoder(f).Encode(ts); err != nil { 70 | return fmt.Errorf("failed to write token to file: %w", err) 71 | } 72 | return nil 73 | } 74 | -------------------------------------------------------------------------------- /internal/auth/iflow/cookie_helpers.go: -------------------------------------------------------------------------------- 1 | package iflow 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | "strings" 9 | ) 10 | 11 | // NormalizeCookie normalizes raw cookie strings for iFlow authentication flows. 12 | func NormalizeCookie(raw string) (string, error) { 13 | trimmed := strings.TrimSpace(raw) 14 | if trimmed == "" { 15 | return "", fmt.Errorf("cookie cannot be empty") 16 | } 17 | 18 | combined := strings.Join(strings.Fields(trimmed), " ") 19 | if !strings.HasSuffix(combined, ";") { 20 | combined += ";" 21 | } 22 | if !strings.Contains(combined, "BXAuth=") { 23 | return "", fmt.Errorf("cookie missing BXAuth field") 24 | } 25 | return combined, nil 26 | } 27 | 28 | // SanitizeIFlowFileName normalizes user identifiers for safe filename usage. 29 | func SanitizeIFlowFileName(raw string) string { 30 | if raw == "" { 31 | return "" 32 | } 33 | cleanEmail := strings.ReplaceAll(raw, "*", "x") 34 | var result strings.Builder 35 | for _, r := range cleanEmail { 36 | if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '_' || r == '@' || r == '.' || r == '-' { 37 | result.WriteRune(r) 38 | } 39 | } 40 | return strings.TrimSpace(result.String()) 41 | } 42 | 43 | // ExtractBXAuth extracts the BXAuth value from a cookie string. 44 | func ExtractBXAuth(cookie string) string { 45 | parts := strings.Split(cookie, ";") 46 | for _, part := range parts { 47 | part = strings.TrimSpace(part) 48 | if strings.HasPrefix(part, "BXAuth=") { 49 | return strings.TrimPrefix(part, "BXAuth=") 50 | } 51 | } 52 | return "" 53 | } 54 | 55 | // CheckDuplicateBXAuth checks if the given BXAuth value already exists in any iflow auth file. 56 | // Returns the path of the existing file if found, empty string otherwise. 57 | func CheckDuplicateBXAuth(authDir, bxAuth string) (string, error) { 58 | if bxAuth == "" { 59 | return "", nil 60 | } 61 | 62 | entries, err := os.ReadDir(authDir) 63 | if err != nil { 64 | if os.IsNotExist(err) { 65 | return "", nil 66 | } 67 | return "", fmt.Errorf("read auth dir failed: %w", err) 68 | } 69 | 70 | for _, entry := range entries { 71 | if entry.IsDir() { 72 | continue 73 | } 74 | name := entry.Name() 75 | if !strings.HasPrefix(name, "iflow-") || !strings.HasSuffix(name, ".json") { 76 | continue 77 | } 78 | 79 | filePath := filepath.Join(authDir, name) 80 | data, err := os.ReadFile(filePath) 81 | if err != nil { 82 | continue 83 | } 84 | 85 | var tokenData struct { 86 | Cookie string `json:"cookie"` 87 | } 88 | if err := json.Unmarshal(data, &tokenData); err != nil { 89 | continue 90 | } 91 | 92 | existingBXAuth := ExtractBXAuth(tokenData.Cookie) 93 | if existingBXAuth != "" && existingBXAuth == bxAuth { 94 | return filePath, nil 95 | } 96 | } 97 | 98 | return "", nil 99 | } 100 | -------------------------------------------------------------------------------- /internal/api/modules/amp/gemini_bridge_test.go: -------------------------------------------------------------------------------- 1 | package amp 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "testing" 7 | 8 | "github.com/gin-gonic/gin" 9 | ) 10 | 11 | func TestCreateGeminiBridgeHandler_ActionParameterExtraction(t *testing.T) { 12 | gin.SetMode(gin.TestMode) 13 | 14 | tests := []struct { 15 | name string 16 | path string 17 | mappedModel string // empty string means no mapping 18 | expectedAction string 19 | }{ 20 | { 21 | name: "no_mapping_uses_url_model", 22 | path: "/publishers/google/models/gemini-pro:generateContent", 23 | mappedModel: "", 24 | expectedAction: "gemini-pro:generateContent", 25 | }, 26 | { 27 | name: "mapped_model_replaces_url_model", 28 | path: "/publishers/google/models/gemini-exp:generateContent", 29 | mappedModel: "gemini-2.0-flash", 30 | expectedAction: "gemini-2.0-flash:generateContent", 31 | }, 32 | { 33 | name: "mapping_preserves_method", 34 | path: "/publishers/google/models/gemini-2.5-preview:streamGenerateContent", 35 | mappedModel: "gemini-flash", 36 | expectedAction: "gemini-flash:streamGenerateContent", 37 | }, 38 | } 39 | 40 | for _, tt := range tests { 41 | t.Run(tt.name, func(t *testing.T) { 42 | var capturedAction string 43 | 44 | mockGeminiHandler := func(c *gin.Context) { 45 | capturedAction = c.Param("action") 46 | c.JSON(http.StatusOK, gin.H{"captured": capturedAction}) 47 | } 48 | 49 | // Use the actual createGeminiBridgeHandler function 50 | bridgeHandler := createGeminiBridgeHandler(mockGeminiHandler) 51 | 52 | r := gin.New() 53 | if tt.mappedModel != "" { 54 | r.Use(func(c *gin.Context) { 55 | c.Set(MappedModelContextKey, tt.mappedModel) 56 | c.Next() 57 | }) 58 | } 59 | r.POST("/api/provider/google/v1beta1/*path", bridgeHandler) 60 | 61 | req := httptest.NewRequest(http.MethodPost, "/api/provider/google/v1beta1"+tt.path, nil) 62 | w := httptest.NewRecorder() 63 | r.ServeHTTP(w, req) 64 | 65 | if w.Code != http.StatusOK { 66 | t.Fatalf("Expected status 200, got %d", w.Code) 67 | } 68 | if capturedAction != tt.expectedAction { 69 | t.Errorf("Expected action '%s', got '%s'", tt.expectedAction, capturedAction) 70 | } 71 | }) 72 | } 73 | } 74 | 75 | func TestCreateGeminiBridgeHandler_InvalidPath(t *testing.T) { 76 | gin.SetMode(gin.TestMode) 77 | 78 | mockHandler := func(c *gin.Context) { 79 | c.JSON(http.StatusOK, gin.H{"ok": true}) 80 | } 81 | bridgeHandler := createGeminiBridgeHandler(mockHandler) 82 | 83 | r := gin.New() 84 | r.POST("/api/provider/google/v1beta1/*path", bridgeHandler) 85 | 86 | req := httptest.NewRequest(http.MethodPost, "/api/provider/google/v1beta1/invalid/path", nil) 87 | w := httptest.NewRecorder() 88 | r.ServeHTTP(w, req) 89 | 90 | if w.Code != http.StatusBadRequest { 91 | t.Errorf("Expected status 400 for invalid path, got %d", w.Code) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /internal/translator/openai/gemini-cli/openai_gemini_response.go: -------------------------------------------------------------------------------- 1 | // Package geminiCLI provides response translation functionality for OpenAI to Gemini API. 2 | // This package handles the conversion of OpenAI Chat Completions API responses into Gemini API-compatible 3 | // JSON format, transforming streaming events and non-streaming responses into the format 4 | // expected by Gemini API clients. It supports both streaming and non-streaming modes, 5 | // handling text content, tool calls, and usage metadata appropriately. 6 | package geminiCLI 7 | 8 | import ( 9 | "context" 10 | "fmt" 11 | 12 | . "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/openai/gemini" 13 | "github.com/tidwall/sjson" 14 | ) 15 | 16 | // ConvertOpenAIResponseToGeminiCLI converts OpenAI Chat Completions streaming response format to Gemini API format. 17 | // This function processes OpenAI streaming chunks and transforms them into Gemini-compatible JSON responses. 18 | // It handles text content, tool calls, and usage metadata, outputting responses that match the Gemini API format. 19 | // 20 | // Parameters: 21 | // - ctx: The context for the request. 22 | // - modelName: The name of the model. 23 | // - rawJSON: The raw JSON response from the OpenAI API. 24 | // - param: A pointer to a parameter object for the conversion. 25 | // 26 | // Returns: 27 | // - []string: A slice of strings, each containing a Gemini-compatible JSON response. 28 | func ConvertOpenAIResponseToGeminiCLI(ctx context.Context, modelName string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) []string { 29 | outputs := ConvertOpenAIResponseToGemini(ctx, modelName, originalRequestRawJSON, requestRawJSON, rawJSON, param) 30 | newOutputs := make([]string, 0) 31 | for i := 0; i < len(outputs); i++ { 32 | json := `{"response": {}}` 33 | output, _ := sjson.SetRaw(json, "response", outputs[i]) 34 | newOutputs = append(newOutputs, output) 35 | } 36 | return newOutputs 37 | } 38 | 39 | // ConvertOpenAIResponseToGeminiCLINonStream converts a non-streaming OpenAI response to a non-streaming Gemini CLI response. 40 | // 41 | // Parameters: 42 | // - ctx: The context for the request. 43 | // - modelName: The name of the model. 44 | // - rawJSON: The raw JSON response from the OpenAI API. 45 | // - param: A pointer to a parameter object for the conversion. 46 | // 47 | // Returns: 48 | // - string: A Gemini-compatible JSON response. 49 | func ConvertOpenAIResponseToGeminiCLINonStream(ctx context.Context, modelName string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) string { 50 | strJSON := ConvertOpenAIResponseToGeminiNonStream(ctx, modelName, originalRequestRawJSON, requestRawJSON, rawJSON, param) 51 | json := `{"response": {}}` 52 | strJSON, _ = sjson.SetRaw(json, "response", strJSON) 53 | return strJSON 54 | } 55 | 56 | func GeminiCLITokenCount(ctx context.Context, count int64) string { 57 | return fmt.Sprintf(`{"totalTokens":%d,"promptTokensDetails":[{"modality":"TEXT","tokenCount":%d}]}`, count, count) 58 | } 59 | -------------------------------------------------------------------------------- /internal/translator/openai/openai/chat-completions/openai_openai_response.go: -------------------------------------------------------------------------------- 1 | // Package openai provides response translation functionality for Gemini CLI to OpenAI API compatibility. 2 | // This package handles the conversion of Gemini CLI API responses into OpenAI Chat Completions-compatible 3 | // JSON format, transforming streaming events and non-streaming responses into the format 4 | // expected by OpenAI API clients. It supports both streaming and non-streaming modes, 5 | // handling text content, tool calls, reasoning content, and usage metadata appropriately. 6 | package chat_completions 7 | 8 | import ( 9 | "bytes" 10 | "context" 11 | ) 12 | 13 | // ConvertOpenAIResponseToOpenAI translates a single chunk of a streaming response from the 14 | // Gemini CLI API format to the OpenAI Chat Completions streaming format. 15 | // It processes various Gemini CLI event types and transforms them into OpenAI-compatible JSON responses. 16 | // The function handles text content, tool calls, reasoning content, and usage metadata, outputting 17 | // responses that match the OpenAI API format. It supports incremental updates for streaming responses. 18 | // 19 | // Parameters: 20 | // - ctx: The context for the request, used for cancellation and timeout handling 21 | // - modelName: The name of the model being used for the response (unused in current implementation) 22 | // - rawJSON: The raw JSON response from the Gemini CLI API 23 | // - param: A pointer to a parameter object for maintaining state between calls 24 | // 25 | // Returns: 26 | // - []string: A slice of strings, each containing an OpenAI-compatible JSON response 27 | func ConvertOpenAIResponseToOpenAI(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) []string { 28 | if bytes.HasPrefix(rawJSON, []byte("data:")) { 29 | rawJSON = bytes.TrimSpace(rawJSON[5:]) 30 | } 31 | if bytes.Equal(rawJSON, []byte("[DONE]")) { 32 | return []string{} 33 | } 34 | return []string{string(rawJSON)} 35 | } 36 | 37 | // ConvertOpenAIResponseToOpenAINonStream converts a non-streaming Gemini CLI response to a non-streaming OpenAI response. 38 | // This function processes the complete Gemini CLI response and transforms it into a single OpenAI-compatible 39 | // JSON response. It handles message content, tool calls, reasoning content, and usage metadata, combining all 40 | // the information into a single response that matches the OpenAI API format. 41 | // 42 | // Parameters: 43 | // - ctx: The context for the request, used for cancellation and timeout handling 44 | // - modelName: The name of the model being used for the response 45 | // - rawJSON: The raw JSON response from the Gemini CLI API 46 | // - param: A pointer to a parameter object for the conversion 47 | // 48 | // Returns: 49 | // - string: An OpenAI-compatible JSON response containing all message content and metadata 50 | func ConvertOpenAIResponseToOpenAINonStream(ctx context.Context, modelName string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) string { 51 | return string(rawJSON) 52 | } 53 | -------------------------------------------------------------------------------- /docs/sdk-watcher.md: -------------------------------------------------------------------------------- 1 | # SDK Watcher Integration 2 | 3 | The SDK service exposes a watcher integration that surfaces granular auth updates without forcing a full reload. This document explains the queue contract, how the service consumes updates, and how high-frequency change bursts are handled. 4 | 5 | ## Update Queue Contract 6 | 7 | - `watcher.AuthUpdate` represents a single credential change. `Action` may be `add`, `modify`, or `delete`, and `ID` carries the credential identifier. For `add`/`modify` the `Auth` payload contains a fully populated clone of the credential; `delete` may omit `Auth`. 8 | - `WatcherWrapper.SetAuthUpdateQueue(chan<- watcher.AuthUpdate)` wires the queue produced by the SDK service into the watcher. The queue must be created before the watcher starts. 9 | - The service builds the queue via `ensureAuthUpdateQueue`, using a buffered channel (`capacity=256`) and a dedicated consumer goroutine (`consumeAuthUpdates`). The consumer drains bursts by looping through the backlog before reacquiring the select loop. 10 | 11 | ## Watcher Behaviour 12 | 13 | - `internal/watcher/watcher.go` keeps a shadow snapshot of auth state (`currentAuths`). Each filesystem or configuration event triggers a recomputation and a diff against the previous snapshot to produce minimal `AuthUpdate` entries that mirror adds, edits, and removals. 14 | - Updates are coalesced per credential identifier. If multiple changes occur before dispatch (e.g., write followed by delete), only the final action is sent downstream. 15 | - The watcher runs an internal dispatch loop that buffers pending updates in memory and forwards them asynchronously to the queue. Producers never block on channel capacity; they just enqueue into the in-memory buffer and signal the dispatcher. Dispatch cancellation happens when the watcher stops, guaranteeing goroutines exit cleanly. 16 | 17 | ## High-Frequency Change Handling 18 | 19 | - The dispatch loop and service consumer run independently, preventing filesystem watchers from blocking even when many updates arrive at once. 20 | - Back-pressure is absorbed in two places: 21 | - The dispatch buffer (map + order slice) coalesces repeated updates for the same credential until the consumer catches up. 22 | - The service channel capacity (256) combined with the consumer drain loop ensures several bursts can be processed without oscillation. 23 | - If the queue is saturated for an extended period, updates continue to be merged, so the latest state is eventually applied without replaying redundant intermediate states. 24 | 25 | ## Usage Checklist 26 | 27 | 1. Instantiate the SDK service (builder or manual construction). 28 | 2. Call `ensureAuthUpdateQueue` before starting the watcher to allocate the shared channel. 29 | 3. When the `WatcherWrapper` is created, call `SetAuthUpdateQueue` with the service queue, then start the watcher. 30 | 4. Provide a reload callback that handles configuration updates; auth deltas will arrive via the queue and are applied by the service automatically through `handleAuthUpdate`. 31 | 32 | Following this flow keeps auth changes responsive while avoiding full reloads for every edit. 33 | -------------------------------------------------------------------------------- /internal/cmd/iflow_cookie.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "bufio" 5 | "context" 6 | "fmt" 7 | "os" 8 | "path/filepath" 9 | "strings" 10 | "time" 11 | 12 | "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/iflow" 13 | "github.com/router-for-me/CLIProxyAPI/v6/internal/config" 14 | ) 15 | 16 | // DoIFlowCookieAuth performs the iFlow cookie-based authentication. 17 | func DoIFlowCookieAuth(cfg *config.Config, options *LoginOptions) { 18 | if options == nil { 19 | options = &LoginOptions{} 20 | } 21 | 22 | promptFn := options.Prompt 23 | if promptFn == nil { 24 | reader := bufio.NewReader(os.Stdin) 25 | promptFn = func(prompt string) (string, error) { 26 | fmt.Print(prompt) 27 | value, err := reader.ReadString('\n') 28 | if err != nil { 29 | return "", err 30 | } 31 | return strings.TrimSpace(value), nil 32 | } 33 | } 34 | 35 | // Prompt user for cookie 36 | cookie, err := promptForCookie(promptFn) 37 | if err != nil { 38 | fmt.Printf("Failed to get cookie: %v\n", err) 39 | return 40 | } 41 | 42 | // Check for duplicate BXAuth before authentication 43 | bxAuth := iflow.ExtractBXAuth(cookie) 44 | if existingFile, err := iflow.CheckDuplicateBXAuth(cfg.AuthDir, bxAuth); err != nil { 45 | fmt.Printf("Failed to check duplicate: %v\n", err) 46 | return 47 | } else if existingFile != "" { 48 | fmt.Printf("Duplicate BXAuth found, authentication already exists: %s\n", filepath.Base(existingFile)) 49 | return 50 | } 51 | 52 | // Authenticate with cookie 53 | auth := iflow.NewIFlowAuth(cfg) 54 | ctx := context.Background() 55 | 56 | tokenData, err := auth.AuthenticateWithCookie(ctx, cookie) 57 | if err != nil { 58 | fmt.Printf("iFlow cookie authentication failed: %v\n", err) 59 | return 60 | } 61 | 62 | // Create token storage 63 | tokenStorage := auth.CreateCookieTokenStorage(tokenData) 64 | 65 | // Get auth file path using email in filename 66 | authFilePath := getAuthFilePath(cfg, "iflow", tokenData.Email) 67 | 68 | // Save token to file 69 | if err := tokenStorage.SaveTokenToFile(authFilePath); err != nil { 70 | fmt.Printf("Failed to save authentication: %v\n", err) 71 | return 72 | } 73 | 74 | fmt.Printf("Authentication successful! API key: %s\n", tokenData.APIKey) 75 | fmt.Printf("Expires at: %s\n", tokenData.Expire) 76 | fmt.Printf("Authentication saved to: %s\n", authFilePath) 77 | } 78 | 79 | // promptForCookie prompts the user to enter their iFlow cookie 80 | func promptForCookie(promptFn func(string) (string, error)) (string, error) { 81 | line, err := promptFn("Enter iFlow Cookie (from browser cookies): ") 82 | if err != nil { 83 | return "", fmt.Errorf("failed to read cookie: %w", err) 84 | } 85 | 86 | cookie, err := iflow.NormalizeCookie(line) 87 | if err != nil { 88 | return "", err 89 | } 90 | 91 | return cookie, nil 92 | } 93 | 94 | // getAuthFilePath returns the auth file path for the given provider and email 95 | func getAuthFilePath(cfg *config.Config, provider, email string) string { 96 | fileName := iflow.SanitizeIFlowFileName(email) 97 | return fmt.Sprintf("%s/%s-%s-%d.json", cfg.AuthDir, provider, fileName, time.Now().Unix()) 98 | } 99 | -------------------------------------------------------------------------------- /internal/access/config_access/provider.go: -------------------------------------------------------------------------------- 1 | package configaccess 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "strings" 7 | "sync" 8 | 9 | sdkaccess "github.com/router-for-me/CLIProxyAPI/v6/sdk/access" 10 | sdkconfig "github.com/router-for-me/CLIProxyAPI/v6/sdk/config" 11 | ) 12 | 13 | var registerOnce sync.Once 14 | 15 | // Register ensures the config-access provider is available to the access manager. 16 | func Register() { 17 | registerOnce.Do(func() { 18 | sdkaccess.RegisterProvider(sdkconfig.AccessProviderTypeConfigAPIKey, newProvider) 19 | }) 20 | } 21 | 22 | type provider struct { 23 | name string 24 | keys map[string]struct{} 25 | } 26 | 27 | func newProvider(cfg *sdkconfig.AccessProvider, _ *sdkconfig.SDKConfig) (sdkaccess.Provider, error) { 28 | name := cfg.Name 29 | if name == "" { 30 | name = sdkconfig.DefaultAccessProviderName 31 | } 32 | keys := make(map[string]struct{}, len(cfg.APIKeys)) 33 | for _, key := range cfg.APIKeys { 34 | if key == "" { 35 | continue 36 | } 37 | keys[key] = struct{}{} 38 | } 39 | return &provider{name: name, keys: keys}, nil 40 | } 41 | 42 | func (p *provider) Identifier() string { 43 | if p == nil || p.name == "" { 44 | return sdkconfig.DefaultAccessProviderName 45 | } 46 | return p.name 47 | } 48 | 49 | func (p *provider) Authenticate(_ context.Context, r *http.Request) (*sdkaccess.Result, error) { 50 | if p == nil { 51 | return nil, sdkaccess.ErrNotHandled 52 | } 53 | if len(p.keys) == 0 { 54 | return nil, sdkaccess.ErrNotHandled 55 | } 56 | authHeader := r.Header.Get("Authorization") 57 | authHeaderGoogle := r.Header.Get("X-Goog-Api-Key") 58 | authHeaderAnthropic := r.Header.Get("X-Api-Key") 59 | queryKey := "" 60 | queryAuthToken := "" 61 | if r.URL != nil { 62 | queryKey = r.URL.Query().Get("key") 63 | queryAuthToken = r.URL.Query().Get("auth_token") 64 | } 65 | if authHeader == "" && authHeaderGoogle == "" && authHeaderAnthropic == "" && queryKey == "" && queryAuthToken == "" { 66 | return nil, sdkaccess.ErrNoCredentials 67 | } 68 | 69 | apiKey := extractBearerToken(authHeader) 70 | 71 | candidates := []struct { 72 | value string 73 | source string 74 | }{ 75 | {apiKey, "authorization"}, 76 | {authHeaderGoogle, "x-goog-api-key"}, 77 | {authHeaderAnthropic, "x-api-key"}, 78 | {queryKey, "query-key"}, 79 | {queryAuthToken, "query-auth-token"}, 80 | } 81 | 82 | for _, candidate := range candidates { 83 | if candidate.value == "" { 84 | continue 85 | } 86 | if _, ok := p.keys[candidate.value]; ok { 87 | return &sdkaccess.Result{ 88 | Provider: p.Identifier(), 89 | Principal: candidate.value, 90 | Metadata: map[string]string{ 91 | "source": candidate.source, 92 | }, 93 | }, nil 94 | } 95 | } 96 | 97 | return nil, sdkaccess.ErrInvalidCredential 98 | } 99 | 100 | func extractBearerToken(header string) string { 101 | if header == "" { 102 | return "" 103 | } 104 | parts := strings.SplitN(header, " ", 2) 105 | if len(parts) != 2 { 106 | return header 107 | } 108 | if strings.ToLower(parts[0]) != "bearer" { 109 | return header 110 | } 111 | return strings.TrimSpace(parts[1]) 112 | } 113 | -------------------------------------------------------------------------------- /internal/watcher/diff/model_hash.go: -------------------------------------------------------------------------------- 1 | package diff 2 | 3 | import ( 4 | "crypto/sha256" 5 | "encoding/hex" 6 | "encoding/json" 7 | "sort" 8 | "strings" 9 | 10 | "github.com/router-for-me/CLIProxyAPI/v6/internal/config" 11 | ) 12 | 13 | // ComputeOpenAICompatModelsHash returns a stable hash for OpenAI-compat models. 14 | // Used to detect model list changes during hot reload. 15 | func ComputeOpenAICompatModelsHash(models []config.OpenAICompatibilityModel) string { 16 | keys := normalizeModelPairs(func(out func(key string)) { 17 | for _, model := range models { 18 | name := strings.TrimSpace(model.Name) 19 | alias := strings.TrimSpace(model.Alias) 20 | if name == "" && alias == "" { 21 | continue 22 | } 23 | out(strings.ToLower(name) + "|" + strings.ToLower(alias)) 24 | } 25 | }) 26 | return hashJoined(keys) 27 | } 28 | 29 | // ComputeVertexCompatModelsHash returns a stable hash for Vertex-compatible models. 30 | func ComputeVertexCompatModelsHash(models []config.VertexCompatModel) string { 31 | keys := normalizeModelPairs(func(out func(key string)) { 32 | for _, model := range models { 33 | name := strings.TrimSpace(model.Name) 34 | alias := strings.TrimSpace(model.Alias) 35 | if name == "" && alias == "" { 36 | continue 37 | } 38 | out(strings.ToLower(name) + "|" + strings.ToLower(alias)) 39 | } 40 | }) 41 | return hashJoined(keys) 42 | } 43 | 44 | // ComputeClaudeModelsHash returns a stable hash for Claude model aliases. 45 | func ComputeClaudeModelsHash(models []config.ClaudeModel) string { 46 | keys := normalizeModelPairs(func(out func(key string)) { 47 | for _, model := range models { 48 | name := strings.TrimSpace(model.Name) 49 | alias := strings.TrimSpace(model.Alias) 50 | if name == "" && alias == "" { 51 | continue 52 | } 53 | out(strings.ToLower(name) + "|" + strings.ToLower(alias)) 54 | } 55 | }) 56 | return hashJoined(keys) 57 | } 58 | 59 | // ComputeExcludedModelsHash returns a normalized hash for excluded model lists. 60 | func ComputeExcludedModelsHash(excluded []string) string { 61 | if len(excluded) == 0 { 62 | return "" 63 | } 64 | normalized := make([]string, 0, len(excluded)) 65 | for _, entry := range excluded { 66 | if trimmed := strings.TrimSpace(entry); trimmed != "" { 67 | normalized = append(normalized, strings.ToLower(trimmed)) 68 | } 69 | } 70 | if len(normalized) == 0 { 71 | return "" 72 | } 73 | sort.Strings(normalized) 74 | data, _ := json.Marshal(normalized) 75 | sum := sha256.Sum256(data) 76 | return hex.EncodeToString(sum[:]) 77 | } 78 | 79 | func normalizeModelPairs(collect func(out func(key string))) []string { 80 | seen := make(map[string]struct{}) 81 | keys := make([]string, 0) 82 | collect(func(key string) { 83 | if _, exists := seen[key]; exists { 84 | return 85 | } 86 | seen[key] = struct{}{} 87 | keys = append(keys, key) 88 | }) 89 | if len(keys) == 0 { 90 | return nil 91 | } 92 | sort.Strings(keys) 93 | return keys 94 | } 95 | 96 | func hashJoined(keys []string) string { 97 | if len(keys) == 0 { 98 | return "" 99 | } 100 | sum := sha256.Sum256([]byte(strings.Join(keys, "\n"))) 101 | return hex.EncodeToString(sum[:]) 102 | } 103 | -------------------------------------------------------------------------------- /internal/translator/gemini/gemini-cli/gemini_gemini-cli_request.go: -------------------------------------------------------------------------------- 1 | // Package gemini provides request translation functionality for Claude API. 2 | // It handles parsing and transforming Claude API requests into the internal client format, 3 | // extracting model information, system instructions, message contents, and tool declarations. 4 | // The package also performs JSON data cleaning and transformation to ensure compatibility 5 | // between Claude API format and the internal client's expected format. 6 | package geminiCLI 7 | 8 | import ( 9 | "bytes" 10 | "fmt" 11 | 12 | "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/gemini/common" 13 | "github.com/router-for-me/CLIProxyAPI/v6/internal/util" 14 | "github.com/tidwall/gjson" 15 | "github.com/tidwall/sjson" 16 | ) 17 | 18 | // PrepareClaudeRequest parses and transforms a Claude API request into internal client format. 19 | // It extracts the model name, system instruction, message contents, and tool declarations 20 | // from the raw JSON request and returns them in the format expected by the internal client. 21 | func ConvertGeminiCLIRequestToGemini(_ string, inputRawJSON []byte, _ bool) []byte { 22 | rawJSON := bytes.Clone(inputRawJSON) 23 | modelResult := gjson.GetBytes(rawJSON, "model") 24 | rawJSON = []byte(gjson.GetBytes(rawJSON, "request").Raw) 25 | rawJSON, _ = sjson.SetBytes(rawJSON, "model", modelResult.String()) 26 | if gjson.GetBytes(rawJSON, "systemInstruction").Exists() { 27 | rawJSON, _ = sjson.SetRawBytes(rawJSON, "system_instruction", []byte(gjson.GetBytes(rawJSON, "systemInstruction").Raw)) 28 | rawJSON, _ = sjson.DeleteBytes(rawJSON, "systemInstruction") 29 | } 30 | 31 | toolsResult := gjson.GetBytes(rawJSON, "tools") 32 | if toolsResult.Exists() && toolsResult.IsArray() { 33 | toolResults := toolsResult.Array() 34 | for i := 0; i < len(toolResults); i++ { 35 | functionDeclarationsResult := gjson.GetBytes(rawJSON, fmt.Sprintf("tools.%d.function_declarations", i)) 36 | if functionDeclarationsResult.Exists() && functionDeclarationsResult.IsArray() { 37 | functionDeclarationsResults := functionDeclarationsResult.Array() 38 | for j := 0; j < len(functionDeclarationsResults); j++ { 39 | parametersResult := gjson.GetBytes(rawJSON, fmt.Sprintf("tools.%d.function_declarations.%d.parameters", i, j)) 40 | if parametersResult.Exists() { 41 | strJson, _ := util.RenameKey(string(rawJSON), fmt.Sprintf("tools.%d.function_declarations.%d.parameters", i, j), fmt.Sprintf("tools.%d.function_declarations.%d.parametersJsonSchema", i, j)) 42 | rawJSON = []byte(strJson) 43 | } 44 | } 45 | } 46 | } 47 | } 48 | 49 | gjson.GetBytes(rawJSON, "contents").ForEach(func(key, content gjson.Result) bool { 50 | if content.Get("role").String() == "model" { 51 | content.Get("parts").ForEach(func(partKey, part gjson.Result) bool { 52 | if part.Get("functionCall").Exists() { 53 | rawJSON, _ = sjson.SetBytes(rawJSON, fmt.Sprintf("contents.%d.parts.%d.thoughtSignature", key.Int(), partKey.Int()), "skip_thought_signature_validator") 54 | } else if part.Get("thoughtSignature").Exists() { 55 | rawJSON, _ = sjson.SetBytes(rawJSON, fmt.Sprintf("contents.%d.parts.%d.thoughtSignature", key.Int(), partKey.Int()), "skip_thought_signature_validator") 56 | } 57 | return true 58 | }) 59 | } 60 | return true 61 | }) 62 | 63 | return common.AttachDefaultSafetySettings(rawJSON, "safetySettings") 64 | } 65 | -------------------------------------------------------------------------------- /internal/auth/gemini/gemini_token.go: -------------------------------------------------------------------------------- 1 | // Package gemini provides authentication and token management functionality 2 | // for Google's Gemini AI services. It handles OAuth2 token storage, serialization, 3 | // and retrieval for maintaining authenticated sessions with the Gemini API. 4 | package gemini 5 | 6 | import ( 7 | "encoding/json" 8 | "fmt" 9 | "os" 10 | "path/filepath" 11 | "strings" 12 | 13 | "github.com/router-for-me/CLIProxyAPI/v6/internal/misc" 14 | log "github.com/sirupsen/logrus" 15 | ) 16 | 17 | // GeminiTokenStorage stores OAuth2 token information for Google Gemini API authentication. 18 | // It maintains compatibility with the existing auth system while adding Gemini-specific fields 19 | // for managing access tokens, refresh tokens, and user account information. 20 | type GeminiTokenStorage struct { 21 | // Token holds the raw OAuth2 token data, including access and refresh tokens. 22 | Token any `json:"token"` 23 | 24 | // ProjectID is the Google Cloud Project ID associated with this token. 25 | ProjectID string `json:"project_id"` 26 | 27 | // Email is the email address of the authenticated user. 28 | Email string `json:"email"` 29 | 30 | // Auto indicates if the project ID was automatically selected. 31 | Auto bool `json:"auto"` 32 | 33 | // Checked indicates if the associated Cloud AI API has been verified as enabled. 34 | Checked bool `json:"checked"` 35 | 36 | // Type indicates the authentication provider type, always "gemini" for this storage. 37 | Type string `json:"type"` 38 | } 39 | 40 | // SaveTokenToFile serializes the Gemini token storage to a JSON file. 41 | // This method creates the necessary directory structure and writes the token 42 | // data in JSON format to the specified file path for persistent storage. 43 | // 44 | // Parameters: 45 | // - authFilePath: The full path where the token file should be saved 46 | // 47 | // Returns: 48 | // - error: An error if the operation fails, nil otherwise 49 | func (ts *GeminiTokenStorage) SaveTokenToFile(authFilePath string) error { 50 | misc.LogSavingCredentials(authFilePath) 51 | ts.Type = "gemini" 52 | if err := os.MkdirAll(filepath.Dir(authFilePath), 0700); err != nil { 53 | return fmt.Errorf("failed to create directory: %v", err) 54 | } 55 | 56 | f, err := os.Create(authFilePath) 57 | if err != nil { 58 | return fmt.Errorf("failed to create token file: %w", err) 59 | } 60 | defer func() { 61 | if errClose := f.Close(); errClose != nil { 62 | log.Errorf("failed to close file: %v", errClose) 63 | } 64 | }() 65 | 66 | if err = json.NewEncoder(f).Encode(ts); err != nil { 67 | return fmt.Errorf("failed to write token to file: %w", err) 68 | } 69 | return nil 70 | } 71 | 72 | // CredentialFileName returns the filename used to persist Gemini CLI credentials. 73 | // When projectID represents multiple projects (comma-separated or literal ALL), 74 | // the suffix is normalized to "all" and a "gemini-" prefix is enforced to keep 75 | // web and CLI generated files consistent. 76 | func CredentialFileName(email, projectID string, includeProviderPrefix bool) string { 77 | email = strings.TrimSpace(email) 78 | project := strings.TrimSpace(projectID) 79 | if strings.EqualFold(project, "all") || strings.Contains(project, ",") { 80 | return fmt.Sprintf("gemini-%s-all.json", email) 81 | } 82 | prefix := "" 83 | if includeProviderPrefix { 84 | prefix = "gemini-" 85 | } 86 | return fmt.Sprintf("%s%s-%s.json", prefix, email, project) 87 | } 88 | -------------------------------------------------------------------------------- /internal/translator/codex/gemini-cli/codex_gemini-cli_response.go: -------------------------------------------------------------------------------- 1 | // Package geminiCLI provides response translation functionality for Codex to Gemini CLI API compatibility. 2 | // This package handles the conversion of Codex API responses into Gemini CLI-compatible 3 | // JSON format, transforming streaming events and non-streaming responses into the format 4 | // expected by Gemini CLI API clients. 5 | package geminiCLI 6 | 7 | import ( 8 | "context" 9 | "fmt" 10 | 11 | . "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/codex/gemini" 12 | "github.com/tidwall/sjson" 13 | ) 14 | 15 | // ConvertCodexResponseToGeminiCLI converts Codex streaming response format to Gemini CLI format. 16 | // This function processes various Codex event types and transforms them into Gemini-compatible JSON responses. 17 | // It handles text content, tool calls, and usage metadata, outputting responses that match the Gemini CLI API format. 18 | // The function wraps each converted response in a "response" object to match the Gemini CLI API structure. 19 | // 20 | // Parameters: 21 | // - ctx: The context for the request, used for cancellation and timeout handling 22 | // - modelName: The name of the model being used for the response 23 | // - rawJSON: The raw JSON response from the Codex API 24 | // - param: A pointer to a parameter object for maintaining state between calls 25 | // 26 | // Returns: 27 | // - []string: A slice of strings, each containing a Gemini-compatible JSON response wrapped in a response object 28 | func ConvertCodexResponseToGeminiCLI(ctx context.Context, modelName string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) []string { 29 | outputs := ConvertCodexResponseToGemini(ctx, modelName, originalRequestRawJSON, requestRawJSON, rawJSON, param) 30 | newOutputs := make([]string, 0) 31 | for i := 0; i < len(outputs); i++ { 32 | json := `{"response": {}}` 33 | output, _ := sjson.SetRaw(json, "response", outputs[i]) 34 | newOutputs = append(newOutputs, output) 35 | } 36 | return newOutputs 37 | } 38 | 39 | // ConvertCodexResponseToGeminiCLINonStream converts a non-streaming Codex response to a non-streaming Gemini CLI response. 40 | // This function processes the complete Codex response and transforms it into a single Gemini-compatible 41 | // JSON response. It wraps the converted response in a "response" object to match the Gemini CLI API structure. 42 | // 43 | // Parameters: 44 | // - ctx: The context for the request, used for cancellation and timeout handling 45 | // - modelName: The name of the model being used for the response 46 | // - rawJSON: The raw JSON response from the Codex API 47 | // - param: A pointer to a parameter object for the conversion 48 | // 49 | // Returns: 50 | // - string: A Gemini-compatible JSON response wrapped in a response object 51 | func ConvertCodexResponseToGeminiCLINonStream(ctx context.Context, modelName string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) string { 52 | // log.Debug(string(rawJSON)) 53 | strJSON := ConvertCodexResponseToGeminiNonStream(ctx, modelName, originalRequestRawJSON, requestRawJSON, rawJSON, param) 54 | json := `{"response": {}}` 55 | strJSON, _ = sjson.SetRaw(json, "response", strJSON) 56 | return strJSON 57 | } 58 | 59 | func GeminiCLITokenCount(ctx context.Context, count int64) string { 60 | return fmt.Sprintf(`{"totalTokens":%d,"promptTokensDetails":[{"modality":"TEXT","tokenCount":%d}]}`, count, count) 61 | } 62 | -------------------------------------------------------------------------------- /internal/api/server_test.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "os" 7 | "path/filepath" 8 | "strings" 9 | "testing" 10 | 11 | gin "github.com/gin-gonic/gin" 12 | proxyconfig "github.com/router-for-me/CLIProxyAPI/v6/internal/config" 13 | sdkaccess "github.com/router-for-me/CLIProxyAPI/v6/sdk/access" 14 | "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" 15 | sdkconfig "github.com/router-for-me/CLIProxyAPI/v6/sdk/config" 16 | ) 17 | 18 | func newTestServer(t *testing.T) *Server { 19 | t.Helper() 20 | 21 | gin.SetMode(gin.TestMode) 22 | 23 | tmpDir := t.TempDir() 24 | authDir := filepath.Join(tmpDir, "auth") 25 | if err := os.MkdirAll(authDir, 0o700); err != nil { 26 | t.Fatalf("failed to create auth dir: %v", err) 27 | } 28 | 29 | cfg := &proxyconfig.Config{ 30 | SDKConfig: sdkconfig.SDKConfig{ 31 | APIKeys: []string{"test-key"}, 32 | }, 33 | Port: 0, 34 | AuthDir: authDir, 35 | Debug: true, 36 | LoggingToFile: false, 37 | UsageStatisticsEnabled: false, 38 | } 39 | 40 | authManager := auth.NewManager(nil, nil, nil) 41 | accessManager := sdkaccess.NewManager() 42 | 43 | configPath := filepath.Join(tmpDir, "config.yaml") 44 | return NewServer(cfg, authManager, accessManager, configPath) 45 | } 46 | 47 | func TestAmpProviderModelRoutes(t *testing.T) { 48 | testCases := []struct { 49 | name string 50 | path string 51 | wantStatus int 52 | wantContains string 53 | }{ 54 | { 55 | name: "openai root models", 56 | path: "/api/provider/openai/models", 57 | wantStatus: http.StatusOK, 58 | wantContains: `"object":"list"`, 59 | }, 60 | { 61 | name: "groq root models", 62 | path: "/api/provider/groq/models", 63 | wantStatus: http.StatusOK, 64 | wantContains: `"object":"list"`, 65 | }, 66 | { 67 | name: "openai models", 68 | path: "/api/provider/openai/v1/models", 69 | wantStatus: http.StatusOK, 70 | wantContains: `"object":"list"`, 71 | }, 72 | { 73 | name: "anthropic models", 74 | path: "/api/provider/anthropic/v1/models", 75 | wantStatus: http.StatusOK, 76 | wantContains: `"data"`, 77 | }, 78 | { 79 | name: "google models v1", 80 | path: "/api/provider/google/v1/models", 81 | wantStatus: http.StatusOK, 82 | wantContains: `"models"`, 83 | }, 84 | { 85 | name: "google models v1beta", 86 | path: "/api/provider/google/v1beta/models", 87 | wantStatus: http.StatusOK, 88 | wantContains: `"models"`, 89 | }, 90 | } 91 | 92 | for _, tc := range testCases { 93 | tc := tc 94 | t.Run(tc.name, func(t *testing.T) { 95 | server := newTestServer(t) 96 | 97 | req := httptest.NewRequest(http.MethodGet, tc.path, nil) 98 | req.Header.Set("Authorization", "Bearer test-key") 99 | 100 | rr := httptest.NewRecorder() 101 | server.engine.ServeHTTP(rr, req) 102 | 103 | if rr.Code != tc.wantStatus { 104 | t.Fatalf("unexpected status code for %s: got %d want %d; body=%s", tc.path, rr.Code, tc.wantStatus, rr.Body.String()) 105 | } 106 | if body := rr.Body.String(); !strings.Contains(body, tc.wantContains) { 107 | t.Fatalf("response body for %s missing %q: %s", tc.path, tc.wantContains, body) 108 | } 109 | }) 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/router-for-me/CLIProxyAPI/v6 2 | 3 | go 1.24.0 4 | 5 | require ( 6 | github.com/andybalholm/brotli v1.0.6 7 | github.com/fsnotify/fsnotify v1.9.0 8 | github.com/gin-gonic/gin v1.10.1 9 | github.com/go-git/go-git/v6 v6.0.0-20251009132922-75a182125145 10 | github.com/google/uuid v1.6.0 11 | github.com/gorilla/websocket v1.5.3 12 | github.com/jackc/pgx/v5 v5.7.6 13 | github.com/joho/godotenv v1.5.1 14 | github.com/klauspost/compress v1.17.4 15 | github.com/minio/minio-go/v7 v7.0.66 16 | github.com/sirupsen/logrus v1.9.3 17 | github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 18 | github.com/tidwall/gjson v1.18.0 19 | github.com/tidwall/sjson v1.2.5 20 | github.com/tiktoken-go/tokenizer v0.7.0 21 | golang.org/x/crypto v0.45.0 22 | golang.org/x/net v0.47.0 23 | golang.org/x/oauth2 v0.30.0 24 | gopkg.in/natefinch/lumberjack.v2 v2.2.1 25 | gopkg.in/yaml.v3 v3.0.1 26 | ) 27 | 28 | require ( 29 | cloud.google.com/go/compute/metadata v0.3.0 // indirect 30 | github.com/Microsoft/go-winio v0.6.2 // indirect 31 | github.com/ProtonMail/go-crypto v1.3.0 // indirect 32 | github.com/bytedance/sonic v1.11.6 // indirect 33 | github.com/bytedance/sonic/loader v0.1.1 // indirect 34 | github.com/cloudflare/circl v1.6.1 // indirect 35 | github.com/cloudwego/base64x v0.1.4 // indirect 36 | github.com/cloudwego/iasm v0.2.0 // indirect 37 | github.com/cyphar/filepath-securejoin v0.4.1 // indirect 38 | github.com/dlclark/regexp2 v1.11.5 // indirect 39 | github.com/dustin/go-humanize v1.0.1 // indirect 40 | github.com/emirpasic/gods v1.18.1 // indirect 41 | github.com/gabriel-vasile/mimetype v1.4.3 // indirect 42 | github.com/gin-contrib/sse v0.1.0 // indirect 43 | github.com/go-git/gcfg/v2 v2.0.2 // indirect 44 | github.com/go-git/go-billy/v6 v6.0.0-20250627091229-31e2a16eef30 // indirect 45 | github.com/go-playground/locales v0.14.1 // indirect 46 | github.com/go-playground/universal-translator v0.18.1 // indirect 47 | github.com/go-playground/validator/v10 v10.20.0 // indirect 48 | github.com/goccy/go-json v0.10.2 // indirect 49 | github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect 50 | github.com/jackc/pgpassfile v1.0.0 // indirect 51 | github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect 52 | github.com/jackc/puddle/v2 v2.2.2 // indirect 53 | github.com/json-iterator/go v1.1.12 // indirect 54 | github.com/kevinburke/ssh_config v1.4.0 // indirect 55 | github.com/klauspost/cpuid/v2 v2.3.0 // indirect 56 | github.com/leodido/go-urn v1.4.0 // indirect 57 | github.com/mattn/go-isatty v0.0.20 // indirect 58 | github.com/minio/md5-simd v1.1.2 // indirect 59 | github.com/minio/sha256-simd v1.0.1 // indirect 60 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 61 | github.com/modern-go/reflect2 v1.0.2 // indirect 62 | github.com/pelletier/go-toml/v2 v2.2.2 // indirect 63 | github.com/pjbgf/sha1cd v0.5.0 // indirect 64 | github.com/rs/xid v1.5.0 // indirect 65 | github.com/sergi/go-diff v1.4.0 // indirect 66 | github.com/tidwall/match v1.1.1 // indirect 67 | github.com/tidwall/pretty v1.2.0 // indirect 68 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 69 | github.com/ugorji/go/codec v1.2.12 // indirect 70 | golang.org/x/arch v0.8.0 // indirect 71 | golang.org/x/sync v0.18.0 // indirect 72 | golang.org/x/sys v0.38.0 // indirect 73 | golang.org/x/text v0.31.0 // indirect 74 | google.golang.org/protobuf v1.34.1 // indirect 75 | gopkg.in/ini.v1 v1.67.0 // indirect 76 | ) 77 | -------------------------------------------------------------------------------- /internal/util/util.go: -------------------------------------------------------------------------------- 1 | // Package util provides utility functions for the CLI Proxy API server. 2 | // It includes helper functions for logging configuration, file system operations, 3 | // and other common utilities used throughout the application. 4 | package util 5 | 6 | import ( 7 | "fmt" 8 | "io/fs" 9 | "os" 10 | "path/filepath" 11 | "strings" 12 | 13 | "github.com/router-for-me/CLIProxyAPI/v6/internal/config" 14 | log "github.com/sirupsen/logrus" 15 | ) 16 | 17 | // SetLogLevel configures the logrus log level based on the configuration. 18 | // It sets the log level to DebugLevel if debug mode is enabled, otherwise to InfoLevel. 19 | func SetLogLevel(cfg *config.Config) { 20 | currentLevel := log.GetLevel() 21 | var newLevel log.Level 22 | if cfg.Debug { 23 | newLevel = log.DebugLevel 24 | } else { 25 | newLevel = log.InfoLevel 26 | } 27 | 28 | if currentLevel != newLevel { 29 | log.SetLevel(newLevel) 30 | log.Infof("log level changed from %s to %s (debug=%t)", currentLevel, newLevel, cfg.Debug) 31 | } 32 | } 33 | 34 | // ResolveAuthDir normalizes the auth directory path for consistent reuse throughout the app. 35 | // It expands a leading tilde (~) to the user's home directory and returns a cleaned path. 36 | func ResolveAuthDir(authDir string) (string, error) { 37 | if authDir == "" { 38 | return "", nil 39 | } 40 | if strings.HasPrefix(authDir, "~") { 41 | home, err := os.UserHomeDir() 42 | if err != nil { 43 | return "", fmt.Errorf("resolve auth dir: %w", err) 44 | } 45 | remainder := strings.TrimPrefix(authDir, "~") 46 | remainder = strings.TrimLeft(remainder, "/\\") 47 | if remainder == "" { 48 | return filepath.Clean(home), nil 49 | } 50 | normalized := strings.ReplaceAll(remainder, "\\", "/") 51 | return filepath.Clean(filepath.Join(home, filepath.FromSlash(normalized))), nil 52 | } 53 | return filepath.Clean(authDir), nil 54 | } 55 | 56 | // CountAuthFiles returns the number of JSON auth files located under the provided directory. 57 | // The function resolves leading tildes to the user's home directory and performs a case-insensitive 58 | // match on the ".json" suffix so that files saved with uppercase extensions are also counted. 59 | func CountAuthFiles(authDir string) int { 60 | dir, err := ResolveAuthDir(authDir) 61 | if err != nil { 62 | log.Debugf("countAuthFiles: failed to resolve auth directory: %v", err) 63 | return 0 64 | } 65 | if dir == "" { 66 | return 0 67 | } 68 | count := 0 69 | walkErr := filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error { 70 | if err != nil { 71 | log.Debugf("countAuthFiles: error accessing %s: %v", path, err) 72 | return nil 73 | } 74 | if d.IsDir() { 75 | return nil 76 | } 77 | if strings.HasSuffix(strings.ToLower(d.Name()), ".json") { 78 | count++ 79 | } 80 | return nil 81 | }) 82 | if walkErr != nil { 83 | log.Debugf("countAuthFiles: walk error: %v", walkErr) 84 | } 85 | return count 86 | } 87 | 88 | // WritablePath returns the cleaned WRITABLE_PATH environment variable when it is set. 89 | // It accepts both uppercase and lowercase variants for compatibility with existing conventions. 90 | func WritablePath() string { 91 | for _, key := range []string{"WRITABLE_PATH", "writable_path"} { 92 | if value, ok := os.LookupEnv(key); ok { 93 | trimmed := strings.TrimSpace(value) 94 | if trimmed != "" { 95 | return filepath.Clean(trimmed) 96 | } 97 | } 98 | } 99 | return "" 100 | } 101 | -------------------------------------------------------------------------------- /internal/translator/claude/gemini-cli/claude_gemini-cli_response.go: -------------------------------------------------------------------------------- 1 | // Package geminiCLI provides response translation functionality for Claude Code to Gemini CLI API compatibility. 2 | // This package handles the conversion of Claude Code API responses into Gemini CLI-compatible 3 | // JSON format, transforming streaming events and non-streaming responses into the format 4 | // expected by Gemini CLI API clients. 5 | package geminiCLI 6 | 7 | import ( 8 | "context" 9 | 10 | . "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/claude/gemini" 11 | "github.com/tidwall/sjson" 12 | ) 13 | 14 | // ConvertClaudeResponseToGeminiCLI converts Claude Code streaming response format to Gemini CLI format. 15 | // This function processes various Claude Code event types and transforms them into Gemini-compatible JSON responses. 16 | // It handles text content, tool calls, and usage metadata, outputting responses that match the Gemini CLI API format. 17 | // The function wraps each converted response in a "response" object to match the Gemini CLI API structure. 18 | // 19 | // Parameters: 20 | // - ctx: The context for the request, used for cancellation and timeout handling 21 | // - modelName: The name of the model being used for the response 22 | // - rawJSON: The raw JSON response from the Claude Code API 23 | // - param: A pointer to a parameter object for maintaining state between calls 24 | // 25 | // Returns: 26 | // - []string: A slice of strings, each containing a Gemini-compatible JSON response wrapped in a response object 27 | func ConvertClaudeResponseToGeminiCLI(ctx context.Context, modelName string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) []string { 28 | outputs := ConvertClaudeResponseToGemini(ctx, modelName, originalRequestRawJSON, requestRawJSON, rawJSON, param) 29 | // Wrap each converted response in a "response" object to match Gemini CLI API structure 30 | newOutputs := make([]string, 0) 31 | for i := 0; i < len(outputs); i++ { 32 | json := `{"response": {}}` 33 | output, _ := sjson.SetRaw(json, "response", outputs[i]) 34 | newOutputs = append(newOutputs, output) 35 | } 36 | return newOutputs 37 | } 38 | 39 | // ConvertClaudeResponseToGeminiCLINonStream converts a non-streaming Claude Code response to a non-streaming Gemini CLI response. 40 | // This function processes the complete Claude Code response and transforms it into a single Gemini-compatible 41 | // JSON response. It wraps the converted response in a "response" object to match the Gemini CLI API structure. 42 | // 43 | // Parameters: 44 | // - ctx: The context for the request, used for cancellation and timeout handling 45 | // - modelName: The name of the model being used for the response 46 | // - rawJSON: The raw JSON response from the Claude Code API 47 | // - param: A pointer to a parameter object for the conversion 48 | // 49 | // Returns: 50 | // - string: A Gemini-compatible JSON response wrapped in a response object 51 | func ConvertClaudeResponseToGeminiCLINonStream(ctx context.Context, modelName string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) string { 52 | strJSON := ConvertClaudeResponseToGeminiNonStream(ctx, modelName, originalRequestRawJSON, requestRawJSON, rawJSON, param) 53 | // Wrap the converted response in a "response" object to match Gemini CLI API structure 54 | json := `{"response": {}}` 55 | strJSON, _ = sjson.SetRaw(json, "response", strJSON) 56 | return strJSON 57 | } 58 | 59 | func GeminiCLITokenCount(ctx context.Context, count int64) string { 60 | return GeminiTokenCount(ctx, count) 61 | } 62 | -------------------------------------------------------------------------------- /internal/logging/gin_logger.go: -------------------------------------------------------------------------------- 1 | // Package logging provides Gin middleware for HTTP request logging and panic recovery. 2 | // It integrates Gin web framework with logrus for structured logging of HTTP requests, 3 | // responses, and error handling with panic recovery capabilities. 4 | package logging 5 | 6 | import ( 7 | "fmt" 8 | "net/http" 9 | "runtime/debug" 10 | "time" 11 | 12 | "github.com/gin-gonic/gin" 13 | "github.com/router-for-me/CLIProxyAPI/v6/internal/util" 14 | log "github.com/sirupsen/logrus" 15 | ) 16 | 17 | const skipGinLogKey = "__gin_skip_request_logging__" 18 | 19 | // GinLogrusLogger returns a Gin middleware handler that logs HTTP requests and responses 20 | // using logrus. It captures request details including method, path, status code, latency, 21 | // client IP, and any error messages, formatting them in a Gin-style log format. 22 | // 23 | // Returns: 24 | // - gin.HandlerFunc: A middleware handler for request logging 25 | func GinLogrusLogger() gin.HandlerFunc { 26 | return func(c *gin.Context) { 27 | start := time.Now() 28 | path := c.Request.URL.Path 29 | raw := util.MaskSensitiveQuery(c.Request.URL.RawQuery) 30 | 31 | c.Next() 32 | 33 | if shouldSkipGinRequestLogging(c) { 34 | return 35 | } 36 | 37 | if raw != "" { 38 | path = path + "?" + raw 39 | } 40 | 41 | latency := time.Since(start) 42 | if latency > time.Minute { 43 | latency = latency.Truncate(time.Second) 44 | } else { 45 | latency = latency.Truncate(time.Millisecond) 46 | } 47 | 48 | statusCode := c.Writer.Status() 49 | clientIP := c.ClientIP() 50 | method := c.Request.Method 51 | errorMessage := c.Errors.ByType(gin.ErrorTypePrivate).String() 52 | timestamp := time.Now().Format("2006/01/02 - 15:04:05") 53 | logLine := fmt.Sprintf("[GIN] %s | %3d | %13v | %15s | %-7s \"%s\"", timestamp, statusCode, latency, clientIP, method, path) 54 | if errorMessage != "" { 55 | logLine = logLine + " | " + errorMessage 56 | } 57 | 58 | switch { 59 | case statusCode >= http.StatusInternalServerError: 60 | log.Error(logLine) 61 | case statusCode >= http.StatusBadRequest: 62 | log.Warn(logLine) 63 | default: 64 | log.Info(logLine) 65 | } 66 | } 67 | } 68 | 69 | // GinLogrusRecovery returns a Gin middleware handler that recovers from panics and logs 70 | // them using logrus. When a panic occurs, it captures the panic value, stack trace, 71 | // and request path, then returns a 500 Internal Server Error response to the client. 72 | // 73 | // Returns: 74 | // - gin.HandlerFunc: A middleware handler for panic recovery 75 | func GinLogrusRecovery() gin.HandlerFunc { 76 | return gin.CustomRecovery(func(c *gin.Context, recovered interface{}) { 77 | log.WithFields(log.Fields{ 78 | "panic": recovered, 79 | "stack": string(debug.Stack()), 80 | "path": c.Request.URL.Path, 81 | }).Error("recovered from panic") 82 | 83 | c.AbortWithStatus(http.StatusInternalServerError) 84 | }) 85 | } 86 | 87 | // SkipGinRequestLogging marks the provided Gin context so that GinLogrusLogger 88 | // will skip emitting a log line for the associated request. 89 | func SkipGinRequestLogging(c *gin.Context) { 90 | if c == nil { 91 | return 92 | } 93 | c.Set(skipGinLogKey, true) 94 | } 95 | 96 | func shouldSkipGinRequestLogging(c *gin.Context) bool { 97 | if c == nil { 98 | return false 99 | } 100 | val, exists := c.Get(skipGinLogKey) 101 | if !exists { 102 | return false 103 | } 104 | flag, ok := val.(bool) 105 | return ok && flag 106 | } 107 | -------------------------------------------------------------------------------- /README_CN.md: -------------------------------------------------------------------------------- 1 | # CLI 代理 API 2 | 3 | [English](README.md) | 中文 4 | 5 | 一个为 CLI 提供 OpenAI/Gemini/Claude/Codex 兼容 API 接口的代理服务器。 6 | 7 | 现已支持通过 OAuth 登录接入 OpenAI Codex(GPT 系列)和 Claude Code。 8 | 9 | 您可以使用本地或多账户的CLI方式,通过任何与 OpenAI(包括Responses)/Gemini/Claude 兼容的客户端和SDK进行访问。 10 | 11 | ## 赞助商 12 | 13 | [![bigmodel.cn](https://assets.router-for.me/chinese.png)](https://www.bigmodel.cn/claude-code?ic=RRVJPB5SII) 14 | 15 | 本项目由 Z智谱 提供赞助, 他们通过 GLM CODING PLAN 对本项目提供技术支持。 16 | 17 | GLM CODING PLAN 是专为AI编码打造的订阅套餐,每月最低仅需20元,即可在十余款主流AI编码工具如 Claude Code、Cline、Roo Code 中畅享智谱旗舰模型GLM-4.6,为开发者提供顶尖的编码体验。 18 | 19 | 智谱AI为本软件提供了特别优惠,使用以下链接购买可以享受九折优惠:https://www.bigmodel.cn/claude-code?ic=RRVJPB5SII 20 | 21 | ## 功能特性 22 | 23 | - 为 CLI 模型提供 OpenAI/Gemini/Claude/Codex 兼容的 API 端点 24 | - 新增 OpenAI Codex(GPT 系列)支持(OAuth 登录) 25 | - 新增 Claude Code 支持(OAuth 登录) 26 | - 新增 Qwen Code 支持(OAuth 登录) 27 | - 新增 iFlow 支持(OAuth 登录) 28 | - 支持流式与非流式响应 29 | - 函数调用/工具支持 30 | - 多模态输入(文本、图片) 31 | - 多账户支持与轮询负载均衡(Gemini、OpenAI、Claude、Qwen 与 iFlow) 32 | - 简单的 CLI 身份验证流程(Gemini、OpenAI、Claude、Qwen 与 iFlow) 33 | - 支持 Gemini AIStudio API 密钥 34 | - 支持 AI Studio Build 多账户轮询 35 | - 支持 Gemini CLI 多账户轮询 36 | - 支持 Claude Code 多账户轮询 37 | - 支持 Qwen Code 多账户轮询 38 | - 支持 iFlow 多账户轮询 39 | - 支持 OpenAI Codex 多账户轮询 40 | - 通过配置接入上游 OpenAI 兼容提供商(例如 OpenRouter) 41 | - 可复用的 Go SDK(见 `docs/sdk-usage_CN.md`) 42 | 43 | ## 新手入门 44 | 45 | CLIProxyAPI 用户手册: [https://help.router-for.me/](https://help.router-for.me/cn/) 46 | 47 | ## 管理 API 文档 48 | 49 | 请参见 [MANAGEMENT_API_CN.md](https://help.router-for.me/cn/management/api) 50 | 51 | ## Amp CLI 支持 52 | 53 | CLIProxyAPI 已内置对 [Amp CLI](https://ampcode.com) 和 Amp IDE 扩展的支持,可让你使用自己的 Google/ChatGPT/Claude OAuth 订阅来配合 Amp 编码工具: 54 | 55 | - 提供商路由别名,兼容 Amp 的 API 路径模式(`/api/provider/{provider}/v1...`) 56 | - 管理代理,处理 OAuth 认证和账号功能 57 | - 智能模型回退与自动路由 58 | - 以安全为先的设计,管理端点仅限 localhost 59 | 60 | **→ [Amp CLI 完整集成指南](https://help.router-for.me/cn/agent-client/amp-cli.html)** 61 | 62 | ## SDK 文档 63 | 64 | - 使用文档:[docs/sdk-usage_CN.md](docs/sdk-usage_CN.md) 65 | - 高级(执行器与翻译器):[docs/sdk-advanced_CN.md](docs/sdk-advanced_CN.md) 66 | - 认证: [docs/sdk-access_CN.md](docs/sdk-access_CN.md) 67 | - 凭据加载/更新: [docs/sdk-watcher_CN.md](docs/sdk-watcher_CN.md) 68 | - 自定义 Provider 示例:`examples/custom-provider` 69 | 70 | ## 贡献 71 | 72 | 欢迎贡献!请随时提交 Pull Request。 73 | 74 | 1. Fork 仓库 75 | 2. 创建您的功能分支(`git checkout -b feature/amazing-feature`) 76 | 3. 提交您的更改(`git commit -m 'Add some amazing feature'`) 77 | 4. 推送到分支(`git push origin feature/amazing-feature`) 78 | 5. 打开 Pull Request 79 | 80 | ## 谁与我们在一起? 81 | 82 | 这些项目基于 CLIProxyAPI: 83 | 84 | ### [vibeproxy](https://github.com/automazeio/vibeproxy) 85 | 86 | 一个原生 macOS 菜单栏应用,让您可以使用 Claude Code & ChatGPT 订阅服务和 AI 编程工具,无需 API 密钥。 87 | 88 | ### [Subtitle Translator](https://github.com/VjayC/SRT-Subtitle-Translator-Validator) 89 | 90 | 一款基于浏览器的 SRT 字幕翻译工具,可通过 CLI 代理 API 使用您的 Gemini 订阅。内置自动验证与错误修正功能,无需 API 密钥。 91 | 92 | ### [CCS (Claude Code Switch)](https://github.com/kaitranntt/ccs) 93 | 94 | CLI 封装器,用于通过 CLIProxyAPI OAuth 即时切换多个 Claude 账户和替代模型(Gemini, Codex, Antigravity),无需 API 密钥。 95 | 96 | ### [ProxyPal](https://github.com/heyhuynhgiabuu/proxypal) 97 | 98 | 基于 macOS 平台的原生 CLIProxyAPI GUI:配置供应商、模型映射以及OAuth端点,无需 API 密钥。 99 | 100 | > [!NOTE] 101 | > 如果你开发了基于 CLIProxyAPI 的项目,请提交一个 PR(拉取请求)将其添加到此列表中。 102 | 103 | ## 许可证 104 | 105 | 此项目根据 MIT 许可证授权 - 有关详细信息,请参阅 [LICENSE](LICENSE) 文件。 106 | 107 | ## 写给所有中国网友的 108 | 109 | QQ 群:188637136 110 | 111 | 或 112 | 113 | Telegram 群:https://t.me/CLIProxyAPI 114 | -------------------------------------------------------------------------------- /internal/api/modules/amp/response_rewriter.go: -------------------------------------------------------------------------------- 1 | package amp 2 | 3 | import ( 4 | "bytes" 5 | "net/http" 6 | "strings" 7 | 8 | "github.com/gin-gonic/gin" 9 | log "github.com/sirupsen/logrus" 10 | "github.com/tidwall/gjson" 11 | "github.com/tidwall/sjson" 12 | ) 13 | 14 | // ResponseRewriter wraps a gin.ResponseWriter to intercept and modify the response body 15 | // It's used to rewrite model names in responses when model mapping is used 16 | type ResponseRewriter struct { 17 | gin.ResponseWriter 18 | body *bytes.Buffer 19 | originalModel string 20 | isStreaming bool 21 | } 22 | 23 | // NewResponseRewriter creates a new response rewriter for model name substitution 24 | func NewResponseRewriter(w gin.ResponseWriter, originalModel string) *ResponseRewriter { 25 | return &ResponseRewriter{ 26 | ResponseWriter: w, 27 | body: &bytes.Buffer{}, 28 | originalModel: originalModel, 29 | } 30 | } 31 | 32 | // Write intercepts response writes and buffers them for model name replacement 33 | func (rw *ResponseRewriter) Write(data []byte) (int, error) { 34 | // Detect streaming on first write 35 | if rw.body.Len() == 0 && !rw.isStreaming { 36 | contentType := rw.Header().Get("Content-Type") 37 | rw.isStreaming = strings.Contains(contentType, "text/event-stream") || 38 | strings.Contains(contentType, "stream") 39 | } 40 | 41 | if rw.isStreaming { 42 | n, err := rw.ResponseWriter.Write(rw.rewriteStreamChunk(data)) 43 | if err == nil { 44 | if flusher, ok := rw.ResponseWriter.(http.Flusher); ok { 45 | flusher.Flush() 46 | } 47 | } 48 | return n, err 49 | } 50 | return rw.body.Write(data) 51 | } 52 | 53 | // Flush writes the buffered response with model names rewritten 54 | func (rw *ResponseRewriter) Flush() { 55 | if rw.isStreaming { 56 | if flusher, ok := rw.ResponseWriter.(http.Flusher); ok { 57 | flusher.Flush() 58 | } 59 | return 60 | } 61 | if rw.body.Len() > 0 { 62 | if _, err := rw.ResponseWriter.Write(rw.rewriteModelInResponse(rw.body.Bytes())); err != nil { 63 | log.Warnf("amp response rewriter: failed to write rewritten response: %v", err) 64 | } 65 | } 66 | } 67 | 68 | // modelFieldPaths lists all JSON paths where model name may appear 69 | var modelFieldPaths = []string{"model", "modelVersion", "response.modelVersion", "message.model"} 70 | 71 | // rewriteModelInResponse replaces all occurrences of the mapped model with the original model in JSON 72 | func (rw *ResponseRewriter) rewriteModelInResponse(data []byte) []byte { 73 | if rw.originalModel == "" { 74 | return data 75 | } 76 | for _, path := range modelFieldPaths { 77 | if gjson.GetBytes(data, path).Exists() { 78 | data, _ = sjson.SetBytes(data, path, rw.originalModel) 79 | } 80 | } 81 | return data 82 | } 83 | 84 | // rewriteStreamChunk rewrites model names in SSE stream chunks 85 | func (rw *ResponseRewriter) rewriteStreamChunk(chunk []byte) []byte { 86 | if rw.originalModel == "" { 87 | return chunk 88 | } 89 | 90 | // SSE format: "data: {json}\n\n" 91 | lines := bytes.Split(chunk, []byte("\n")) 92 | for i, line := range lines { 93 | if bytes.HasPrefix(line, []byte("data: ")) { 94 | jsonData := bytes.TrimPrefix(line, []byte("data: ")) 95 | if len(jsonData) > 0 && jsonData[0] == '{' { 96 | // Rewrite JSON in the data line 97 | rewritten := rw.rewriteModelInResponse(jsonData) 98 | lines[i] = append([]byte("data: "), rewritten...) 99 | } 100 | } 101 | } 102 | 103 | return bytes.Join(lines, []byte("\n")) 104 | } 105 | -------------------------------------------------------------------------------- /sdk/auth/qwen.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "strings" 7 | "time" 8 | 9 | "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/qwen" 10 | "github.com/router-for-me/CLIProxyAPI/v6/internal/browser" 11 | // legacy client removed 12 | "github.com/router-for-me/CLIProxyAPI/v6/internal/config" 13 | coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" 14 | log "github.com/sirupsen/logrus" 15 | ) 16 | 17 | // QwenAuthenticator implements the device flow login for Qwen accounts. 18 | type QwenAuthenticator struct{} 19 | 20 | // NewQwenAuthenticator constructs a Qwen authenticator. 21 | func NewQwenAuthenticator() *QwenAuthenticator { 22 | return &QwenAuthenticator{} 23 | } 24 | 25 | func (a *QwenAuthenticator) Provider() string { 26 | return "qwen" 27 | } 28 | 29 | func (a *QwenAuthenticator) RefreshLead() *time.Duration { 30 | d := 3 * time.Hour 31 | return &d 32 | } 33 | 34 | func (a *QwenAuthenticator) Login(ctx context.Context, cfg *config.Config, opts *LoginOptions) (*coreauth.Auth, error) { 35 | if cfg == nil { 36 | return nil, fmt.Errorf("cliproxy auth: configuration is required") 37 | } 38 | if ctx == nil { 39 | ctx = context.Background() 40 | } 41 | if opts == nil { 42 | opts = &LoginOptions{} 43 | } 44 | 45 | authSvc := qwen.NewQwenAuth(cfg) 46 | 47 | deviceFlow, err := authSvc.InitiateDeviceFlow(ctx) 48 | if err != nil { 49 | return nil, fmt.Errorf("qwen device flow initiation failed: %w", err) 50 | } 51 | 52 | authURL := deviceFlow.VerificationURIComplete 53 | 54 | if !opts.NoBrowser { 55 | fmt.Println("Opening browser for Qwen authentication") 56 | if !browser.IsAvailable() { 57 | log.Warn("No browser available; please open the URL manually") 58 | fmt.Printf("Visit the following URL to continue authentication:\n%s\n", authURL) 59 | } else if err = browser.OpenURL(authURL); err != nil { 60 | log.Warnf("Failed to open browser automatically: %v", err) 61 | fmt.Printf("Visit the following URL to continue authentication:\n%s\n", authURL) 62 | } 63 | } else { 64 | fmt.Printf("Visit the following URL to continue authentication:\n%s\n", authURL) 65 | } 66 | 67 | fmt.Println("Waiting for Qwen authentication...") 68 | 69 | tokenData, err := authSvc.PollForToken(deviceFlow.DeviceCode, deviceFlow.CodeVerifier) 70 | if err != nil { 71 | return nil, fmt.Errorf("qwen authentication failed: %w", err) 72 | } 73 | 74 | tokenStorage := authSvc.CreateTokenStorage(tokenData) 75 | 76 | email := "" 77 | if opts.Metadata != nil { 78 | email = opts.Metadata["email"] 79 | if email == "" { 80 | email = opts.Metadata["alias"] 81 | } 82 | } 83 | 84 | if email == "" && opts.Prompt != nil { 85 | email, err = opts.Prompt("Please input your email address or alias for Qwen:") 86 | if err != nil { 87 | return nil, err 88 | } 89 | } 90 | 91 | email = strings.TrimSpace(email) 92 | if email == "" { 93 | return nil, &EmailRequiredError{Prompt: "Please provide an email address or alias for Qwen."} 94 | } 95 | 96 | tokenStorage.Email = email 97 | 98 | // no legacy client construction 99 | 100 | fileName := fmt.Sprintf("qwen-%s.json", tokenStorage.Email) 101 | metadata := map[string]any{ 102 | "email": tokenStorage.Email, 103 | } 104 | 105 | fmt.Println("Qwen authentication successful") 106 | 107 | return &coreauth.Auth{ 108 | ID: fileName, 109 | Provider: a.Provider(), 110 | FileName: fileName, 111 | Storage: tokenStorage, 112 | Metadata: metadata, 113 | }, nil 114 | } 115 | -------------------------------------------------------------------------------- /sdk/config/config.go: -------------------------------------------------------------------------------- 1 | // Package config provides configuration management for the CLI Proxy API server. 2 | // It handles loading and parsing YAML configuration files, and provides structured 3 | // access to application settings including server port, authentication directory, 4 | // debug settings, proxy configuration, and API keys. 5 | package config 6 | 7 | // SDKConfig represents the application's configuration, loaded from a YAML file. 8 | type SDKConfig struct { 9 | // ProxyURL is the URL of an optional proxy server to use for outbound requests. 10 | ProxyURL string `yaml:"proxy-url" json:"proxy-url"` 11 | 12 | // ForceModelPrefix requires explicit model prefixes (e.g., "teamA/gemini-3-pro-preview") 13 | // to target prefixed credentials. When false, unprefixed model requests may use prefixed 14 | // credentials as well. 15 | ForceModelPrefix bool `yaml:"force-model-prefix" json:"force-model-prefix"` 16 | 17 | // RequestLog enables or disables detailed request logging functionality. 18 | RequestLog bool `yaml:"request-log" json:"request-log"` 19 | 20 | // APIKeys is a list of keys for authenticating clients to this proxy server. 21 | APIKeys []string `yaml:"api-keys" json:"api-keys"` 22 | 23 | // Access holds request authentication provider configuration. 24 | Access AccessConfig `yaml:"auth,omitempty" json:"auth,omitempty"` 25 | } 26 | 27 | // AccessConfig groups request authentication providers. 28 | type AccessConfig struct { 29 | // Providers lists configured authentication providers. 30 | Providers []AccessProvider `yaml:"providers,omitempty" json:"providers,omitempty"` 31 | } 32 | 33 | // AccessProvider describes a request authentication provider entry. 34 | type AccessProvider struct { 35 | // Name is the instance identifier for the provider. 36 | Name string `yaml:"name" json:"name"` 37 | 38 | // Type selects the provider implementation registered via the SDK. 39 | Type string `yaml:"type" json:"type"` 40 | 41 | // SDK optionally names a third-party SDK module providing this provider. 42 | SDK string `yaml:"sdk,omitempty" json:"sdk,omitempty"` 43 | 44 | // APIKeys lists inline keys for providers that require them. 45 | APIKeys []string `yaml:"api-keys,omitempty" json:"api-keys,omitempty"` 46 | 47 | // Config passes provider-specific options to the implementation. 48 | Config map[string]any `yaml:"config,omitempty" json:"config,omitempty"` 49 | } 50 | 51 | const ( 52 | // AccessProviderTypeConfigAPIKey is the built-in provider validating inline API keys. 53 | AccessProviderTypeConfigAPIKey = "config-api-key" 54 | 55 | // DefaultAccessProviderName is applied when no provider name is supplied. 56 | DefaultAccessProviderName = "config-inline" 57 | ) 58 | 59 | // ConfigAPIKeyProvider returns the first inline API key provider if present. 60 | func (c *SDKConfig) ConfigAPIKeyProvider() *AccessProvider { 61 | if c == nil { 62 | return nil 63 | } 64 | for i := range c.Access.Providers { 65 | if c.Access.Providers[i].Type == AccessProviderTypeConfigAPIKey { 66 | if c.Access.Providers[i].Name == "" { 67 | c.Access.Providers[i].Name = DefaultAccessProviderName 68 | } 69 | return &c.Access.Providers[i] 70 | } 71 | } 72 | return nil 73 | } 74 | 75 | // MakeInlineAPIKeyProvider constructs an inline API key provider configuration. 76 | // It returns nil when no keys are supplied. 77 | func MakeInlineAPIKeyProvider(keys []string) *AccessProvider { 78 | if len(keys) == 0 { 79 | return nil 80 | } 81 | provider := &AccessProvider{ 82 | Name: DefaultAccessProviderName, 83 | Type: AccessProviderTypeConfigAPIKey, 84 | APIKeys: append([]string(nil), keys...), 85 | } 86 | return provider 87 | } 88 | -------------------------------------------------------------------------------- /internal/logging/global_logger.go: -------------------------------------------------------------------------------- 1 | package logging 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "os" 8 | "path/filepath" 9 | "strings" 10 | "sync" 11 | 12 | "github.com/gin-gonic/gin" 13 | "github.com/router-for-me/CLIProxyAPI/v6/internal/util" 14 | log "github.com/sirupsen/logrus" 15 | "gopkg.in/natefinch/lumberjack.v2" 16 | ) 17 | 18 | var ( 19 | setupOnce sync.Once 20 | writerMu sync.Mutex 21 | logWriter *lumberjack.Logger 22 | ginInfoWriter *io.PipeWriter 23 | ginErrorWriter *io.PipeWriter 24 | ) 25 | 26 | // LogFormatter defines a custom log format for logrus. 27 | // This formatter adds timestamp, level, and source location to each log entry. 28 | type LogFormatter struct{} 29 | 30 | // Format renders a single log entry with custom formatting. 31 | func (m *LogFormatter) Format(entry *log.Entry) ([]byte, error) { 32 | var buffer *bytes.Buffer 33 | if entry.Buffer != nil { 34 | buffer = entry.Buffer 35 | } else { 36 | buffer = &bytes.Buffer{} 37 | } 38 | 39 | timestamp := entry.Time.Format("2006-01-02 15:04:05") 40 | message := strings.TrimRight(entry.Message, "\r\n") 41 | 42 | var formatted string 43 | if entry.Caller != nil { 44 | formatted = fmt.Sprintf("[%s] [%s] [%s:%d] %s\n", timestamp, entry.Level, filepath.Base(entry.Caller.File), entry.Caller.Line, message) 45 | } else { 46 | formatted = fmt.Sprintf("[%s] [%s] %s\n", timestamp, entry.Level, message) 47 | } 48 | buffer.WriteString(formatted) 49 | 50 | return buffer.Bytes(), nil 51 | } 52 | 53 | // SetupBaseLogger configures the shared logrus instance and Gin writers. 54 | // It is safe to call multiple times; initialization happens only once. 55 | func SetupBaseLogger() { 56 | setupOnce.Do(func() { 57 | log.SetOutput(os.Stdout) 58 | log.SetReportCaller(true) 59 | log.SetFormatter(&LogFormatter{}) 60 | 61 | ginInfoWriter = log.StandardLogger().Writer() 62 | gin.DefaultWriter = ginInfoWriter 63 | ginErrorWriter = log.StandardLogger().WriterLevel(log.ErrorLevel) 64 | gin.DefaultErrorWriter = ginErrorWriter 65 | gin.DebugPrintFunc = func(format string, values ...interface{}) { 66 | format = strings.TrimRight(format, "\r\n") 67 | log.StandardLogger().Infof(format, values...) 68 | } 69 | 70 | log.RegisterExitHandler(closeLogOutputs) 71 | }) 72 | } 73 | 74 | // ConfigureLogOutput switches the global log destination between rotating files and stdout. 75 | func ConfigureLogOutput(loggingToFile bool) error { 76 | SetupBaseLogger() 77 | 78 | writerMu.Lock() 79 | defer writerMu.Unlock() 80 | 81 | if loggingToFile { 82 | logDir := "logs" 83 | if base := util.WritablePath(); base != "" { 84 | logDir = filepath.Join(base, "logs") 85 | } 86 | if err := os.MkdirAll(logDir, 0o755); err != nil { 87 | return fmt.Errorf("logging: failed to create log directory: %w", err) 88 | } 89 | if logWriter != nil { 90 | _ = logWriter.Close() 91 | } 92 | logWriter = &lumberjack.Logger{ 93 | Filename: filepath.Join(logDir, "main.log"), 94 | MaxSize: 10, 95 | MaxBackups: 0, 96 | MaxAge: 0, 97 | Compress: false, 98 | } 99 | log.SetOutput(logWriter) 100 | return nil 101 | } 102 | 103 | if logWriter != nil { 104 | _ = logWriter.Close() 105 | logWriter = nil 106 | } 107 | log.SetOutput(os.Stdout) 108 | return nil 109 | } 110 | 111 | func closeLogOutputs() { 112 | writerMu.Lock() 113 | defer writerMu.Unlock() 114 | 115 | if logWriter != nil { 116 | _ = logWriter.Close() 117 | logWriter = nil 118 | } 119 | if ginInfoWriter != nil { 120 | _ = ginInfoWriter.Close() 121 | ginInfoWriter = nil 122 | } 123 | if ginErrorWriter != nil { 124 | _ = ginErrorWriter.Close() 125 | ginErrorWriter = nil 126 | } 127 | } 128 | --------------------------------------------------------------------------------