v6 version first commit

This commit is contained in:
Luis Pater
2025-09-22 01:40:24 +08:00
parent d42384cdb7
commit 4999fce7f4
171 changed files with 7626 additions and 7494 deletions

178
sdk/auth/claude.go Normal file
View File

@@ -0,0 +1,178 @@
package auth
import (
"context"
"fmt"
"net/http"
"strings"
"time"
"github.com/router-for-me/CLIProxyAPI/v6/internal/auth/claude"
"github.com/router-for-me/CLIProxyAPI/v6/internal/browser"
// legacy client removed
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
"github.com/router-for-me/CLIProxyAPI/v6/internal/misc"
"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
log "github.com/sirupsen/logrus"
)
// ClaudeAuthenticator implements the OAuth login flow for Anthropic Claude accounts.
type ClaudeAuthenticator struct {
CallbackPort int
}
// NewClaudeAuthenticator constructs a Claude authenticator with default settings.
func NewClaudeAuthenticator() *ClaudeAuthenticator {
return &ClaudeAuthenticator{CallbackPort: 54545}
}
func (a *ClaudeAuthenticator) Provider() string {
return "claude"
}
func (a *ClaudeAuthenticator) RefreshLead() *time.Duration {
d := 4 * time.Hour
return &d
}
func (a *ClaudeAuthenticator) Login(ctx context.Context, cfg *config.Config, opts *LoginOptions) (*TokenRecord, error) {
if cfg == nil {
return nil, fmt.Errorf("cliproxy auth: configuration is required")
}
if ctx == nil {
ctx = context.Background()
}
if opts == nil {
opts = &LoginOptions{}
}
pkceCodes, err := claude.GeneratePKCECodes()
if err != nil {
return nil, fmt.Errorf("claude pkce generation failed: %w", err)
}
state, err := misc.GenerateRandomState()
if err != nil {
return nil, fmt.Errorf("claude state generation failed: %w", err)
}
oauthServer := claude.NewOAuthServer(a.CallbackPort)
if err = oauthServer.Start(); err != nil {
if strings.Contains(err.Error(), "already in use") {
return nil, claude.NewAuthenticationError(claude.ErrPortInUse, err)
}
return nil, claude.NewAuthenticationError(claude.ErrServerStartFailed, err)
}
defer func() {
stopCtx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
if stopErr := oauthServer.Stop(stopCtx); stopErr != nil {
log.Warnf("claude oauth server stop error: %v", stopErr)
}
}()
authSvc := claude.NewClaudeAuth(cfg)
authURL, returnedState, err := authSvc.GenerateAuthURL(state, pkceCodes)
if err != nil {
return nil, fmt.Errorf("claude authorization url generation failed: %w", err)
}
state = returnedState
if !opts.NoBrowser {
log.Info("Opening browser for Claude authentication")
if !browser.IsAvailable() {
log.Warn("No browser available; please open the URL manually")
util.PrintSSHTunnelInstructions(a.CallbackPort)
log.Infof("Visit the following URL to continue authentication:\n%s", authURL)
} else if err = browser.OpenURL(authURL); err != nil {
log.Warnf("Failed to open browser automatically: %v", err)
util.PrintSSHTunnelInstructions(a.CallbackPort)
log.Infof("Visit the following URL to continue authentication:\n%s", authURL)
}
} else {
util.PrintSSHTunnelInstructions(a.CallbackPort)
log.Infof("Visit the following URL to continue authentication:\n%s", authURL)
}
log.Info("Waiting for Claude authentication callback...")
result, err := oauthServer.WaitForCallback(5 * time.Minute)
if err != nil {
if strings.Contains(err.Error(), "timeout") {
return nil, claude.NewAuthenticationError(claude.ErrCallbackTimeout, err)
}
return nil, err
}
if result.Error != "" {
return nil, claude.NewOAuthError(result.Error, "", http.StatusBadRequest)
}
if result.State != state {
return nil, claude.NewAuthenticationError(claude.ErrInvalidState, fmt.Errorf("state mismatch"))
}
log.Debug("Claude authorization code received; exchanging for tokens")
authBundle, err := authSvc.ExchangeCodeForTokens(ctx, result.Code, state, pkceCodes)
if err != nil {
return nil, claude.NewAuthenticationError(claude.ErrCodeExchangeFailed, err)
}
tokenStorage := authSvc.CreateTokenStorage(authBundle)
if tokenStorage == nil || tokenStorage.Email == "" {
return nil, fmt.Errorf("claude token storage missing account information")
}
fileName := fmt.Sprintf("claude-%s.json", tokenStorage.Email)
metadata := map[string]string{
"email": tokenStorage.Email,
}
log.Info("Claude authentication successful")
if authBundle.APIKey != "" {
log.Info("Claude API key obtained and stored")
}
return &TokenRecord{
Provider: a.Provider(),
FileName: fileName,
Storage: tokenStorage,
Metadata: metadata,
}, nil
}
func (a *ClaudeAuthenticator) Refresh(ctx context.Context, cfg *config.Config, record *TokenRecord) (*TokenRecord, error) {
if record == nil || record.Storage == nil {
return nil, fmt.Errorf("cliproxy auth: empty token record for claude refresh")
}
if cfg == nil {
return nil, fmt.Errorf("cliproxy auth: configuration is required")
}
if ctx == nil {
ctx = context.Background()
}
storage, ok := record.Storage.(*claude.ClaudeTokenStorage)
if !ok {
return nil, fmt.Errorf("cliproxy auth: unexpected token storage type for claude refresh")
}
// Refresh via auth service directly (no legacy client)
svc := claude.NewClaudeAuth(cfg)
td, err := svc.RefreshTokensWithRetry(ctx, storage.RefreshToken, 3)
if err != nil {
return nil, err
}
svc.UpdateTokenStorage(storage, td)
result := &TokenRecord{
Provider: a.Provider(),
FileName: record.FileName,
Storage: storage,
Metadata: record.Metadata,
}
return result, nil
}

