Compare commits

...

7 Commits

Author SHA1 Message Date
Supra4E8C
d1f667cf8d feat(registry): add support for 'kimi' channel in model definitions 2026-02-15 15:21:33 +08:00
Luis Pater
46a6782065 refactor(all): replace manual pointer assignments with new to enhance code readability and maintainability 2026-02-15 14:10:10 +08:00
Luis Pater
c359f61859 fix(auth): normalize Gemini credential file prefix for consistency 2026-02-15 13:59:33 +08:00
Luis Pater
908c8eab5b Merge pull request #1543 from sususu98/feat/gemini-cli-google-one
feat(gemini-cli): add Google One login and improve auto-discovery
2026-02-15 13:58:21 +08:00
Luis Pater
f5f2c69233 Merge pull request #1595 from alexey-yanchenko/feature/cache-usage-from-codex-to-chat-completions
Pass cache usage from codex to openai chat completions
2026-02-15 13:56:46 +08:00
Alexey Yanchenko
63d4de5eea Pass cache usage from codex to openai chat completions 2026-02-15 12:04:15 +07:00
sususu98
f3ccd85ba1 feat(gemini-cli): add Google One login and improve auto-discovery
Add Google One personal account login to Gemini CLI OAuth flow:
- CLI --login shows mode menu (Code Assist vs Google One)
- Web management API accepts project_id=GOOGLE_ONE sentinel
- Auto-discover project via onboardUser without cloudaicompanionProject when project is unresolved

Improve robustness of auto-discovery and token handling:
- Add context-aware auto-discovery polling (30s timeout, 2s interval)
- Distinguish network errors from project-selection-required errors
- Refresh expired access tokens in readAuthFile before project lookup
- Extend project_id auto-fill to gemini auth type (was antigravity-only)

Unify credential file naming to geminicli- prefix for both CLI and web.

Add extractAccessToken unit tests (9 cases).
2026-02-11 17:53:03 +08:00
24 changed files with 355 additions and 85 deletions

View File

@@ -19,7 +19,7 @@ jobs:
- run: git fetch --force --tags
- uses: actions/setup-go@v4
with:
go-version: '>=1.24.0'
go-version: '>=1.26.0'
cache: true
- name: Generate Build Metadata
run: |

2
go.mod
View File

