History

Merge pull request #63 from router-for-me/gemini-web

Gemini-web

Luis Pater committed 1 year agov6.0.2

Commit 830b51c2 parents cc8c46d 2a274d48 files · +73 −112Browse files

Unified · Split

8 files
  1. internal/api/handlers/management/auth_files.go +2 −0
  2. internal/auth/gemini/gemini-web_token.go +14 −0
  3. internal/cmd/gemini-web_auth.go +4 −0
  4. internal/provider/gemini-web/client.go +21 −43
  5. internal/provider/gemini-web/media.go +6 −30
  6. internal/provider/gemini-web/state.go +15 −28
  7. sdk/cliproxy/auth/manager.go +4 −3
  8. sdk/cliproxy/auth/types.go +7 −8
internal/api/handlers/management/auth_files.go+2−0
@@ -714,6 +714,8 @@ func (h *Handler) CreateGeminiWebToken(c *gin.Context) { · 713 unmodified lines
714714 Secure1PSID: payload.Secure1PSID,
715715 Secure1PSIDTS: payload.Secure1PSIDTS,
716716 }
717+ // Provide a stable label (gemini-web-<hash>) for logging and identification
718+ tokenStorage.Label = strings.TrimSuffix(fileName, ".json")
717719
718720 record := &sdkAuth.TokenRecord{
719721 Provider: "gemini-web",
internal/auth/gemini/gemini-web_token.go+14−0
@@ -8,6 +8,7 @@ import ( · 7 unmodified lines
88 "fmt"
99 "os"
1010 "path/filepath"
11+ "strings"
1112 "time"
1213
1314 "github.com/router-for-me/CLIProxyAPI/v6/internal/misc"
@@ -20,12 +21,25 @@ type GeminiWebTokenStorage struct { · 6 unmodified lines
2021 Secure1PSIDTS string `json:"secure_1psidts"`
2122 Type string `json:"type"`
2223 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"`
2327 }
2428
2529 // SaveTokenToFile serializes the Gemini Web token storage to a JSON file.
2630 func (ts *GeminiWebTokenStorage) SaveTokenToFile(authFilePath string) error {
2731 misc.LogSavingCredentials(authFilePath)
2832 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+ }
2943 if ts.LastRefresh == "" {
3044 ts.LastRefresh = time.Now().Format(time.RFC3339)
3145 }
internal/cmd/gemini-web_auth.go+4−0
@@ -49,6 +49,10 @@ func DoGeminiWebAuth(cfg *config.Config) { · 48 unmodified lines
4949 hasher.Write([]byte(secure1psid))
5050 hash := hex.EncodeToString(hasher.Sum(nil))
5151 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+ }
5256 record := &sdkAuth.TokenRecord{
5357 Provider: "gemini-web",
5458 FileName: fileName,
internal/provider/gemini-web/client.go+21−43
@@ -97,8 +97,12 @@ func getAccessToken(baseCookies map[string]string, proxy string, verbose bool, i · 96 unmodified lines
9797 {
9898 client := newHTTPClient(httpOptions{ProxyURL: proxy, Insecure: insecure, FollowRedirects: true})
9999 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 {
102106 if u, err := url.Parse(EndpointGoogle); err == nil {
103107 for _, c := range client.Jar.Cookies(u) {
104108 extraCookies[c.Name] = c.Value
@@ -172,18 +176,10 @@ func rotate1PSIDTS(cookies map[string]string, proxy string, insecure bool) (stri · 67 unmodified lines
172176 return "", &AuthError{Msg: "__Secure-1PSID missing"}
173177 }
174178
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})
185181
186− req, _ := http.NewRequest(http.MethodPost, EndpointRotateCookies, io.NopCloser(stringsReader("[000,\"-0000000000000000000\"]")))
182+ req, _ := http.NewRequest(http.MethodPost, EndpointRotateCookies, strings.NewReader("[000,\"-0000000000000000000\"]"))
187183 applyHeaders(req, HeadersRotateCookies)
188184 applyCookies(req, cookies)
189185
@@ -207,25 +203,18 @@ func rotate1PSIDTS(cookies map[string]string, proxy string, insecure bool) (stri · 17 unmodified lines
207203 return c.Value, nil
208204 }
209205 }
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+ }
221213 }
222− n := copy(p, r.s[r.i:])
223− r.i += n
224− return n, nil
214+ return "", nil
225215 }
226216
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.
229218 func MaskToken28(s string) string {
230219 n := len(s)
231220 if n == 0 {
@@ -431,21 +420,10 @@ func (c *GeminiClient) generateOnce(prompt string, files []string, model Model, · 199 unmodified lines
431420 form.Set("f.req", string(outerJSON))
432421
433422 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)
445425 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)
449427
450428 resp, err := c.httpClient.Do(req)
451429 if err != nil {
internal/provider/gemini-web/media.go+6−30
@@ -2,7 +2,6 @@ package geminiwebapi · 1 unmodified line
22
33 import (
44 "bytes"
5− "crypto/tls"
65 "encoding/base64"
76 "encoding/json"
87 "errors"
@@ -11,8 +10,6 @@ import ( · 2 unmodified lines
1110 "math"
1211 "mime/multipart"
1312 "net/http"
14− "net/http/cookiejar"
15− "net/url"
1613 "os"
1714 "path/filepath"
1815 "regexp"
@@ -69,18 +66,9 @@ func (i Image) Save(path string, filename string, cookies map[string]string, ver · 50 unmodified lines
6966 }
7067 }
7168 }
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
8472
8573 // Helper to set raw Cookie header using provided cookies (to mirror Python client behavior).
8674 buildCookieHeader := func(m map[string]string) string {
@@ -352,23 +340,11 @@ func uploadFile(path string, proxy string, insecure bool) (string, error) { · 265 unmodified lines
352340 }
353341 _ = mw.Close()
354342
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
365345
366346 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)
372348 req.Header.Set("Content-Type", mw.FormDataContentType())
373349 req.Header.Set("Accept", "*/*")
374350 req.Header.Set("Connection", "keep-alive")
internal/provider/gemini-web/state.go+15−28
@@ -21,6 +21,7 @@ import ( · 20 unmodified lines
2121 "github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces"
2222 "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/translator"
2323 cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor"
24+ log "github.com/sirupsen/logrus"
2425 "github.com/tidwall/gjson"
2526 "github.com/tidwall/sjson"
2627 bolt "go.etcd.io/bbolt"
@@ -97,12 +98,12 @@ func (s *GeminiWebState) Label() string { · 70 unmodified lines
9798 }
9899
99100 func (s *GeminiWebState) loadConversationCaches() {
100− if path := s.convStorePath(); path != "" {
101+ if path := s.convPath(); path != "" {
101102 if store, err := LoadConvStore(path); err == nil {
102103 s.convStore = store
103104 }
104105 }
105− if path := s.convDataPath(); path != "" {
106+ if path := s.convPath(); path != "" {
106107 if items, index, err := LoadConvData(path); err == nil {
107108 s.convData = items
108109 s.convIndex = index
@@ -110,20 +111,14 @@ func (s *GeminiWebState) loadConversationCaches() { · 1 unmodified line
110111 }
111112 }
112113
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 {
114116 base := s.storagePath
115117 if base == "" {
116− base = s.accountID + ".json"
118+ // Use accountID directly as base name; ConvBoltPath will append .bolt.
119+ base = s.accountID
117120 }
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)
127122 }
128123
129124 func (s *GeminiWebState) GetRequestMutex() *sync.Mutex { return &s.reqMu }
@@ -174,6 +169,8 @@ func (s *GeminiWebState) Refresh(ctx context.Context) error { · 44 unmodified lines
174169 s.client.Cookies["__Secure-1PSIDTS"] = newTS
175170 }
176171 s.tokenMu.Unlock()
172+ // Detailed debug log: provider and account.
173+ log.Debugf("gemini web account %s rotated 1PSIDTS: %s", s.accountID, MaskToken28(newTS))
177174 }
178175 s.lastRefresh = time.Now()
179176 return nil
@@ -405,7 +402,7 @@ func (s *GeminiWebState) persistConversation(modelName string, prep *geminiWebPr · 225 unmodified lines
405402 storeSnapshot[k] = cp
406403 }
407404 s.convMu.Unlock()
408− _ = SaveConvStore(s.convStorePath(), storeSnapshot)
405+ _ = SaveConvStore(s.convPath(), storeSnapshot)
409406 }
410407
411408 if !s.useReusableContext() {
@@ -433,7 +430,7 @@ func (s *GeminiWebState) persistConversation(modelName string, prep *geminiWebPr · 21 unmodified lines
433430 indexSnapshot[k] = v
434431 }
435432 s.convMu.Unlock()
436− _ = SaveConvData(s.convDataPath(), dataSnapshot, indexSnapshot)
433+ _ = SaveConvData(s.convPath(), dataSnapshot, indexSnapshot)
437434 }
438435
439436 func (s *GeminiWebState) addAPIResponseData(ctx context.Context, line []byte) {
@@ -570,19 +567,9 @@ func HashConversation(clientID, model string, msgs []StoredMessage) string { · 130 unmodified lines
570567 return Sha256Hex(b.String())
571568 }
572569
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 {
586573 wd, err := os.Getwd()
587574 if err != nil || wd == "" {
588575 wd = "."
sdk/cliproxy/auth/manager.go+4−3
@@ -286,7 +286,8 @@ func (m *Manager) executeWithProvider(ctx context.Context, provider string, req · 285 unmodified lines
286286 } else if accountType == "oauth" {
287287 log.Debugf("Use OAuth %s for model %s", accountInfo, req.Model)
288288 } 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)
290291 }
291292
292293 tried[auth.ID] = struct{}{}
@@ -333,7 +334,7 @@ func (m *Manager) executeCountWithProvider(ctx context.Context, provider string, · 40 unmodified lines
333334 } else if accountType == "oauth" {
334335 log.Debugf("Use OAuth %s for model %s", accountInfo, req.Model)
335336 } 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)
337338 }
338339
339340 tried[auth.ID] = struct{}{}
@@ -380,7 +381,7 @@ func (m *Manager) executeStreamWithProvider(ctx context.Context, provider string · 40 unmodified lines
380381 } else if accountType == "oauth" {
381382 log.Debugf("Use OAuth %s for model %s", accountInfo, req.Model)
382383 } 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)
384385 }
385386
386387 tried[auth.ID] = struct{}{}
sdk/cliproxy/auth/types.go+7−8
@@ -129,19 +129,18 @@ func (a *Auth) AccountInfo() (string, string) { · 128 unmodified lines
129129 return "", ""
130130 }
131131 if strings.ToLower(a.Provider) == "gemini-web" {
132+ // Prefer explicit label written into auth file (e.g., gemini-web-<hash>)
132133 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)
138136 }
139137 }
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 != "" {
142141 return "cookie", v
143142 }
144− if v := a.Attributes["api_key"]; v != "" {
143+ if v, ok := a.Metadata["__Secure-1PSID"].(string); ok && v != "" {
145144 return "cookie", v
146145 }
147146 }

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t