176
sdk/auth/codex.go Normal file
View File

@@ -0,0 +1,176 @@
package auth
import (
"context"
"fmt"
"net/http"
"strings"
"time"
"github.com/router-for-me/CLIProxyAPI/v6/internal/auth/codex"
"github.com/router-for-me/CLIProxyAPI/v6/internal/browser"
// legacy client removed
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
"github.com/router-for-me/CLIProxyAPI/v6/internal/misc"
"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
log "github.com/sirupsen/logrus"
)
// CodexAuthenticator implements the OAuth login flow for Codex accounts.
type CodexAuthenticator struct {
CallbackPort int
}
// NewCodexAuthenticator constructs a Codex authenticator with default settings.
func NewCodexAuthenticator() *CodexAuthenticator {
return &CodexAuthenticator{CallbackPort: 1455}
}
func (a *CodexAuthenticator) Provider() string {
return "codex"
}
func (a *CodexAuthenticator) RefreshLead() *time.Duration {
d := 5 * 24 * time.Hour
return &d
}
func (a *CodexAuthenticator) Login(ctx context.Context, cfg *config.Config, opts *LoginOptions) (*TokenRecord, error) {
if cfg == nil {
return nil, fmt.Errorf("cliproxy auth: configuration is required")
}
if ctx == nil {
ctx = context.Background()
}
if opts == nil {
opts = &LoginOptions{}
}
pkceCodes, err := codex.GeneratePKCECodes()
if err != nil {
return nil, fmt.Errorf("codex pkce generation failed: %w", err)
}
state, err := misc.GenerateRandomState()
if err != nil {
return nil, fmt.Errorf("codex state generation failed: %w", err)
}
oauthServer := codex.NewOAuthServer(a.CallbackPort)
if err = oauthServer.Start(); err != nil {
if strings.Contains(err.Error(), "already in use") {
return nil, codex.NewAuthenticationError(codex.ErrPortInUse, err)
}
return nil, codex.NewAuthenticationError(codex.ErrServerStartFailed, err)
}
defer func() {
stopCtx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
if stopErr := oauthServer.Stop(stopCtx); stopErr != nil {
log.Warnf("codex oauth server stop error: %v", stopErr)
}
}()
authSvc := codex.NewCodexAuth(cfg)
authURL, err := authSvc.GenerateAuthURL(state, pkceCodes)
if err != nil {
return nil, fmt.Errorf("codex authorization url generation failed: %w", err)
}
if !opts.NoBrowser {
log.Info("Opening browser for Codex authentication")
if !browser.IsAvailable() {
log.Warn("No browser available; please open the URL manually")
util.PrintSSHTunnelInstructions(a.CallbackPort)
log.Infof("Visit the following URL to continue authentication:\n%s", authURL)
} else if err = browser.OpenURL(authURL); err != nil {
log.Warnf("Failed to open browser automatically: %v", err)
util.PrintSSHTunnelInstructions(a.CallbackPort)
log.Infof("Visit the following URL to continue authentication:\n%s", authURL)
}
} else {
util.PrintSSHTunnelInstructions(a.CallbackPort)
log.Infof("Visit the following URL to continue authentication:\n%s", authURL)
}
log.Info("Waiting for Codex authentication callback...")
result, err := oauthServer.WaitForCallback(5 * time.Minute)
if err != nil {
if strings.Contains(err.Error(), "timeout") {
return nil, codex.NewAuthenticationError(codex.ErrCallbackTimeout, err)
}
return nil, err
}
if result.Error != "" {
return nil, codex.NewOAuthError(result.Error, "", http.StatusBadRequest)
}
if result.State != state {
return nil, codex.NewAuthenticationError(codex.ErrInvalidState, fmt.Errorf("state mismatch"))
}
log.Debug("Codex authorization code received; exchanging for tokens")
authBundle, err := authSvc.ExchangeCodeForTokens(ctx, result.Code, pkceCodes)
if err != nil {
return nil, codex.NewAuthenticationError(codex.ErrCodeExchangeFailed, err)
}
tokenStorage := authSvc.CreateTokenStorage(authBundle)
if tokenStorage == nil || tokenStorage.Email == "" {
return nil, fmt.Errorf("codex token storage missing account information")
}
fileName := fmt.Sprintf("codex-%s.json", tokenStorage.Email)
metadata := map[string]string{
"email": tokenStorage.Email,
}
log.Info("Codex authentication successful")
if authBundle.APIKey != "" {
log.Info("Codex API key obtained and stored")
}
return &TokenRecord{
Provider: a.Provider(),
FileName: fileName,
Storage: tokenStorage,
Metadata: metadata,
}, nil
}
func (a *CodexAuthenticator) Refresh(ctx context.Context, cfg *config.Config, record *TokenRecord) (*TokenRecord, error) {
if record == nil || record.Storage == nil {
return nil, fmt.Errorf("cliproxy auth: empty token record for codex refresh")
}
if cfg == nil {
return nil, fmt.Errorf("cliproxy auth: configuration is required")
}
if ctx == nil {
ctx = context.Background()
}
storage, ok := record.Storage.(*codex.CodexTokenStorage)
if !ok {
return nil, fmt.Errorf("cliproxy auth: unexpected token storage type for codex refresh")
}
svc := codex.NewCodexAuth(cfg)
td, err := svc.RefreshTokensWithRetry(ctx, storage.RefreshToken, 3)
if err != nil {
return nil, err
}
svc.UpdateTokenStorage(storage, td)
result := &TokenRecord{
Provider: a.Provider(),
FileName: record.FileName,
Storage: storage,
Metadata: record.Metadata,
}
return result, nil
}

