Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions cmd/server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ func main() {
var codeBuddyAILogin bool
var btLogin bool
var qoderLogin bool
var codeartsLogin bool
var projectID string
var vertexImport string
var vertexImportPrefix string
Expand Down Expand Up @@ -141,6 +142,7 @@ func main() {
flag.BoolVar(&codeBuddyAILogin, "codebuddy-ai-login", false, "Login to CodeBuddy AI (www.codebuddy.ai) using browser OAuth flow")
flag.BoolVar(&btLogin, "bt-login", false, "Login to BaoTa Panel AI using phone and password")
flag.BoolVar(&qoderLogin, "qoder-login", false, "Login to Qoder using PKCE browser flow")
flag.BoolVar(&codeartsLogin, "codearts-login", false, "Login to HuaweiCloud CodeArts using OAuth")
flag.StringVar(&projectID, "project_id", "", "Project ID (Gemini only, not required)")
flag.StringVar(&configPath, "config", DefaultConfigPath, "Configure File Path")
flag.StringVar(&vertexImport, "vertex-import", "", "Import Vertex service account key JSON file")
Expand Down Expand Up @@ -595,6 +597,8 @@ func main() {
cmd.DoBTLogin(cfg)
} else if qoderLogin {
cmd.DoQoderLogin(cfg, options)
} else if codeartsLogin {
cmd.DoCodeArtsLogin(cfg, options)
} else {
// In cloud deploy mode without config file, just wait for shutdown signals
if isCloudDeploy && !configFileExists {
Expand Down
4 changes: 2 additions & 2 deletions config.example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -348,7 +348,7 @@ nonstream-keepalive-interval: 0

# Global OAuth model name aliases (per channel)
# These aliases rename model IDs for both model listing and request routing.
# Supported channels: gemini-cli, vertex, aistudio, antigravity, claude, codex, iflow, kiro, github-copilot, kimi.
# Supported channels: gemini-cli, vertex, aistudio, antigravity, claude, codex, iflow, kiro, github-copilot, kimi, codearts.
# NOTE: Aliases do not apply to gemini-api-key, codex-api-key, claude-api-key, openai-compatibility, vertex-api-key, or ampcode.
# NOTE: Because aliases affect the merged /v1 model list and merged request routing, overlapping
# client-visible names can become ambiguous across providers. /api/provider/{provider}/... helps
Expand Down Expand Up @@ -398,7 +398,7 @@ nonstream-keepalive-interval: 0
# alias: "copilot-gpt5"

# OAuth provider excluded models
# Supported channels: gemini-cli, vertex, aistudio, antigravity, claude, codex, qwen, iflow, kiro, github-copilot.
# Supported channels: gemini-cli, vertex, aistudio, antigravity, claude, codex, iflow, kiro, github-copilot, codearts.
# oauth-excluded-models:
# gemini-cli:
# - "gemini-2.5-pro" # exclude specific models (exact match)
Expand Down
28 changes: 14 additions & 14 deletions internal/api/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -306,12 +306,12 @@ func NewServer(cfg *config.Config, authManager *auth.Manager, accessManager *sdk
s.registerManagementRoutes()
}

// === CLIProxyAPIPlus 扩展: 注册 Kiro OAuth Web 路由 ===
// === CLIProxyAPIPlus extension: Register Kiro OAuth Web routes ===
kiroOAuthHandler := kiro.NewOAuthWebHandler(cfg)
kiroOAuthHandler.RegisterRoutes(engine)
log.Info("Kiro OAuth Web routes registered at /v0/oauth/kiro/*")

// === CLIProxyAPIPlus 扩展: 注册 CodeArts OAuth Web 路由 ===
// === CLIProxyAPIPlus extension: Register CodeArts OAuth Web routes ===
codeArtsOAuthHandler := codearts.NewOAuthWebHandler(cfg)
codeArtsOAuthHandler.RegisterRoutes(engine)
log.Info("CodeArts OAuth Web routes registered at /v0/oauth/codearts/*")
Expand Down Expand Up @@ -702,19 +702,19 @@ func (s *Server) registerManagementRoutes() {
mgmt.GET("/gitlab-auth-url", s.mgmt.RequestGitLabToken)
mgmt.POST("/gitlab-auth-url", s.mgmt.RequestGitLabPATToken)
mgmt.GET("/gemini-cli-auth-url", s.mgmt.RequestGeminiCLIToken)
mgmt.GET("/antigravity-auth-url", s.mgmt.RequestAntigravityToken)
mgmt.GET("/kilo-auth-url", s.mgmt.RequestKiloToken)
mgmt.GET("/kimi-auth-url", s.mgmt.RequestKimiToken)
mgmt.GET("/iflow-auth-url", s.mgmt.RequestIFlowToken)
mgmt.POST("/iflow-auth-url", s.mgmt.RequestIFlowCookieToken)
mgmt.GET("/kiro-auth-url", s.mgmt.RequestKiroToken)
mgmt.GET("/cursor-auth-url", s.mgmt.RequestCursorToken)
mgmt.GET("/github-auth-url", s.mgmt.RequestGitHubToken)
mgmt.GET("/qoder-auth-url", s.mgmt.RequestQoderToken)
mgmt.POST("/oauth-callback", s.mgmt.PostOAuthCallback)
mgmt.GET("/get-auth-status", s.mgmt.GetAuthStatus)
}
mgmt.GET("/antigravity-auth-url", s.mgmt.RequestAntigravityToken)
mgmt.GET("/kilo-auth-url", s.mgmt.RequestKiloToken)
mgmt.GET("/kimi-auth-url", s.mgmt.RequestKimiToken)
mgmt.GET("/iflow-auth-url", s.mgmt.RequestIFlowToken)
mgmt.POST("/iflow-auth-url", s.mgmt.RequestIFlowCookieToken)
mgmt.GET("/kiro-auth-url", s.mgmt.RequestKiroToken)
mgmt.GET("/cursor-auth-url", s.mgmt.RequestCursorToken)
mgmt.GET("/github-auth-url", s.mgmt.RequestGitHubToken)
mgmt.GET("/qoder-auth-url", s.mgmt.RequestQoderToken)
mgmt.POST("/oauth-callback", s.mgmt.PostOAuthCallback)
mgmt.GET("/get-auth-status", s.mgmt.GetAuthStatus)
}
}

func (s *Server) managementAvailabilityMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
Expand Down
2 changes: 1 addition & 1 deletion internal/auth/codearts/codearts_auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,7 @@ func (a *CodeArtsAuth) RefreshToken(ctx context.Context, token *CodeArtsTokenDat
req.Header.Set("Access-Key", token.AK)

// Sign with SDK-HMAC-SHA256
SignRequest(req, token.AK, token.SK, token.SecurityToken)
SignRequest(req, body, token.AK, token.SK, token.SecurityToken)

resp, err := a.httpClient.Do(req)
if err != nil {
Expand Down
32 changes: 16 additions & 16 deletions internal/auth/codearts/oauth_web.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,11 @@ import (
type sessionStatus string

const (
sPending sessionStatus = "pending"
sWaitingCB sessionStatus = "waiting_callback"
sPolling sessionStatus = "polling"
sSuccess sessionStatus = "success"
sFailed sessionStatus = "failed"
sPending sessionStatus = "pending"
sWaitingCB sessionStatus = "waiting_callback"
sPolling sessionStatus = "polling"
sSuccess sessionStatus = "success"
sFailed sessionStatus = "failed"
)

type webSession struct {
Expand Down Expand Up @@ -301,17 +301,17 @@ func (h *OAuthWebHandler) saveTokenToFile(tokenData *CodeArtsTokenData) {
// Save in the same format as the file synthesizer expects:
// { "type": "codearts", ... }
storage := map[string]interface{}{
"type": "codearts",
"ak": tokenData.AK,
"sk": tokenData.SK,
"security_token": tokenData.SecurityToken,
"x_auth_token": tokenData.XAuthToken,
"expires_at": tokenData.ExpiresAt.Format(time.RFC3339),
"user_id": tokenData.UserID,
"user_name": tokenData.UserName,
"domain_id": tokenData.DomainID,
"email": tokenData.Email,
"last_refresh": time.Now().Format(time.RFC3339),
"type": "codearts",
"ak": tokenData.AK,
"sk": tokenData.SK,
"security_token": tokenData.SecurityToken,
"x_auth_token": tokenData.XAuthToken,
"expires_at": tokenData.ExpiresAt.Format(time.RFC3339),
"user_id": tokenData.UserID,
"user_name": tokenData.UserName,
"domain_id": tokenData.DomainID,
"email": tokenData.Email,
"last_refresh": time.Now().Format(time.RFC3339),
}

data, err := json.MarshalIndent(storage, "", " ")
Expand Down
4 changes: 2 additions & 2 deletions internal/auth/codearts/signer.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import (
// - Single-step HMAC (no derived key)
// - Path must end with "/"
// - Algorithm name is "SDK-HMAC-SHA256"
func SignRequest(req *http.Request, ak, sk, securityToken string) {
func SignRequest(req *http.Request, body []byte, ak, sk, securityToken string) {
now := time.Now().UTC()
timeStr := now.Format("20060102T150405Z")

Expand Down Expand Up @@ -72,7 +72,7 @@ func SignRequest(req *http.Request, ak, sk, securityToken string) {
signedHeadersStr := strings.Join(signedHeaderKeys, ";")

// Body hash (empty for GET, or use existing hash)
bodyHash := sha256Hex([]byte(""))
bodyHash := sha256Hex(body)

canonicalReq := fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n%s",
method, path, canonicalQuery,
Expand Down
1 change: 1 addition & 0 deletions internal/cmd/auth_manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ func newAuthManager() *sdkAuth.Manager {
sdkAuth.NewCodeBuddyAIAuthenticator(),
sdkAuth.NewCursorAuthenticator(),
sdkAuth.NewQoderAuthenticator(),
sdkAuth.NewCodeArtsAuthenticator(),
)
return manager
}
37 changes: 37 additions & 0 deletions internal/cmd/codearts_login.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package cmd

import (
"context"
"fmt"

"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
sdkAuth "github.com/router-for-me/CLIProxyAPI/v6/sdk/auth"
log "github.com/sirupsen/logrus"
)

func DoCodeArtsLogin(cfg *config.Config, options *LoginOptions) {
if options == nil {
options = &LoginOptions{}
}

manager := newAuthManager()
authOpts := &sdkAuth.LoginOptions{
NoBrowser: options.NoBrowser,
CallbackPort: options.CallbackPort,
Metadata: map[string]string{},
}

record, savedPath, err := manager.Login(context.Background(), "codearts", cfg, authOpts)
if err != nil {
log.Errorf("CodeArts authentication failed: %v", err)
return
}

if savedPath != "" {
fmt.Printf("Authentication saved to %s\n", savedPath)
}
if record != nil && record.Label != "" {
fmt.Printf("Authenticated as %s\n", record.Label)
}
fmt.Println("CodeArts authentication successful!")
}
4 changes: 2 additions & 2 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -138,12 +138,12 @@ type Config struct {
AmpCode AmpCode `yaml:"ampcode" json:"ampcode"`

// OAuthExcludedModels defines per-provider global model exclusions applied to OAuth/file-backed auth entries.
// Supported channels: gemini-cli, vertex, aistudio, antigravity, claude, codex, iflow, kiro, github-copilot, kimi.
// Supported channels: gemini-cli, vertex, aistudio, antigravity, claude, codex, iflow, kiro, github-copilot, kimi, codearts.
OAuthExcludedModels map[string][]string `yaml:"oauth-excluded-models,omitempty" json:"oauth-excluded-models,omitempty"`

// OAuthModelAlias defines global model name aliases for OAuth/file-backed auth channels.
// These aliases affect both model listing and model routing for supported channels:
// gemini-cli, vertex, aistudio, antigravity, claude, codex, iflow, kiro, github-copilot, kimi.
// gemini-cli, vertex, aistudio, antigravity, claude, codex, iflow, kiro, github-copilot, kimi, codearts.
//
// NOTE: This does not apply to existing per-credential model alias features under:
// gemini-api-key, codex-api-key, claude-api-key, openai-compatibility, vertex-api-key, and ampcode.
Expand Down
Loading
Loading