@@ -1,6 +1,6 @@
module github.com/router-for-me/CLIProxyAPI/v6
go 1.24.0
go 1.26.0
require (
github.com/andybalholm/brotli v1.0.6

View File

@@ -1188,6 +1188,30 @@ func (h *Handler) RequestGeminiCLIToken(c *gin.Context) {
}
ts.ProjectID = strings.Join(projects, ",")
ts.Checked = true
} else if strings.EqualFold(requestedProjectID, "GOOGLE_ONE") {
ts.Auto = false
if errSetup := performGeminiCLISetup(ctx, gemClient, &ts, ""); errSetup != nil {
log.Errorf("Google One auto-discovery failed: %v", errSetup)
SetOAuthSessionError(state, "Google One auto-discovery failed")
return
}
if strings.TrimSpace(ts.ProjectID) == "" {
log.Error("Google One auto-discovery returned empty project ID")
SetOAuthSessionError(state, "Google One auto-discovery returned empty project ID")
return
}
isChecked, errCheck := checkCloudAPIIsEnabled(ctx, gemClient, ts.ProjectID)
if errCheck != nil {
log.Errorf("Failed to verify Cloud AI API status: %v", errCheck)
SetOAuthSessionError(state, "Failed to verify Cloud AI API status")
return
}
ts.Checked = isChecked
if !isChecked {
log.Error("Cloud AI API is not enabled for the auto-discovered project")
SetOAuthSessionError(state, "Cloud AI API not enabled")
return
}
} else {
if errEnsure := ensureGeminiProjectAndOnboard(ctx, gemClient, &ts, requestedProjectID); errEnsure != nil {
log.Errorf("Failed to complete Gemini CLI onboarding: %v", errEnsure)
@@ -2036,7 +2060,48 @@ func performGeminiCLISetup(ctx context.Context, httpClient *http.Client, storage
}
}
if projectID == "" {
return &projectSelectionRequiredError{}
// Auto-discovery: try onboardUser without specifying a project
// to let Google auto-provision one (matches Gemini CLI headless behavior
// and Antigravity's FetchProjectID pattern).
autoOnboardReq := map[string]any{
"tierId": tierID,
"metadata": metadata,
}
autoCtx, autoCancel := context.WithTimeout(ctx, 30*time.Second)
defer autoCancel()
for attempt := 1; ; attempt++ {
var onboardResp map[string]any
if errOnboard := callGeminiCLI(autoCtx, httpClient, "onboardUser", autoOnboardReq, &onboardResp); errOnboard != nil {
return fmt.Errorf("auto-discovery onboardUser: %w", errOnboard)
}
if done, okDone := onboardResp["done"].(bool); okDone && done {
if resp, okResp := onboardResp["response"].(map[string]any); okResp {
switch v := resp["cloudaicompanionProject"].(type) {
case string:
projectID = strings.TrimSpace(v)
case map[string]any:
if id, okID := v["id"].(string); okID {
projectID = strings.TrimSpace(id)
}
}
}
break
}
log.Debugf("Auto-discovery: onboarding in progress, attempt %d...", attempt)
select {
case <-autoCtx.Done():
return &projectSelectionRequiredError{}
case <-time.After(2 * time.Second):
}
}
if projectID == "" {
return &projectSelectionRequiredError{}
}
log.Infof("Auto-discovered project ID via onboarding: %s", projectID)
}
onboardReqBody := map[string]any{

View File

@@ -28,8 +28,7 @@ func (h *Handler) GetConfig(c *gin.Context) {
c.JSON(200, gin.H{})
return
}
cfgCopy := *h.cfg
c.JSON(200, &cfgCopy)
c.JSON(200, new(*h.cfg))
}
type releaseInfo struct {

View File

@@ -127,8 +127,7 @@ func (m *AmpModule) Register(ctx modules.Context) error {
m.modelMapper = NewModelMapper(settings.ModelMappings)
// Store initial config for partial reload comparison
settingsCopy := settings
m.lastConfig = &settingsCopy
m.lastConfig = new(settings)
// Initialize localhost restriction setting (hot-reloadable)
m.setRestrictToLocalhost(settings.RestrictManagementToLocalhost)

View File

@@ -40,8 +40,7 @@ func DoClaudeLogin(cfg *config.Config, options *LoginOptions) {
_, savedPath, err := manager.Login(context.Background(), "claude", cfg, authOpts)
if err != nil {
var authErr *claude.AuthenticationError
if errors.As(err, &authErr) {
if authErr, ok := errors.AsType[*claude.AuthenticationError](err); ok {
log.Error(claude.GetUserFriendlyMessage(authErr))
if authErr.Type == claude.ErrPortInUse.Type {
os.Exit(claude.ErrPortInUse.Code)

View File

@@ -32,8 +32,7 @@ func DoIFlowLogin(cfg *config.Config, options *LoginOptions) {
_, savedPath, err := manager.Login(context.Background(), "iflow", cfg, authOpts)
if err != nil {
var emailErr *sdkAuth.EmailRequiredError
if errors.As(err, &emailErr) {
if emailErr, ok := errors.AsType[*sdkAuth.EmailRequiredError](err); ok {
log.Error(emailErr.Error())
return
}

View File

@@ -100,49 +100,74 @@ func DoLogin(cfg *config.Config, projectID string, options *LoginOptions) {
log.Info("Authentication successful.")
projects, errProjects := fetchGCPProjects(ctx, httpClient)
if errProjects != nil {
log.Errorf("Failed to get project list: %v", errProjects)
return
var activatedProjects []string
useGoogleOne := false
if trimmedProjectID == "" && promptFn != nil {
fmt.Println("\nSelect login mode:")
fmt.Println(" 1. Code Assist (GCP project, manual selection)")
fmt.Println(" 2. Google One (personal account, auto-discover project)")
choice, errPrompt := promptFn("Enter choice [1/2] (default: 1): ")
if errPrompt == nil && strings.TrimSpace(choice) == "2" {
useGoogleOne = true
}
}
selectedProjectID := promptForProjectSelection(projects, trimmedProjectID, promptFn)
projectSelections, errSelection := resolveProjectSelections(selectedProjectID, projects)
if errSelection != nil {
log.Errorf("Invalid project selection: %v", errSelection)
return
}
if len(projectSelections) == 0 {
log.Error("No project selected; aborting login.")
return
}
activatedProjects := make([]string, 0, len(projectSelections))
seenProjects := make(map[string]bool)
for _, candidateID := range projectSelections {
log.Infof("Activating project %s", candidateID)
if errSetup := performGeminiCLISetup(ctx, httpClient, storage, candidateID); errSetup != nil {
var projectErr *projectSelectionRequiredError
if errors.As(errSetup, &projectErr) {
log.Error("Failed to start user onboarding: A project ID is required.")
showProjectSelectionHelp(storage.Email, projects)
return
}
log.Errorf("Failed to complete user setup: %v", errSetup)
if useGoogleOne {
log.Info("Google One mode: auto-discovering project...")
if errSetup := performGeminiCLISetup(ctx, httpClient, storage, ""); errSetup != nil {
log.Errorf("Google One auto-discovery failed: %v", errSetup)
return
}
finalID := strings.TrimSpace(storage.ProjectID)
if finalID == "" {
finalID = candidateID
autoProject := strings.TrimSpace(storage.ProjectID)
if autoProject == "" {
log.Error("Google One auto-discovery returned empty project ID")
return
}
log.Infof("Auto-discovered project: %s", autoProject)
activatedProjects = []string{autoProject}
} else {
projects, errProjects := fetchGCPProjects(ctx, httpClient)
if errProjects != nil {
log.Errorf("Failed to get project list: %v", errProjects)
return
}
// Skip duplicates
if seenProjects[finalID] {
log.Infof("Project %s already activated, skipping", finalID)
continue
selectedProjectID := promptForProjectSelection(projects, trimmedProjectID, promptFn)
projectSelections, errSelection := resolveProjectSelections(selectedProjectID, projects)
if errSelection != nil {
log.Errorf("Invalid project selection: %v", errSelection)
return
}
if len(projectSelections) == 0 {
log.Error("No project selected; aborting login.")
return
}
seenProjects := make(map[string]bool)
for _, candidateID := range projectSelections {
log.Infof("Activating project %s", candidateID)
if errSetup := performGeminiCLISetup(ctx, httpClient, storage, candidateID); errSetup != nil {
if _, ok := errors.AsType[*projectSelectionRequiredError](errSetup); ok {
log.Error("Failed to start user onboarding: A project ID is required.")
showProjectSelectionHelp(storage.Email, projects)
return
}
log.Errorf("Failed to complete user setup: %v", errSetup)
return
}
finalID := strings.TrimSpace(storage.ProjectID)
if finalID == "" {
finalID = candidateID
}
if seenProjects[finalID] {
log.Infof("Project %s already activated, skipping", finalID)
continue
}
seenProjects[finalID] = true
activatedProjects = append(activatedProjects, finalID)
}
seenProjects[finalID] = true
activatedProjects = append(activatedProjects, finalID)
}
storage.Auto = false
@@ -235,7 +260,48 @@ func performGeminiCLISetup(ctx context.Context, httpClient *http.Client, storage
}
}
if projectID == "" {
return &projectSelectionRequiredError{}
// Auto-discovery: try onboardUser without specifying a project
// to let Google auto-provision one (matches Gemini CLI headless behavior
// and Antigravity's FetchProjectID pattern).
autoOnboardReq := map[string]any{
"tierId": tierID,
"metadata": metadata,
}
autoCtx, autoCancel := context.WithTimeout(ctx, 30*time.Second)
defer autoCancel()
for attempt := 1; ; attempt++ {
var onboardResp map[string]any
if errOnboard := callGeminiCLI(autoCtx, httpClient, "onboardUser", autoOnboardReq, &onboardResp); errOnboard != nil {
return fmt.Errorf("auto-discovery onboardUser: %w", errOnboard)
}
if done, okDone := onboardResp["done"].(bool); okDone && done {
if resp, okResp := onboardResp["response"].(map[string]any); okResp {
switch v := resp["cloudaicompanionProject"].(type) {
case string:
projectID = strings.TrimSpace(v)
case map[string]any:
if id, okID := v["id"].(string); okID {
projectID = strings.TrimSpace(id)
}
}
}
break
}
log.Debugf("Auto-discovery: onboarding in progress, attempt %d...", attempt)
select {
case <-autoCtx.Done():
return &projectSelectionRequiredError{}
case <-time.After(2 * time.Second):
}
}
if projectID == "" {
return &projectSelectionRequiredError{}
}
log.Infof("Auto-discovered project ID via onboarding: %s", projectID)
}
onboardReqBody := map[string]any{
@@ -617,7 +683,7 @@ func updateAuthRecord(record *cliproxyauth.Auth, storage *gemini.GeminiTokenStor
return
}
finalName := gemini.CredentialFileName(storage.Email, storage.ProjectID, false)
finalName := gemini.CredentialFileName(storage.Email, storage.ProjectID, true)
if record.Metadata == nil {
record.Metadata = make(map[string]any)

View File

@@ -54,8 +54,7 @@ func DoCodexLogin(cfg *config.Config, options *LoginOptions) {
_, savedPath, err := manager.Login(context.Background(), "codex", cfg, authOpts)
if err != nil {
var authErr *codex.AuthenticationError
if errors.As(err, &authErr) {
if authErr, ok := errors.AsType[*codex.AuthenticationError](err); ok {
log.Error(codex.GetUserFriendlyMessage(authErr))
if authErr.Type == codex.ErrPortInUse.Type {
os.Exit(codex.ErrPortInUse.Code)

View File

@@ -44,8 +44,7 @@ func DoQwenLogin(cfg *config.Config, options *LoginOptions) {
_, savedPath, err := manager.Login(context.Background(), "qwen", cfg, authOpts)
if err != nil {
var emailErr *sdkAuth.EmailRequiredError
if errors.As(err, &emailErr) {
if emailErr, ok := errors.AsType[*sdkAuth.EmailRequiredError](err); ok {
log.Error(emailErr.Error())
return
}

View File

@@ -19,6 +19,7 @@ import (
// - codex
// - qwen
// - iflow
// - kimi
// - antigravity (returns static overrides only)
func GetStaticModelDefinitionsByChannel(channel string) []*ModelInfo {
key := strings.ToLower(strings.TrimSpace(channel))
@@ -39,6 +40,8 @@ func GetStaticModelDefinitionsByChannel(channel string) []*ModelInfo {
return GetQwenModels()
case "iflow":
return GetIFlowModels()
case "kimi":
return GetKimiModels()
case "antigravity":
cfg := GetAntigravityModelConfig()
if len(cfg) == 0 {
@@ -83,6 +86,7 @@ func LookupStaticModelInfo(modelID string) *ModelInfo {
GetOpenAIModels(),
GetQwenModels(),
GetIFlowModels(),
GetKimiModels(),
}
for _, models := range allModels {
for _, m := range models {

View File

@@ -596,8 +596,7 @@ func (r *ModelRegistry) SetModelQuotaExceeded(clientID, modelID string) {
defer r.mutex.Unlock()
if registration, exists := r.models[modelID]; exists {
now := time.Now()
registration.QuotaExceededClients[clientID] = &now
registration.QuotaExceededClients[clientID] = new(time.Now())
log.Debugf("Marked model %s as quota exceeded for client %s", modelID, clientID)
}
}

View File

@@ -899,8 +899,7 @@ func parseRetryDelay(errorBody []byte) (*time.Duration, error) {
if matches := re.FindStringSubmatch(message); len(matches) > 1 {
seconds, err := strconv.Atoi(matches[1])
if err == nil {
duration := time.Duration(seconds) * time.Second
return &duration, nil
return new(time.Duration(seconds) * time.Second), nil
}
}
}

View File

@@ -90,6 +90,9 @@ func ConvertCodexResponseToOpenAI(_ context.Context, modelName string, originalR
if inputTokensResult := usageResult.Get("input_tokens"); inputTokensResult.Exists() {
template, _ = sjson.Set(template, "usage.prompt_tokens", inputTokensResult.Int())
}
if cachedTokensResult := usageResult.Get("input_tokens_details.cached_tokens"); cachedTokensResult.Exists() {
template, _ = sjson.Set(template, "usage.prompt_tokens_details.cached_tokens", cachedTokensResult.Int())
}
if reasoningTokensResult := usageResult.Get("output_tokens_details.reasoning_tokens"); reasoningTokensResult.Exists() {
template, _ = sjson.Set(template, "usage.completion_tokens_details.reasoning_tokens", reasoningTokensResult.Int())
}
@@ -205,6 +208,9 @@ func ConvertCodexResponseToOpenAINonStream(_ context.Context, _ string, original
if inputTokensResult := usageResult.Get("input_tokens"); inputTokensResult.Exists() {
template, _ = sjson.Set(template, "usage.prompt_tokens", inputTokensResult.Int())
}
if cachedTokensResult := usageResult.Get("input_tokens_details.cached_tokens"); cachedTokensResult.Exists() {
template, _ = sjson.Set(template, "usage.prompt_tokens_details.cached_tokens", cachedTokensResult.Int())
}
if reasoningTokensResult := usageResult.Get("output_tokens_details.reasoning_tokens"); reasoningTokensResult.Exists() {
template, _ = sjson.Set(template, "usage.completion_tokens_details.reasoning_tokens", reasoningTokensResult.Int())
}

View File

@@ -185,8 +185,7 @@ func (h *GeminiCLIAPIHandler) handleInternalGenerateContent(c *gin.Context, rawJ
func (h *GeminiCLIAPIHandler) forwardCLIStream(c *gin.Context, flusher http.Flusher, alt string, cancel func(error), data <-chan []byte, errs <-chan *interfaces.ErrorMessage) {
var keepAliveInterval *time.Duration
if alt != "" {
disabled := time.Duration(0)
keepAliveInterval = &disabled
keepAliveInterval = new(time.Duration(0))
}
h.ForwardStream(c, flusher, cancel, data, errs, handlers.StreamForwardOptions{

View File

@@ -300,8 +300,7 @@ func (h *GeminiAPIHandler) handleGenerateContent(c *gin.Context, modelName strin
func (h *GeminiAPIHandler) forwardGeminiStream(c *gin.Context, flusher http.Flusher, alt string, cancel func(error), data <-chan []byte, errs <-chan *interfaces.ErrorMessage) {
var keepAliveInterval *time.Duration
if alt != "" {
disabled := time.Duration(0)
keepAliveInterval = &disabled
keepAliveInterval = new(time.Duration(0))
}
h.ForwardStream(c, flusher, cancel, data, errs, handlers.StreamForwardOptions{

View File

@@ -28,8 +28,7 @@ func (AntigravityAuthenticator) Provider() string { return "antigravity" }
// RefreshLead instructs the manager to refresh five minutes before expiry.
func (AntigravityAuthenticator) RefreshLead() *time.Duration {
lead := 5 * time.Minute
return &lead
return new(5 * time.Minute)
}
// Login launches a local OAuth flow to obtain antigravity tokens and persists them.

View File

@@ -32,8 +32,7 @@ func (a *ClaudeAuthenticator) Provider() string {
}
func (a *ClaudeAuthenticator) RefreshLead() *time.Duration {
d := 4 * time.Hour
return &d
return new(4 * time.Hour)
}
func (a *ClaudeAuthenticator) Login(ctx context.Context, cfg *config.Config, opts *LoginOptions) (*coreauth.Auth, error) {

View File

@@ -34,8 +34,7 @@ func (a *CodexAuthenticator) Provider() string {
}
func (a *CodexAuthenticator) RefreshLead() *time.Duration {
d := 5 * 24 * time.Hour
return &d
return new(5 * 24 * time.Hour)
}
func (a *CodexAuthenticator) Login(ctx context.Context, cfg *config.Config, opts *LoginOptions) (*coreauth.Auth, error) {

View File

@@ -4,8 +4,10 @@ import (
"context"
"encoding/json"
"fmt"
"io"
"io/fs"
"net/http"
"net/url"
"os"
"path/filepath"
"strings"
@@ -186,15 +188,21 @@ func (s *FileTokenStore) readAuthFile(path, baseDir string) (*cliproxyauth.Auth,
if provider == "" {
provider = "unknown"
}
if provider == "antigravity" {
if provider == "antigravity" || provider == "gemini" {
projectID := ""
if pid, ok := metadata["project_id"].(string); ok {
projectID = strings.TrimSpace(pid)
}
if projectID == "" {
accessToken := ""
if token, ok := metadata["access_token"].(string); ok {
accessToken = strings.TrimSpace(token)
accessToken := extractAccessToken(metadata)
// For gemini type, the stored access_token is likely expired (~1h lifetime).
// Refresh it using the long-lived refresh_token before querying.
if provider == "gemini" {
if tokenMap, ok := metadata["token"].(map[string]any); ok {
if refreshed, errRefresh := refreshGeminiAccessToken(tokenMap, http.DefaultClient); errRefresh == nil {
accessToken = refreshed
}
}
}
if accessToken != "" {
fetchedProjectID, errFetch := FetchAntigravityProjectID(context.Background(), accessToken, http.DefaultClient)
@@ -304,6 +312,67 @@ func (s *FileTokenStore) baseDirSnapshot() string {
return s.baseDir
}
func extractAccessToken(metadata map[string]any) string {
if at, ok := metadata["access_token"].(string); ok {
if v := strings.TrimSpace(at); v != "" {
return v
}
}
if tokenMap, ok := metadata["token"].(map[string]any); ok {
if at, ok := tokenMap["access_token"].(string); ok {
if v := strings.TrimSpace(at); v != "" {
return v
}
}
}
return ""
}
func refreshGeminiAccessToken(tokenMap map[string]any, httpClient *http.Client) (string, error) {
refreshToken, _ := tokenMap["refresh_token"].(string)
clientID, _ := tokenMap["client_id"].(string)
clientSecret, _ := tokenMap["client_secret"].(string)
tokenURI, _ := tokenMap["token_uri"].(string)
if refreshToken == "" || clientID == "" || clientSecret == "" {
return "", fmt.Errorf("missing refresh credentials")
}
if tokenURI == "" {
tokenURI = "https://oauth2.googleapis.com/token"
}
data := url.Values{
"grant_type": {"refresh_token"},
"refresh_token": {refreshToken},
"client_id": {clientID},
"client_secret": {clientSecret},
}
resp, err := httpClient.PostForm(tokenURI, data)
if err != nil {
return "", fmt.Errorf("refresh request: %w", err)
}
defer func() { _ = resp.Body.Close() }()
body, _ := io.ReadAll(resp.Body)
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("refresh failed: status %d", resp.StatusCode)
}
var result map[string]any
if errUnmarshal := json.Unmarshal(body, &result); errUnmarshal != nil {
return "", fmt.Errorf("decode refresh response: %w", errUnmarshal)
}
newAccessToken, _ := result["access_token"].(string)
if newAccessToken == "" {
return "", fmt.Errorf("no access_token in refresh response")
}
tokenMap["access_token"] = newAccessToken
return newAccessToken, nil
}
// jsonEqual compares two JSON blobs by parsing them into Go objects and deep comparing.
func jsonEqual(a, b []byte) bool {
var objA any

View File

@@ -0,0 +1,80 @@
package auth
import "testing"
func TestExtractAccessToken(t *testing.T) {
t.Parallel()
tests := []struct {
name string
metadata map[string]any
expected string
}{
{
"antigravity top-level access_token",
map[string]any{"access_token": "tok-abc"},
"tok-abc",
},
{
"gemini nested token.access_token",
map[string]any{
"token": map[string]any{"access_token": "tok-nested"},
},
"tok-nested",
},
{
"top-level takes precedence over nested",
map[string]any{
"access_token": "tok-top",
"token": map[string]any{"access_token": "tok-nested"},
},
"tok-top",
},
{
"empty metadata",
map[string]any{},
"",
},
{
"whitespace-only access_token",
map[string]any{"access_token": " "},
"",
},
{
"wrong type access_token",
map[string]any{"access_token": 12345},
"",
},
{
"token is not a map",
map[string]any{"token": "not-a-map"},
"",
},
{
"nested whitespace-only",
map[string]any{
"token": map[string]any{"access_token": " "},
},
"",
},
{
"fallback to nested when top-level empty",
map[string]any{
"access_token": "",
"token": map[string]any{"access_token": "tok-fallback"},
},
"tok-fallback",
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
got := extractAccessToken(tt.metadata)
if got != tt.expected {
t.Errorf("extractAccessToken() = %q, want %q", got, tt.expected)
}
})
}
}

View File

@@ -26,8 +26,7 @@ func (a *IFlowAuthenticator) Provider() string { return "iflow" }
// RefreshLead indicates how soon before expiry a refresh should be attempted.
func (a *IFlowAuthenticator) RefreshLead() *time.Duration {
d := 24 * time.Hour
return &d
return new(24 * time.Hour)
}
// Login performs the OAuth code flow using a local callback server.

View File

@@ -27,8 +27,7 @@ func (a *QwenAuthenticator) Provider() string {
}
func (a *QwenAuthenticator) RefreshLead() *time.Duration {
d := 3 * time.Hour
return &d
return new(3 * time.Hour)
}
func (a *QwenAuthenticator) Login(ctx context.Context, cfg *config.Config, opts *LoginOptions) (*coreauth.Auth, error) {

View File

@@ -599,8 +599,7 @@ func (m *Manager) executeMixedOnce(ctx context.Context, providers []string, req
return cliproxyexecutor.Response{}, errCtx
}
result.Error = &Error{Message: errExec.Error()}
var se cliproxyexecutor.StatusError
if errors.As(errExec, &se) && se != nil {
if se, ok := errors.AsType[cliproxyexecutor.StatusError](errExec); ok && se != nil {
result.Error.HTTPStatus = se.StatusCode()
}
if ra := retryAfterFromError(errExec); ra != nil {
@@ -655,8 +654,7 @@ func (m *Manager) executeCountMixedOnce(ctx context.Context, providers []string,
return cliproxyexecutor.Response{}, errCtx
}
result.Error = &Error{Message: errExec.Error()}
var se cliproxyexecutor.StatusError
if errors.As(errExec, &se) && se != nil {
if se, ok := errors.AsType[cliproxyexecutor.StatusError](errExec); ok && se != nil {
result.Error.HTTPStatus = se.StatusCode()
}
if ra := retryAfterFromError(errExec); ra != nil {
@@ -710,8 +708,7 @@ func (m *Manager) executeStreamMixedOnce(ctx context.Context, providers []string
return nil, errCtx
}
rerr := &Error{Message: errStream.Error()}
var se cliproxyexecutor.StatusError
if errors.As(errStream, &se) && se != nil {
if se, ok := errors.AsType[cliproxyexecutor.StatusError](errStream); ok && se != nil {
rerr.HTTPStatus = se.StatusCode()
}
result := Result{AuthID: auth.ID, Provider: provider, Model: routeModel, Success: false, Error: rerr}
@@ -732,8 +729,7 @@ func (m *Manager) executeStreamMixedOnce(ctx context.Context, providers []string
if chunk.Err != nil && !failed {
failed = true
rerr := &Error{Message: chunk.Err.Error()}
var se cliproxyexecutor.StatusError
if errors.As(chunk.Err, &se) && se != nil {
if se, ok := errors.AsType[cliproxyexecutor.StatusError](chunk.Err); ok && se != nil {
rerr.HTTPStatus = se.StatusCode()
}
m.MarkResult(streamCtx, Result{AuthID: streamAuth.ID, Provider: streamProvider, Model: routeModel, Success: false, Error: rerr})
@@ -1431,8 +1427,7 @@ func retryAfterFromError(err error) *time.Duration {
if retryAfter == nil {
return nil
}
val := *retryAfter
return &val
return new(*retryAfter)
}
func statusCodeFromResult(err *Error) int {