40
sdk/auth/errors.go Normal file
View File

@@ -0,0 +1,40 @@
package auth
import (
"fmt"
"github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces"
)
// ProjectSelectionError indicates that the user must choose a specific project ID.
type ProjectSelectionError struct {
Email string
Projects []interfaces.GCPProjectProjects
}
func (e *ProjectSelectionError) Error() string {
if e == nil {
return "cliproxy auth: project selection required"
}
return fmt.Sprintf("cliproxy auth: project selection required for %s", e.Email)
}
// ProjectsDisplay returns the projects list for caller presentation.
func (e *ProjectSelectionError) ProjectsDisplay() []interfaces.GCPProjectProjects {
if e == nil {
return nil
}
return e.Projects
}
// EmailRequiredError indicates that the calling context must provide an email or alias.
type EmailRequiredError struct {
Prompt string
}
func (e *EmailRequiredError) Error() string {
if e == nil || e.Prompt == "" {
return "cliproxy auth: email is required"
}
return e.Prompt
}

37
sdk/auth/filestore.go Normal file
View File

@@ -0,0 +1,37 @@
package auth
import (
"context"
"fmt"
"path/filepath"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
)
// FileTokenStore persists token records into the configured auth directory using the
// filename suggested by the authenticator. Relative paths are resolved against cfg.AuthDir.
type FileTokenStore struct{}
// NewFileTokenStore creates a token store that saves credentials to disk through the
// TokenStorage implementation embedded in the token record.
func NewFileTokenStore() *FileTokenStore {
return &FileTokenStore{}
}
// Save writes the token storage to the resolved file path.
func (s *FileTokenStore) Save(ctx context.Context, cfg *config.Config, record *TokenRecord) (string, error) {
if record == nil || record.Storage == nil {
return "", fmt.Errorf("cliproxy auth: token record is incomplete")
}
target := record.FileName
if target == "" {
return "", fmt.Errorf("cliproxy auth: missing file name for provider %s", record.Provider)
}
if cfg != nil && !filepath.IsAbs(target) {
target = filepath.Join(cfg.AuthDir, target)
}
if err := record.Storage.SaveTokenToFile(target); err != nil {
return "", err
}
return target, nil
}

