Gemini-web
Unified · Split
| @@ -714,6 +714,8 @@ func (h *Handler) CreateGeminiWebToken(c *gin.Context) { · 713 unmodified lines | ||
| 714 | 714 | Secure1PSID: payload.Secure1PSID, |
| 715 | 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 | 720 | record := &sdkAuth.TokenRecord{ |
| 719 | 721 | Provider: "gemini-web", |
| @@ -8,6 +8,7 @@ import ( · 7 unmodified lines | ||
| 8 | 8 | "fmt" |
| 9 | 9 | "os" |
| 10 | 10 | "path/filepath" |
| 11 | + "strings" | |
| 11 | 12 | "time" |
| 12 | 13 | |
| 13 | 14 | "github.com/router-for-me/CLIProxyAPI/v6/internal/misc" |
| @@ -20,12 +21,25 @@ type GeminiWebTokenStorage struct { · 6 unmodified lines | ||
| 20 | 21 | Secure1PSIDTS string `json:"secure_1psidts"` |
| 21 | 22 | Type string `json:"type"` |
| 22 | 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 | 29 | // SaveTokenToFile serializes the Gemini Web token storage to a JSON file. |
| 26 | 30 | func (ts *GeminiWebTokenStorage) SaveTokenToFile(authFilePath string) error { |
| 27 | 31 | misc.LogSavingCredentials(authFilePath) |
| 28 | 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 | 43 | if ts.LastRefresh == "" { |
| 30 | 44 | ts.LastRefresh = time.Now().Format(time.RFC3339) |
| 31 | 45 | } |
| @@ -49,6 +49,10 @@ func DoGeminiWebAuth(cfg *config.Config) { · 48 unmodified lines | ||
| 49 | 49 | hasher.Write([]byte(secure1psid)) |
| 50 | 50 | hash := hex.EncodeToString(hasher.Sum(nil)) |
| 51 | 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 | 56 | record := &sdkAuth.TokenRecord{ |
| 53 | 57 | Provider: "gemini-web", |
| 54 | 58 | FileName: fileName, |
| @@ -97,8 +97,12 @@ func getAccessToken(baseCookies map[string]string, proxy string, verbose bool, i · 96 unmodified lines | ||
| 97 | 97 | { |
| 98 | 98 | client := newHTTPClient(httpOptions{ProxyURL: proxy, Insecure: insecure, FollowRedirects: true}) |
| 99 | 99 | req, _ := http.NewRequest(http.MethodGet, EndpointGoogle, nil) |
| 100 | − resp, _ := client.Do(req) | |
| 101 | − if resp != nil { | |
| 100 | + resp, err := client.Do(req) | |
| 101 | + if err != nil { | |
| 102 | + if verbose { | |
| 103 | + log.Debugf("priming google cookies failed: %v", err) | |
| 104 | + } | |
| 105 | + } else if resp != nil { | |
| 102 | 106 | if u, err := url.Parse(EndpointGoogle); err == nil { |
| 103 | 107 | for _, c := range client.Jar.Cookies(u) { |
| 104 | 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 | 176 | return "", &AuthError{Msg: "__Secure-1PSID missing"} |
| 173 | 177 | } |
| 174 | 178 | |
| 175 | − tr := &http.Transport{} | |
| 176 | − if proxy != "" { | |
| 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} | |
| 179 | + // Reuse shared HTTP client helper for consistency. | |
| 180 | + client := newHTTPClient(httpOptions{ProxyURL: proxy, Insecure: insecure, FollowRedirects: true}) | |
| 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 | 183 | applyHeaders(req, HeadersRotateCookies) |
| 188 | 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 | 203 | return c.Value, nil |
| 208 | 204 | } |
| 209 | 205 | } |
| 210 | − return "", nil | |
| 211 | −} | |
| 212 | − | |
| 213 | −type constReader struct { | |
| 214 | − s string | |
| 215 | − i int | |
| 216 | −} | |
| 217 | − | |
| 218 | −func (r *constReader) Read(p []byte) (int, error) { | |
| 219 | − if r.i >= len(r.s) { | |
| 220 | − return 0, io.EOF | |
| 206 | + // Fallback: check cookie jar in case the Set-Cookie was on a redirect hop | |
| 207 | + if u, err := url.Parse(EndpointRotateCookies); err == nil && client.Jar != nil { | |
| 208 | + for _, c := range client.Jar.Cookies(u) { | |
| 209 | + if c.Name == "__Secure-1PSIDTS" && c.Value != "" { | |
| 210 | + return c.Value, nil | |
| 211 | + } | |
| 212 | + } | |
| 221 | 213 | } |
| 222 | − n := copy(p, r.s[r.i:]) | |
| 223 | − r.i += n | |
| 224 | − return n, nil | |
| 214 | + return "", nil | |
| 225 | 215 | } |
| 226 | 216 | |
| 227 | −func stringsReader(s string) io.Reader { return &constReader{s: s} } | |
| 228 | − | |
| 217 | +// MaskToken28 masks a sensitive token for safe logging. Keep middle partially visible. | |
| 229 | 218 | func MaskToken28(s string) string { |
| 230 | 219 | n := len(s) |
| 231 | 220 | if n == 0 { |
| @@ -431,21 +420,10 @@ func (c *GeminiClient) generateOnce(prompt string, files []string, model Model, · 199 unmodified lines | ||
| 431 | 420 | form.Set("f.req", string(outerJSON)) |
| 432 | 421 | |
| 433 | 422 | req, _ := http.NewRequest(http.MethodPost, EndpointGenerate, strings.NewReader(form.Encode())) |
| 434 | − // headers | |
| 435 | − for k, v := range HeadersGemini { | |
| 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 | − } | |
| 423 | + applyHeaders(req, HeadersGemini) | |
| 424 | + applyHeaders(req, model.ModelHeader) | |
| 445 | 425 | req.Header.Set("Content-Type", "application/x-www-form-urlencoded;charset=utf-8") |
| 446 | − for k, v := range c.Cookies { | |
| 447 | − req.AddCookie(&http.Cookie{Name: k, Value: v}) | |
| 448 | − } | |
| 426 | + applyCookies(req, c.Cookies) | |
| 449 | 427 | |
| 450 | 428 | resp, err := c.httpClient.Do(req) |
| 451 | 429 | if err != nil { |
| @@ -2,7 +2,6 @@ package geminiwebapi · 1 unmodified line | ||
| 2 | 2 | |
| 3 | 3 | import ( |
| 4 | 4 | "bytes" |
| 5 | − "crypto/tls" | |
| 6 | 5 | "encoding/base64" |
| 7 | 6 | "encoding/json" |
| 8 | 7 | "errors" |
| @@ -11,8 +10,6 @@ import ( · 2 unmodified lines | ||
| 11 | 10 | "math" |
| 12 | 11 | "mime/multipart" |
| 13 | 12 | "net/http" |
| 14 | − "net/http/cookiejar" | |
| 15 | − "net/url" | |
| 16 | 13 | "os" |
| 17 | 14 | "path/filepath" |
| 18 | 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. | |
| 73 | − tr := &http.Transport{} | |
| 74 | − if i.Proxy != "" { | |
| 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} | |
| 69 | + // Build client using shared helper to keep proxy/TLS behavior consistent. | |
| 70 | + client := newHTTPClient(httpOptions{ProxyURL: i.Proxy, Insecure: insecure, FollowRedirects: true}) | |
| 71 | + client.Timeout = 120 * time.Second | |
| 84 | 72 | |
| 85 | 73 | // Helper to set raw Cookie header using provided cookies (to mirror Python client behavior). |
| 86 | 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 | 341 | _ = mw.Close() |
| 354 | 342 | |
| 355 | − tr := &http.Transport{} | |
| 356 | − if proxy != "" { | |
| 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} | |
| 343 | + client := newHTTPClient(httpOptions{ProxyURL: proxy, Insecure: insecure, FollowRedirects: true}) | |
| 344 | + client.Timeout = 300 * time.Second | |
| 365 | 345 | |
| 366 | 346 | req, _ := http.NewRequest(http.MethodPost, EndpointUpload, &buf) |
| 367 | − for k, v := range HeadersUpload { | |
| 368 | − for _, vv := range v { | |
| 369 | − req.Header.Add(k, vv) | |
| 370 | − } | |
| 371 | − } | |
| 347 | + applyHeaders(req, HeadersUpload) | |
| 372 | 348 | req.Header.Set("Content-Type", mw.FormDataContentType()) |
| 373 | 349 | req.Header.Set("Accept", "*/*") |
| 374 | 350 | req.Header.Set("Connection", "keep-alive") |
| @@ -21,6 +21,7 @@ import ( · 20 unmodified lines | ||
| 21 | 21 | "github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces" |
| 22 | 22 | "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/translator" |
| 23 | 23 | cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor" |
| 24 | + log "github.com/sirupsen/logrus" | |
| 24 | 25 | "github.com/tidwall/gjson" |
| 25 | 26 | "github.com/tidwall/sjson" |
| 26 | 27 | bolt "go.etcd.io/bbolt" |
| @@ -97,12 +98,12 @@ func (s *GeminiWebState) Label() string { · 70 unmodified lines | ||
| 97 | 98 | } |
| 98 | 99 | |
| 99 | 100 | func (s *GeminiWebState) loadConversationCaches() { |
| 100 | − if path := s.convStorePath(); path != "" { | |
| 101 | + if path := s.convPath(); path != "" { | |
| 101 | 102 | if store, err := LoadConvStore(path); err == nil { |
| 102 | 103 | s.convStore = store |
| 103 | 104 | } |
| 104 | 105 | } |
| 105 | − if path := s.convDataPath(); path != "" { | |
| 106 | + if path := s.convPath(); path != "" { | |
| 106 | 107 | if items, index, err := LoadConvData(path); err == nil { |
| 107 | 108 | s.convData = items |
| 108 | 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 | 116 | base := s.storagePath |
| 115 | 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) | |
| 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) | |
| 121 | + return ConvBoltPath(base) | |
| 127 | 122 | } |
| 128 | 123 | |
| 129 | 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 | 169 | s.client.Cookies["__Secure-1PSIDTS"] = newTS |
| 175 | 170 | } |
| 176 | 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 | 175 | s.lastRefresh = time.Now() |
| 179 | 176 | return nil |
| @@ -405,7 +402,7 @@ func (s *GeminiWebState) persistConversation(modelName string, prep *geminiWebPr · 225 unmodified lines | ||
| 405 | 402 | storeSnapshot[k] = cp |
| 406 | 403 | } |
| 407 | 404 | s.convMu.Unlock() |
| 408 | − _ = SaveConvStore(s.convStorePath(), storeSnapshot) | |
| 405 | + _ = SaveConvStore(s.convPath(), storeSnapshot) | |
| 409 | 406 | } |
| 410 | 407 | |
| 411 | 408 | if !s.useReusableContext() { |
| @@ -433,7 +430,7 @@ func (s *GeminiWebState) persistConversation(modelName string, prep *geminiWebPr · 21 unmodified lines | ||
| 433 | 430 | indexSnapshot[k] = v |
| 434 | 431 | } |
| 435 | 432 | s.convMu.Unlock() |
| 436 | − _ = SaveConvData(s.convDataPath(), dataSnapshot, indexSnapshot) | |
| 433 | + _ = SaveConvData(s.convPath(), dataSnapshot, indexSnapshot) | |
| 437 | 434 | } |
| 438 | 435 | |
| 439 | 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 | 567 | return Sha256Hex(b.String()) |
| 571 | 568 | } |
| 572 | 569 | |
| 573 | −// ConvStorePath returns the path for account-level metadata persistence based on token file path. | |
| 574 | −func ConvStorePath(tokenFilePath string) string { | |
| 575 | − wd, err := os.Getwd() | |
| 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 { | |
| 570 | +// ConvBoltPath returns the BoltDB file path used for both account metadata and conversation data. | |
| 571 | +// Different logical datasets are kept in separate buckets within this single DB file. | |
| 572 | +func ConvBoltPath(tokenFilePath string) string { | |
| 586 | 573 | wd, err := os.Getwd() |
| 587 | 574 | if err != nil || wd == "" { |
| 588 | 575 | wd = "." |
| @@ -286,7 +286,8 @@ func (m *Manager) executeWithProvider(ctx context.Context, provider string, req · 285 unmodified lines | ||
| 286 | 286 | } else if accountType == "oauth" { |
| 287 | 287 | log.Debugf("Use OAuth %s for model %s", accountInfo, req.Model) |
| 288 | 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 | 293 | tried[auth.ID] = struct{}{} |
| @@ -333,7 +334,7 @@ func (m *Manager) executeCountWithProvider(ctx context.Context, provider string, · 40 unmodified lines | ||
| 333 | 334 | } else if accountType == "oauth" { |
| 334 | 335 | log.Debugf("Use OAuth %s for model %s", accountInfo, req.Model) |
| 335 | 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 | 340 | tried[auth.ID] = struct{}{} |
| @@ -380,7 +381,7 @@ func (m *Manager) executeStreamWithProvider(ctx context.Context, provider string · 40 unmodified lines | ||
| 380 | 381 | } else if accountType == "oauth" { |
| 381 | 382 | log.Debugf("Use OAuth %s for model %s", accountInfo, req.Model) |
| 382 | 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 | 387 | tried[auth.ID] = struct{}{} |
| @@ -129,19 +129,18 @@ func (a *Auth) AccountInfo() (string, string) { · 128 unmodified lines | ||
| 129 | 129 | return "", "" |
| 130 | 130 | } |
| 131 | 131 | if strings.ToLower(a.Provider) == "gemini-web" { |
| 132 | + // Prefer explicit label written into auth file (e.g., gemini-web-<hash>) | |
| 132 | 133 | if a.Metadata != nil { |
| 133 | − if v, ok := a.Metadata["secure_1psid"].(string); ok && v != "" { | |
| 134 | − return "cookie", v | |
| 135 | − } | |
| 136 | − if v, ok := a.Metadata["__Secure-1PSID"].(string); ok && v != "" { | |
| 137 | − return "cookie", v | |
| 134 | + if v, ok := a.Metadata["label"].(string); ok && strings.TrimSpace(v) != "" { | |
| 135 | + return "cookie", strings.TrimSpace(v) | |
| 138 | 136 | } |
| 139 | 137 | } |
| 140 | − if a.Attributes != nil { | |
| 141 | − if v := a.Attributes["secure_1psid"]; v != "" { | |
| 138 | + // Minimal fallback to cookie value for backward compatibility | |
| 139 | + if a.Metadata != nil { | |
| 140 | + if v, ok := a.Metadata["secure_1psid"].(string); ok && v != "" { | |
| 142 | 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 | 144 | return "cookie", v |
| 146 | 145 | } |
| 147 | 146 | } |