Gemini-web
Unified · Split
| @@ -714,6 +714,8 @@ func (h *Handler) CreateGeminiWebToken(c *gin.Context) { · 713 unmodified lines | |||
| 714 | Secure1PSID: payload.Secure1PSID, | 714 | Secure1PSID: payload.Secure1PSID, |
| 715 | Secure1PSIDTS: payload.Secure1PSIDTS, | 715 | Secure1PSIDTS: payload.Secure1PSIDTS, |
| 716 | } | 716 | } |
| 717 | // Provide a stable label (gemini-web-<hash>) for logging and identification | ||
| 718 | tokenStorage.Label = strings.TrimSuffix(fileName, ".json") | ||
| 717 | 719 | ||
| 718 | record := &sdkAuth.TokenRecord{ | 720 | record := &sdkAuth.TokenRecord{ |
| 719 | Provider: "gemini-web", | 721 | Provider: "gemini-web", |
| @@ -8,6 +8,7 @@ import ( · 7 unmodified lines | |||
| 8 | "fmt" | 8 | "fmt" |
| 9 | "os" | 9 | "os" |
| 10 | "path/filepath" | 10 | "path/filepath" |
| 11 | "strings" | ||
| 11 | "time" | 12 | "time" |
| 12 | 13 | ||
| 13 | "github.com/router-for-me/CLIProxyAPI/v6/internal/misc" | 14 | "github.com/router-for-me/CLIProxyAPI/v6/internal/misc" |
| @@ -20,12 +21,25 @@ type GeminiWebTokenStorage struct { · 6 unmodified lines | |||
| 20 | Secure1PSIDTS string `json:"secure_1psidts"` | 21 | Secure1PSIDTS string `json:"secure_1psidts"` |
| 21 | Type string `json:"type"` | 22 | Type string `json:"type"` |
| 22 | LastRefresh string `json:"last_refresh,omitempty"` | 23 | LastRefresh string `json:"last_refresh,omitempty"` |
| 24 | // Label is a stable account identifier used for logging, e.g. "gemini-web-<hash>". | ||
| 25 | // It is derived from the auth file name when not explicitly set. | ||
| 26 | Label string `json:"label,omitempty"` | ||
| 23 | } | 27 | } |
| 24 | 28 | ||
| 25 | // SaveTokenToFile serializes the Gemini Web token storage to a JSON file. | 29 | // SaveTokenToFile serializes the Gemini Web token storage to a JSON file. |
| 26 | func (ts *GeminiWebTokenStorage) SaveTokenToFile(authFilePath string) error { | 30 | func (ts *GeminiWebTokenStorage) SaveTokenToFile(authFilePath string) error { |
| 27 | misc.LogSavingCredentials(authFilePath) | 31 | misc.LogSavingCredentials(authFilePath) |
| 28 | ts.Type = "gemini-web" | 32 | ts.Type = "gemini-web" |
| 33 | // Auto-derive a stable label from the file name if missing. | ||
| 34 | if ts.Label == "" { | ||
| 35 | base := filepath.Base(authFilePath) | ||
| 36 | if strings.HasSuffix(strings.ToLower(base), ".json") { | ||
| 37 | base = strings.TrimSuffix(base, filepath.Ext(base)) | ||
| 38 | } | ||
| 39 | if base != "" { | ||
| 40 | ts.Label = base | ||
| 41 | } | ||
| 42 | } | ||
| 29 | if ts.LastRefresh == "" { | 43 | if ts.LastRefresh == "" { |
| 30 | ts.LastRefresh = time.Now().Format(time.RFC3339) | 44 | ts.LastRefresh = time.Now().Format(time.RFC3339) |
| 31 | } | 45 | } |
| @@ -49,6 +49,10 @@ func DoGeminiWebAuth(cfg *config.Config) { · 48 unmodified lines | |||
| 49 | hasher.Write([]byte(secure1psid)) | 49 | hasher.Write([]byte(secure1psid)) |
| 50 | hash := hex.EncodeToString(hasher.Sum(nil)) | 50 | hash := hex.EncodeToString(hasher.Sum(nil)) |
| 51 | fileName := fmt.Sprintf("gemini-web-%s.json", hash[:16]) | 51 | fileName := fmt.Sprintf("gemini-web-%s.json", hash[:16]) |
| 52 | // Set a stable label for logging, e.g. gemini-web-<hash> | ||
| 53 | if tokenStorage != nil { | ||
| 54 | tokenStorage.Label = strings.TrimSuffix(fileName, ".json") | ||
| 55 | } | ||
| 52 | record := &sdkAuth.TokenRecord{ | 56 | record := &sdkAuth.TokenRecord{ |
| 53 | Provider: "gemini-web", | 57 | Provider: "gemini-web", |
| 54 | FileName: fileName, | 58 | FileName: fileName, |
| @@ -97,8 +97,12 @@ func getAccessToken(baseCookies map[string]string, proxy string, verbose bool, i · 96 unmodified lines | |||
| 97 | { | 97 | { |
| 98 | client := newHTTPClient(httpOptions{ProxyURL: proxy, Insecure: insecure, FollowRedirects: true}) | 98 | client := newHTTPClient(httpOptions{ProxyURL: proxy, Insecure: insecure, FollowRedirects: true}) |
| 99 | req, _ := http.NewRequest(http.MethodGet, EndpointGoogle, nil) | 99 | req, _ := http.NewRequest(http.MethodGet, EndpointGoogle, nil) |
| 100 | resp, _ := client.Do(req) | 100 | resp, err := client.Do(req) |
| 101 | if resp != nil { | 101 | if err != nil { |
| 102 | if verbose { | ||
| 103 | log.Debugf("priming google cookies failed: %v", err) | ||
| 104 | } | ||
| 105 | } else if resp != nil { | ||
| 102 | if u, err := url.Parse(EndpointGoogle); err == nil { | 106 | if u, err := url.Parse(EndpointGoogle); err == nil { |
| 103 | for _, c := range client.Jar.Cookies(u) { | 107 | for _, c := range client.Jar.Cookies(u) { |
| 104 | extraCookies[c.Name] = c.Value | 108 | extraCookies[c.Name] = c.Value |
| @@ -172,18 +176,10 @@ func rotate1PSIDTS(cookies map[string]string, proxy string, insecure bool) (stri · 67 unmodified lines | |||
| 172 | return "", &AuthError{Msg: "__Secure-1PSID missing"} | 176 | return "", &AuthError{Msg: "__Secure-1PSID missing"} |
| 173 | } | 177 | } |
| 174 | 178 | ||
| 175 | tr := &http.Transport{} | 179 | // Reuse shared HTTP client helper for consistency. |
| 176 | if proxy != "" { | 180 | client := newHTTPClient(httpOptions{ProxyURL: proxy, Insecure: insecure, FollowRedirects: true}) |
| 177 | if pu, err := url.Parse(proxy); err == nil { | ||
| 178 | tr.Proxy = http.ProxyURL(pu) | ||
| 179 | } | ||
| 180 | } | ||
| 181 | if insecure { | ||
| 182 | tr.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} | ||
| 183 | } | ||
| 184 | client := &http.Client{Transport: tr, Timeout: 60 * time.Second} | ||
| 185 | 181 | ||
| 186 | req, _ := http.NewRequest(http.MethodPost, EndpointRotateCookies, io.NopCloser(stringsReader("[000,\"-0000000000000000000\"]"))) | 182 | req, _ := http.NewRequest(http.MethodPost, EndpointRotateCookies, strings.NewReader("[000,\"-0000000000000000000\"]")) |
| 187 | applyHeaders(req, HeadersRotateCookies) | 183 | applyHeaders(req, HeadersRotateCookies) |
| 188 | applyCookies(req, cookies) | 184 | applyCookies(req, cookies) |
| 189 | 185 | ||
| @@ -207,25 +203,18 @@ func rotate1PSIDTS(cookies map[string]string, proxy string, insecure bool) (stri · 17 unmodified lines | |||
| 207 | return c.Value, nil | 203 | return c.Value, nil |
| 208 | } | 204 | } |
| 209 | } | 205 | } |
| 210 | return "", nil | 206 | // Fallback: check cookie jar in case the Set-Cookie was on a redirect hop |
| 211 | } | 207 | if u, err := url.Parse(EndpointRotateCookies); err == nil && client.Jar != nil { |
| 212 | 208 | for _, c := range client.Jar.Cookies(u) { | |
| 213 | type constReader struct { | 209 | if c.Name == "__Secure-1PSIDTS" && c.Value != "" { |
| 214 | s string | 210 | return c.Value, nil |
| 215 | i int | 211 | } |
| 216 | } | 212 | } |
| 217 | |||
| 218 | func (r *constReader) Read(p []byte) (int, error) { | ||
| 219 | if r.i >= len(r.s) { | ||
| 220 | return 0, io.EOF | ||
| 221 | } | 213 | } |
| 222 | n := copy(p, r.s[r.i:]) | 214 | return "", nil |
| 223 | r.i += n | ||
| 224 | return n, nil | ||
| 225 | } | 215 | } |
| 226 | 216 | ||
| 227 | func stringsReader(s string) io.Reader { return &constReader{s: s} } | 217 | // MaskToken28 masks a sensitive token for safe logging. Keep middle partially visible. |
| 228 | |||
| 229 | func MaskToken28(s string) string { | 218 | func MaskToken28(s string) string { |
| 230 | n := len(s) | 219 | n := len(s) |
| 231 | if n == 0 { | 220 | if n == 0 { |
| @@ -431,21 +420,10 @@ func (c *GeminiClient) generateOnce(prompt string, files []string, model Model, · 199 unmodified lines | |||
| 431 | form.Set("f.req", string(outerJSON)) | 420 | form.Set("f.req", string(outerJSON)) |
| 432 | 421 | ||
| 433 | req, _ := http.NewRequest(http.MethodPost, EndpointGenerate, strings.NewReader(form.Encode())) | 422 | req, _ := http.NewRequest(http.MethodPost, EndpointGenerate, strings.NewReader(form.Encode())) |
| 434 | // headers | 423 | applyHeaders(req, HeadersGemini) |
| 435 | for k, v := range HeadersGemini { | 424 | applyHeaders(req, model.ModelHeader) |
| 436 | for _, vv := range v { | ||
| 437 | req.Header.Add(k, vv) | ||
| 438 | } | ||
| 439 | } | ||
| 440 | for k, v := range model.ModelHeader { | ||
| 441 | for _, vv := range v { | ||
| 442 | req.Header.Add(k, vv) | ||
| 443 | } | ||
| 444 | } | ||
| 445 | req.Header.Set("Content-Type", "application/x-www-form-urlencoded;charset=utf-8") | 425 | req.Header.Set("Content-Type", "application/x-www-form-urlencoded;charset=utf-8") |
| 446 | for k, v := range c.Cookies { | 426 | applyCookies(req, c.Cookies) |
| 447 | req.AddCookie(&http.Cookie{Name: k, Value: v}) | ||
| 448 | } | ||
| 449 | 427 | ||
| 450 | resp, err := c.httpClient.Do(req) | 428 | resp, err := c.httpClient.Do(req) |
| 451 | if err != nil { | 429 | if err != nil { |
| @@ -2,7 +2,6 @@ package geminiwebapi · 1 unmodified line | |||
| 2 | 2 | ||
| 3 | import ( | 3 | import ( |
| 4 | "bytes" | 4 | "bytes" |
| 5 | "crypto/tls" | ||
| 6 | "encoding/base64" | 5 | "encoding/base64" |
| 7 | "encoding/json" | 6 | "encoding/json" |
| 8 | "errors" | 7 | "errors" |
| @@ -11,8 +10,6 @@ import ( · 2 unmodified lines | |||
| 11 | "math" | 10 | "math" |
| 12 | "mime/multipart" | 11 | "mime/multipart" |
| 13 | "net/http" | 12 | "net/http" |
| 14 | "net/http/cookiejar" | ||
| 15 | "net/url" | ||
| 16 | "os" | 13 | "os" |
| 17 | "path/filepath" | 14 | "path/filepath" |
| 18 | "regexp" | 15 | "regexp" |
| @@ -69,18 +66,9 @@ func (i Image) Save(path string, filename string, cookies map[string]string, ver · 50 unmodified lines | |||
| 69 | } | 66 | } |
| 70 | } | 67 | } |
| 71 | } | 68 | } |
| 72 | // Build client with cookie jar so cookies persist across redirects. | 69 | // Build client using shared helper to keep proxy/TLS behavior consistent. |
| 73 | tr := &http.Transport{} | 70 | client := newHTTPClient(httpOptions{ProxyURL: i.Proxy, Insecure: insecure, FollowRedirects: true}) |
| 74 | if i.Proxy != "" { | 71 | client.Timeout = 120 * time.Second |
| 75 | if pu, err := url.Parse(i.Proxy); err == nil { | ||
| 76 | tr.Proxy = http.ProxyURL(pu) | ||
| 77 | } | ||
| 78 | } | ||
| 79 | if insecure { | ||
| 80 | tr.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} | ||
| 81 | } | ||
| 82 | jar, _ := cookiejar.New(nil) | ||
| 83 | client := &http.Client{Transport: tr, Timeout: 120 * time.Second, Jar: jar} | ||
| 84 | 72 | ||
| 85 | // Helper to set raw Cookie header using provided cookies (to mirror Python client behavior). | 73 | // Helper to set raw Cookie header using provided cookies (to mirror Python client behavior). |
| 86 | buildCookieHeader := func(m map[string]string) string { | 74 | buildCookieHeader := func(m map[string]string) string { |
| @@ -352,23 +340,11 @@ func uploadFile(path string, proxy string, insecure bool) (string, error) { · 265 unmodified lines | |||
| 352 | } | 340 | } |
| 353 | _ = mw.Close() | 341 | _ = mw.Close() |
| 354 | 342 | ||
| 355 | tr := &http.Transport{} | 343 | client := newHTTPClient(httpOptions{ProxyURL: proxy, Insecure: insecure, FollowRedirects: true}) |
| 356 | if proxy != "" { | 344 | client.Timeout = 300 * time.Second |
| 357 | if pu, errParse := url.Parse(proxy); errParse == nil { | ||
| 358 | tr.Proxy = http.ProxyURL(pu) | ||
| 359 | } | ||
| 360 | } | ||
| 361 | if insecure { | ||
| 362 | tr.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} | ||
| 363 | } | ||
| 364 | client := &http.Client{Transport: tr, Timeout: 300 * time.Second} | ||
| 365 | 345 | ||
| 366 | req, _ := http.NewRequest(http.MethodPost, EndpointUpload, &buf) | 346 | req, _ := http.NewRequest(http.MethodPost, EndpointUpload, &buf) |
| 367 | for k, v := range HeadersUpload { | 347 | applyHeaders(req, HeadersUpload) |
| 368 | for _, vv := range v { | ||
| 369 | req.Header.Add(k, vv) | ||
| 370 | } | ||
| 371 | } | ||
| 372 | req.Header.Set("Content-Type", mw.FormDataContentType()) | 348 | req.Header.Set("Content-Type", mw.FormDataContentType()) |
| 373 | req.Header.Set("Accept", "*/*") | 349 | req.Header.Set("Accept", "*/*") |
| 374 | req.Header.Set("Connection", "keep-alive") | 350 | req.Header.Set("Connection", "keep-alive") |
| @@ -21,6 +21,7 @@ import ( · 20 unmodified lines | |||
| 21 | "github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces" | 21 | "github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces" |
| 22 | "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/translator" | 22 | "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/translator" |
| 23 | cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor" | 23 | cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor" |
| 24 | log "github.com/sirupsen/logrus" | ||
| 24 | "github.com/tidwall/gjson" | 25 | "github.com/tidwall/gjson" |
| 25 | "github.com/tidwall/sjson" | 26 | "github.com/tidwall/sjson" |
| 26 | bolt "go.etcd.io/bbolt" | 27 | bolt "go.etcd.io/bbolt" |
| @@ -97,12 +98,12 @@ func (s *GeminiWebState) Label() string { · 70 unmodified lines | |||
| 97 | } | 98 | } |
| 98 | 99 | ||
| 99 | func (s *GeminiWebState) loadConversationCaches() { | 100 | func (s *GeminiWebState) loadConversationCaches() { |
| 100 | if path := s.convStorePath(); path != "" { | 101 | if path := s.convPath(); path != "" { |
| 101 | if store, err := LoadConvStore(path); err == nil { | 102 | if store, err := LoadConvStore(path); err == nil { |
| 102 | s.convStore = store | 103 | s.convStore = store |
| 103 | } | 104 | } |
| 104 | } | 105 | } |
| 105 | if path := s.convDataPath(); path != "" { | 106 | if path := s.convPath(); path != "" { |
| 106 | if items, index, err := LoadConvData(path); err == nil { | 107 | if items, index, err := LoadConvData(path); err == nil { |
| 107 | s.convData = items | 108 | s.convData = items |
| 108 | s.convIndex = index | 109 | s.convIndex = index |
| @@ -110,20 +111,14 @@ func (s *GeminiWebState) loadConversationCaches() { · 1 unmodified line | |||
| 110 | } | 111 | } |
| 111 | } | 112 | } |
| 112 | 113 | ||
| 113 | func (s *GeminiWebState) convStorePath() string { | 114 | // convPath returns the BoltDB file path used for both account metadata and conversation data. |
| 115 | func (s *GeminiWebState) convPath() string { | ||
| 114 | base := s.storagePath | 116 | base := s.storagePath |
| 115 | if base == "" { | 117 | if base == "" { |
| 116 | base = s.accountID + ".json" | 118 | // Use accountID directly as base name; ConvBoltPath will append .bolt. |
| 119 | base = s.accountID | ||
| 117 | } | 120 | } |
| 118 | return ConvStorePath(base) | 121 | return ConvBoltPath(base) |
| 119 | } | ||
| 120 | |||
| 121 | func (s *GeminiWebState) convDataPath() string { | ||
| 122 | base := s.storagePath | ||
| 123 | if base == "" { | ||
| 124 | base = s.accountID + ".json" | ||
| 125 | } | ||
| 126 | return ConvDataPath(base) | ||
| 127 | } | 122 | } |
| 128 | 123 | ||
| 129 | func (s *GeminiWebState) GetRequestMutex() *sync.Mutex { return &s.reqMu } | 124 | func (s *GeminiWebState) GetRequestMutex() *sync.Mutex { return &s.reqMu } |
| @@ -174,6 +169,8 @@ func (s *GeminiWebState) Refresh(ctx context.Context) error { · 44 unmodified lines | |||
| 174 | s.client.Cookies["__Secure-1PSIDTS"] = newTS | 169 | s.client.Cookies["__Secure-1PSIDTS"] = newTS |
| 175 | } | 170 | } |
| 176 | s.tokenMu.Unlock() | 171 | s.tokenMu.Unlock() |
| 172 | // Detailed debug log: provider and account. | ||
| 173 | log.Debugf("gemini web account %s rotated 1PSIDTS: %s", s.accountID, MaskToken28(newTS)) | ||
| 177 | } | 174 | } |
| 178 | s.lastRefresh = time.Now() | 175 | s.lastRefresh = time.Now() |
| 179 | return nil | 176 | return nil |
| @@ -405,7 +402,7 @@ func (s *GeminiWebState) persistConversation(modelName string, prep *geminiWebPr · 225 unmodified lines | |||
| 405 | storeSnapshot[k] = cp | 402 | storeSnapshot[k] = cp |
| 406 | } | 403 | } |
| 407 | s.convMu.Unlock() | 404 | s.convMu.Unlock() |
| 408 | _ = SaveConvStore(s.convStorePath(), storeSnapshot) | 405 | _ = SaveConvStore(s.convPath(), storeSnapshot) |
| 409 | } | 406 | } |
| 410 | 407 | ||
| 411 | if !s.useReusableContext() { | 408 | if !s.useReusableContext() { |
| @@ -433,7 +430,7 @@ func (s *GeminiWebState) persistConversation(modelName string, prep *geminiWebPr · 21 unmodified lines | |||
| 433 | indexSnapshot[k] = v | 430 | indexSnapshot[k] = v |
| 434 | } | 431 | } |
| 435 | s.convMu.Unlock() | 432 | s.convMu.Unlock() |
| 436 | _ = SaveConvData(s.convDataPath(), dataSnapshot, indexSnapshot) | 433 | _ = SaveConvData(s.convPath(), dataSnapshot, indexSnapshot) |
| 437 | } | 434 | } |
| 438 | 435 | ||
| 439 | func (s *GeminiWebState) addAPIResponseData(ctx context.Context, line []byte) { | 436 | func (s *GeminiWebState) addAPIResponseData(ctx context.Context, line []byte) { |
| @@ -570,19 +567,9 @@ func HashConversation(clientID, model string, msgs []StoredMessage) string { · 130 unmodified lines | |||
| 570 | return Sha256Hex(b.String()) | 567 | return Sha256Hex(b.String()) |
| 571 | } | 568 | } |
| 572 | 569 | ||
| 573 | // ConvStorePath returns the path for account-level metadata persistence based on token file path. | 570 | // ConvBoltPath returns the BoltDB file path used for both account metadata and conversation data. |
| 574 | func ConvStorePath(tokenFilePath string) string { | 571 | // Different logical datasets are kept in separate buckets within this single DB file. |
| 575 | wd, err := os.Getwd() | 572 | func ConvBoltPath(tokenFilePath string) string { |
| 576 | if err != nil || wd == "" { | ||
| 577 | wd = "." | ||
| 578 | } | ||
| 579 | convDir := filepath.Join(wd, "conv") | ||
| 580 | base := strings.TrimSuffix(filepath.Base(tokenFilePath), filepath.Ext(tokenFilePath)) | ||
| 581 | return filepath.Join(convDir, base+".bolt") | ||
| 582 | } | ||
| 583 | |||
| 584 | // ConvDataPath returns the path for full conversation persistence based on token file path. | ||
| 585 | func ConvDataPath(tokenFilePath string) string { | ||
| 586 | wd, err := os.Getwd() | 573 | wd, err := os.Getwd() |
| 587 | if err != nil || wd == "" { | 574 | if err != nil || wd == "" { |
| 588 | wd = "." | 575 | wd = "." |
| @@ -286,7 +286,8 @@ func (m *Manager) executeWithProvider(ctx context.Context, provider string, req · 285 unmodified lines | |||
| 286 | } else if accountType == "oauth" { | 286 | } else if accountType == "oauth" { |
| 287 | log.Debugf("Use OAuth %s for model %s", accountInfo, req.Model) | 287 | log.Debugf("Use OAuth %s for model %s", accountInfo, req.Model) |
| 288 | } else if accountType == "cookie" { | 288 | } else if accountType == "cookie" { |
| 289 | log.Debugf("Use Cookie %s for model %s", util.HideAPIKey(accountInfo), req.Model) | 289 | // Only Gemini Web uses cookie; print stable account label as-is. |
| 290 | log.Debugf("Use Cookie %s for model %s", accountInfo, req.Model) | ||
| 290 | } | 291 | } |
| 291 | 292 | ||
| 292 | tried[auth.ID] = struct{}{} | 293 | tried[auth.ID] = struct{}{} |
| @@ -333,7 +334,7 @@ func (m *Manager) executeCountWithProvider(ctx context.Context, provider string, · 40 unmodified lines | |||
| 333 | } else if accountType == "oauth" { | 334 | } else if accountType == "oauth" { |
| 334 | log.Debugf("Use OAuth %s for model %s", accountInfo, req.Model) | 335 | log.Debugf("Use OAuth %s for model %s", accountInfo, req.Model) |
| 335 | } else if accountType == "cookie" { | 336 | } else if accountType == "cookie" { |
| 336 | log.Debugf("Use Cookie %s for model %s", util.HideAPIKey(accountInfo), req.Model) | 337 | log.Debugf("Use Cookie %s for model %s", accountInfo, req.Model) |
| 337 | } | 338 | } |
| 338 | 339 | ||
| 339 | tried[auth.ID] = struct{}{} | 340 | tried[auth.ID] = struct{}{} |
| @@ -380,7 +381,7 @@ func (m *Manager) executeStreamWithProvider(ctx context.Context, provider string · 40 unmodified lines | |||
| 380 | } else if accountType == "oauth" { | 381 | } else if accountType == "oauth" { |
| 381 | log.Debugf("Use OAuth %s for model %s", accountInfo, req.Model) | 382 | log.Debugf("Use OAuth %s for model %s", accountInfo, req.Model) |
| 382 | } else if accountType == "cookie" { | 383 | } else if accountType == "cookie" { |
| 383 | log.Debugf("Use Cookie %s for model %s", util.HideAPIKey(accountInfo), req.Model) | 384 | log.Debugf("Use Cookie %s for model %s", accountInfo, req.Model) |
| 384 | } | 385 | } |
| 385 | 386 | ||
| 386 | tried[auth.ID] = struct{}{} | 387 | tried[auth.ID] = struct{}{} |
| @@ -129,19 +129,18 @@ func (a *Auth) AccountInfo() (string, string) { · 128 unmodified lines | |||
| 129 | return "", "" | 129 | return "", "" |
| 130 | } | 130 | } |
| 131 | if strings.ToLower(a.Provider) == "gemini-web" { | 131 | if strings.ToLower(a.Provider) == "gemini-web" { |
| 132 | // Prefer explicit label written into auth file (e.g., gemini-web-<hash>) | ||
| 132 | if a.Metadata != nil { | 133 | if a.Metadata != nil { |
| 133 | if v, ok := a.Metadata["secure_1psid"].(string); ok && v != "" { | 134 | if v, ok := a.Metadata["label"].(string); ok && strings.TrimSpace(v) != "" { |
| 134 | return "cookie", v | 135 | return "cookie", strings.TrimSpace(v) |
| 135 | } | ||
| 136 | if v, ok := a.Metadata["__Secure-1PSID"].(string); ok && v != "" { | ||
| 137 | return "cookie", v | ||
| 138 | } | 136 | } |
| 139 | } | 137 | } |
| 140 | if a.Attributes != nil { | 138 | // Minimal fallback to cookie value for backward compatibility |
| 141 | if v := a.Attributes["secure_1psid"]; v != "" { | 139 | if a.Metadata != nil { |
| 140 | if v, ok := a.Metadata["secure_1psid"].(string); ok && v != "" { | ||
| 142 | return "cookie", v | 141 | return "cookie", v |
| 143 | } | 142 | } |
| 144 | if v := a.Attributes["api_key"]; v != "" { | 143 | if v, ok := a.Metadata["__Secure-1PSID"].(string); ok && v != "" { |
| 145 | return "cookie", v | 144 | return "cookie", v |
| 146 | } | 145 | } |
| 147 | } | 146 | } |