36
sdk/auth/gemini-web.go Normal file
View File

@@ -0,0 +1,36 @@
package auth
import (
"context"
"fmt"
"time"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
)
// GeminiWebAuthenticator provides a minimal wrapper so core components can treat
// Gemini Web credentials via the shared Authenticator contract.
type GeminiWebAuthenticator struct{}
func NewGeminiWebAuthenticator() *GeminiWebAuthenticator { return &GeminiWebAuthenticator{} }
func (a *GeminiWebAuthenticator) Provider() string { return "gemini-web" }
func (a *GeminiWebAuthenticator) Login(ctx context.Context, cfg *config.Config, opts *LoginOptions) (*TokenRecord, error) {
_ = ctx
_ = cfg
_ = opts
return nil, fmt.Errorf("gemini-web authenticator does not support scripted login; use CLI --gemini-web-auth")
}
func (a *GeminiWebAuthenticator) Refresh(ctx context.Context, cfg *config.Config, record *TokenRecord) (*TokenRecord, error) {
_ = ctx
_ = cfg
_ = record
return nil, ErrRefreshNotSupported
}
func (a *GeminiWebAuthenticator) RefreshLead() *time.Duration {
d := 9 * time.Minute
return &d
}

72
sdk/auth/gemini.go Normal file
View File

@@ -0,0 +1,72 @@
package auth
import (
"context"
"fmt"
"time"
"github.com/router-for-me/CLIProxyAPI/v6/internal/auth/gemini"
// legacy client removed
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
log "github.com/sirupsen/logrus"
)
// GeminiAuthenticator implements the login flow for Google Gemini CLI accounts.
type GeminiAuthenticator struct{}
// NewGeminiAuthenticator constructs a Gemini authenticator.
func NewGeminiAuthenticator() *GeminiAuthenticator {
return &GeminiAuthenticator{}
}
func (a *GeminiAuthenticator) Provider() string {
return "gemini"
}
func (a *GeminiAuthenticator) RefreshLead() *time.Duration {
return nil
}
func (a *GeminiAuthenticator) Login(ctx context.Context, cfg *config.Config, opts *LoginOptions) (*TokenRecord, error) {
if cfg == nil {
return nil, fmt.Errorf("cliproxy auth: configuration is required")
}
if ctx == nil {
ctx = context.Background()
}
if opts == nil {
opts = &LoginOptions{}
}
var ts gemini.GeminiTokenStorage
if opts.ProjectID != "" {
ts.ProjectID = opts.ProjectID
}
geminiAuth := gemini.NewGeminiAuth()
_, err := geminiAuth.GetAuthenticatedClient(ctx, &ts, cfg, opts.NoBrowser)
if err != nil {
return nil, fmt.Errorf("gemini authentication failed: %w", err)
}
// Skip onboarding here; rely on upstream configuration
fileName := fmt.Sprintf("%s-%s.json", ts.Email, ts.ProjectID)
metadata := map[string]string{
"email": ts.Email,
"project_id": ts.ProjectID,
}
log.Info("Gemini authentication successful")
return &TokenRecord{
Provider: a.Provider(),
FileName: fileName,
Storage: &ts,
Metadata: metadata,
}, nil
}
func (a *GeminiAuthenticator) Refresh(ctx context.Context, cfg *config.Config, record *TokenRecord) (*TokenRecord, error) {
return nil, ErrRefreshNotSupported
}

42
sdk/auth/interfaces.go Normal file
View File

@@ -0,0 +1,42 @@
package auth
import (
"context"
"errors"
"time"
baseauth "github.com/router-for-me/CLIProxyAPI/v6/internal/auth"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
)
var ErrRefreshNotSupported = errors.New("cliproxy auth: refresh not supported")
// LoginOptions captures generic knobs shared across authenticators.
// Provider-specific logic can inspect Metadata for extra parameters.
type LoginOptions struct {
NoBrowser bool
ProjectID string
Metadata map[string]string
Prompt func(prompt string) (string, error)
}
// TokenRecord represents credential material produced by an authenticator.
type TokenRecord struct {
Provider string
FileName string
Storage baseauth.TokenStorage
Metadata map[string]string
}
// TokenStore persists token records.
type TokenStore interface {
Save(ctx context.Context, cfg *config.Config, record *TokenRecord) (string, error)
}
// Authenticator manages login and optional refresh flows for a provider.
type Authenticator interface {
Provider() string
Login(ctx context.Context, cfg *config.Config, opts *LoginOptions) (*TokenRecord, error)
Refresh(ctx context.Context, cfg *config.Config, record *TokenRecord) (*TokenRecord, error)
RefreshLead() *time.Duration
}

95
sdk/auth/manager.go Normal file
View File

@@ -0,0 +1,95 @@
package auth
import (
"context"
"fmt"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
)
// Manager aggregates authenticators and coordinates persistence via a token store.
type Manager struct {
authenticators map[string]Authenticator
store TokenStore
}
// NewManager constructs a manager with the provided token store and authenticators.
// If store is nil, the caller must set it later using SetStore.
func NewManager(store TokenStore, authenticators ...Authenticator) *Manager {
mgr := &Manager{
authenticators: make(map[string]Authenticator),
store: store,
}
for i := range authenticators {
mgr.Register(authenticators[i])
}
return mgr
}
// Register adds or replaces an authenticator keyed by its provider identifier.
func (m *Manager) Register(a Authenticator) {
if a == nil {
return
}
if m.authenticators == nil {
m.authenticators = make(map[string]Authenticator)
}
m.authenticators[a.Provider()] = a
}
// SetStore updates the token store used for persistence.
func (m *Manager) SetStore(store TokenStore) {
m.store = store
}
// Login executes the provider login flow and persists the resulting token record.
func (m *Manager) Login(ctx context.Context, provider string, cfg *config.Config, opts *LoginOptions) (*TokenRecord, string, error) {
auth, ok := m.authenticators[provider]
if !ok {
return nil, "", fmt.Errorf("cliproxy auth: authenticator %s not registered", provider)
}
record, err := auth.Login(ctx, cfg, opts)
if err != nil {
return nil, "", err
}
if record == nil {
return nil, "", fmt.Errorf("cliproxy auth: authenticator %s returned nil record", provider)
}
if m.store == nil {
return record, "", nil
}
savedPath, err := m.store.Save(ctx, cfg, record)
if err != nil {
return record, "", err
}
return record, savedPath, nil
}
// Refresh delegates to the provider-specific refresh implementation and persists the result.
func (m *Manager) Refresh(ctx context.Context, provider string, cfg *config.Config, record *TokenRecord) (*TokenRecord, string, error) {
auth, ok := m.authenticators[provider]
if !ok {
return nil, "", fmt.Errorf("cliproxy auth: authenticator %s not registered", provider)
}
updated, err := auth.Refresh(ctx, cfg, record)
if err != nil {
return nil, "", err
}
if updated == nil {
updated = record
}
if m.store == nil {
return updated, "", nil
}
savedPath, err := m.store.Save(ctx, cfg, updated)
if err != nil {
return updated, "", err
}
return updated, savedPath, nil
}

147
sdk/auth/qwen.go Normal file
View File

@@ -0,0 +1,147 @@
package auth
import (
"context"
"fmt"
"strings"
"time"
"github.com/router-for-me/CLIProxyAPI/v6/internal/auth/qwen"
"github.com/router-for-me/CLIProxyAPI/v6/internal/browser"
// legacy client removed
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
log "github.com/sirupsen/logrus"
)
// QwenAuthenticator implements the device flow login for Qwen accounts.
type QwenAuthenticator struct{}
// NewQwenAuthenticator constructs a Qwen authenticator.
func NewQwenAuthenticator() *QwenAuthenticator {
return &QwenAuthenticator{}
}
func (a *QwenAuthenticator) Provider() string {
return "qwen"
}
func (a *QwenAuthenticator) RefreshLead() *time.Duration {
d := 3 * time.Hour
return &d
}
func (a *QwenAuthenticator) Login(ctx context.Context, cfg *config.Config, opts *LoginOptions) (*TokenRecord, error) {
if cfg == nil {
return nil, fmt.Errorf("cliproxy auth: configuration is required")
}
if ctx == nil {
ctx = context.Background()
}
if opts == nil {
opts = &LoginOptions{}
}
authSvc := qwen.NewQwenAuth(cfg)
deviceFlow, err := authSvc.InitiateDeviceFlow(ctx)
if err != nil {
return nil, fmt.Errorf("qwen device flow initiation failed: %w", err)
}
authURL := deviceFlow.VerificationURIComplete
if !opts.NoBrowser {
log.Info("Opening browser for Qwen authentication")
if !browser.IsAvailable() {
log.Warn("No browser available; please open the URL manually")
log.Infof("Visit the following URL to continue authentication:\n%s", authURL)
} else if err = browser.OpenURL(authURL); err != nil {
log.Warnf("Failed to open browser automatically: %v", err)
log.Infof("Visit the following URL to continue authentication:\n%s", authURL)
}
} else {
log.Infof("Visit the following URL to continue authentication:\n%s", authURL)
}
log.Info("Waiting for Qwen authentication...")
tokenData, err := authSvc.PollForToken(deviceFlow.DeviceCode, deviceFlow.CodeVerifier)
if err != nil {
return nil, fmt.Errorf("qwen authentication failed: %w", err)
}
tokenStorage := authSvc.CreateTokenStorage(tokenData)
email := ""
if opts.Metadata != nil {
email = opts.Metadata["email"]
if email == "" {
email = opts.Metadata["alias"]
}
}
if email == "" && opts.Prompt != nil {
email, err = opts.Prompt("Please input your email address or alias for Qwen:")
if err != nil {
return nil, err
}
}
email = strings.TrimSpace(email)
if email == "" {
return nil, &EmailRequiredError{Prompt: "Please provide an email address or alias for Qwen."}
}
tokenStorage.Email = email
// no legacy client construction
fileName := fmt.Sprintf("qwen-%s.json", tokenStorage.Email)
metadata := map[string]string{
"email": tokenStorage.Email,
}
log.Info("Qwen authentication successful")
return &TokenRecord{
Provider: a.Provider(),
FileName: fileName,
Storage: tokenStorage,
Metadata: metadata,
}, nil
}
func (a *QwenAuthenticator) Refresh(ctx context.Context, cfg *config.Config, record *TokenRecord) (*TokenRecord, error) {
if record == nil || record.Storage == nil {
return nil, fmt.Errorf("cliproxy auth: empty token record for qwen refresh")
}
if cfg == nil {
return nil, fmt.Errorf("cliproxy auth: configuration is required")
}
if ctx == nil {
ctx = context.Background()
}
storage, ok := record.Storage.(*qwen.QwenTokenStorage)
if !ok {
return nil, fmt.Errorf("cliproxy auth: unexpected token storage type for qwen refresh")
}
svc := qwen.NewQwenAuth(cfg)
td, err := svc.RefreshTokens(ctx, storage.RefreshToken)
if err != nil {
return nil, err
}
storage.AccessToken = td.AccessToken
storage.RefreshToken = td.RefreshToken
storage.ResourceURL = td.ResourceURL
storage.Expire = td.Expire
result := &TokenRecord{
Provider: a.Provider(),
FileName: record.FileName,
Storage: storage,
Metadata: record.Metadata,
}
return result, nil
}