gemini-webapi 1.17.2__py3-none-any.whl → 1.18.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,7 +5,7 @@ import orjson as json
5
5
  from ..constants import GRPC
6
6
  from ..exceptions import APIError
7
7
  from ..types import Gem, GemJar, RPCData
8
- from ..utils import running, logger
8
+ from ..utils import extract_json_from_response, get_nested_value, logger
9
9
 
10
10
 
11
11
  class GemMixin:
@@ -41,7 +41,6 @@ class GemMixin:
41
41
 
42
42
  return self._gems
43
43
 
44
- @running(retry=2)
45
44
  async def fetch_gems(self, include_hidden: bool = False, **kwargs) -> GemJar:
46
45
  """
47
46
  Get a list of available gems from gemini, including system predefined gems and user-created custom gems.
@@ -78,24 +77,32 @@ class GemMixin:
78
77
  )
79
78
 
80
79
  try:
81
- response_json = json.loads(response.text.split("\n")[2])
80
+ response_json = extract_json_from_response(response.text)
82
81
 
83
82
  predefined_gems, custom_gems = [], []
84
83
 
85
84
  for part in response_json:
86
- if part[-1] == "system":
87
- predefined_gems = json.loads(part[2])[2]
88
- elif part[-1] == "custom":
89
- if custom_gems_container := json.loads(part[2]):
90
- custom_gems = custom_gems_container[2]
85
+ try:
86
+ identifier = get_nested_value(part, [-1])
87
+ part_body_str = get_nested_value(part, [2])
88
+ if not part_body_str:
89
+ continue
90
+
91
+ part_body = json.loads(part_body_str)
92
+ if identifier == "system":
93
+ predefined_gems = get_nested_value(part_body, [2], [])
94
+ elif identifier == "custom":
95
+ custom_gems = get_nested_value(part_body, [2], [])
96
+ except json.JSONDecodeError:
97
+ continue
91
98
 
92
99
  if not predefined_gems and not custom_gems:
93
100
  raise Exception
94
101
  except Exception:
95
102
  await self.close()
96
- logger.debug(f"Invalid response: {response.text}")
103
+ logger.debug(f"Unexpected response data structure: {response.text}")
97
104
  raise APIError(
98
- "Failed to fetch gems. Invalid response data received. Client will try to re-initialize on next request."
105
+ "Failed to fetch gems. Unexpected response data structure. Client will try to re-initialize on next request."
99
106
  )
100
107
 
101
108
  self._gems = GemJar(
@@ -131,7 +138,6 @@ class GemMixin:
131
138
 
132
139
  return self._gems
133
140
 
134
- @running(retry=2)
135
141
  async def create_gem(self, name: str, prompt: str, description: str = "") -> Gem:
136
142
  """
137
143
  Create a new custom gem.
@@ -175,19 +181,26 @@ class GemMixin:
175
181
  [],
176
182
  ]
177
183
  ]
178
- ).decode(),
184
+ ).decode("utf-8"),
179
185
  )
180
186
  ]
181
187
  )
182
188
 
183
189
  try:
184
- response_json = json.loads(response.text.split("\n")[2])
185
- gem_id = json.loads(response_json[0][2])[0]
190
+ response_json = extract_json_from_response(response.text)
191
+ part_body_str = get_nested_value(response_json, [0, 2], verbose=True)
192
+ if not part_body_str:
193
+ raise Exception
194
+
195
+ part_body = json.loads(part_body_str)
196
+ gem_id = get_nested_value(part_body, [0], verbose=True)
197
+ if not gem_id:
198
+ raise Exception
186
199
  except Exception:
187
200
  await self.close()
188
- logger.debug(f"Invalid response: {response.text}")
201
+ logger.debug(f"Unexpected response data structure: {response.text}")
189
202
  raise APIError(
190
- "Failed to create gem. Invalid response data received. Client will try to re-initialize on next request."
203
+ "Failed to create gem. Unexpected response data structure. Client will try to re-initialize on next request."
191
204
  )
192
205
 
193
206
  return Gem(
@@ -198,7 +211,6 @@ class GemMixin:
198
211
  predefined=False,
199
212
  )
200
213
 
201
- @running(retry=2)
202
214
  async def update_gem(
203
215
  self, gem: Gem | str, name: str, prompt: str, description: str = ""
204
216
  ) -> Gem:
@@ -253,7 +265,7 @@ class GemMixin:
253
265
  0,
254
266
  ],
255
267
  ]
256
- ).decode(),
268
+ ).decode("utf-8"),
257
269
  )
258
270
  ]
259
271
  )
@@ -266,7 +278,6 @@ class GemMixin:
266
278
  predefined=False,
267
279
  )
268
280
 
269
- @running(retry=2)
270
281
  async def delete_gem(self, gem: Gem | str, **kwargs) -> None:
271
282
  """
272
283
  Delete a custom gem.
@@ -283,6 +294,10 @@ class GemMixin:
283
294
  gem_id = gem
284
295
 
285
296
  await self._batch_execute(
286
- [RPCData(rpcid=GRPC.DELETE_GEM, payload=json.dumps([gem_id]).decode())],
297
+ [
298
+ RPCData(
299
+ rpcid=GRPC.DELETE_GEM, payload=json.dumps([gem_id]).decode("utf-8")
300
+ )
301
+ ],
287
302
  **kwargs,
288
303
  )
@@ -25,6 +25,9 @@ class GRPC(StrEnum):
25
25
  UPDATE_GEM = "kHv0Vd"
26
26
  DELETE_GEM = "UXcSJb"
27
27
 
28
+ # Activity methods
29
+ BARD_ACTIVITY = "ESY5D"
30
+
28
31
 
29
32
  class Headers(Enum):
30
33
  GEMINI = {
@@ -32,7 +35,7 @@ class Headers(Enum):
32
35
  "Host": "gemini.google.com",
33
36
  "Origin": "https://gemini.google.com",
34
37
  "Referer": "https://gemini.google.com/",
35
- "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
38
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36",
36
39
  "X-Same-Domain": "1",
37
40
  }
38
41
  ROTATE_COOKIES = {
@@ -46,21 +49,21 @@ class Model(Enum):
46
49
  G_3_0_PRO = (
47
50
  "gemini-3.0-pro",
48
51
  {
49
- "x-goog-ext-525001261-jspb": '[1,null,null,null,"9d8ca3786ebdfbea",null,null,0,[4]]'
52
+ "x-goog-ext-525001261-jspb": '[1,null,null,null,"9d8ca3786ebdfbea",null,null,0,[4],null,null,1]'
50
53
  },
51
54
  False,
52
55
  )
53
- G_2_5_PRO = (
54
- "gemini-2.5-pro",
56
+ G_3_0_FLASH = (
57
+ "gemini-3.0-flash",
55
58
  {
56
- "x-goog-ext-525001261-jspb": '[1,null,null,null,"4af6c7f5da75d65d",null,null,0,[4]]'
59
+ "x-goog-ext-525001261-jspb": '[1,null,null,null,"fbb127bbb056c959",null,null,0,[4],null,null,1]'
57
60
  },
58
61
  False,
59
62
  )
60
- G_2_5_FLASH = (
61
- "gemini-2.5-flash",
63
+ G_3_0_FLASH_THINKING = (
64
+ "gemini-3.0-flash-thinking",
62
65
  {
63
- "x-goog-ext-525001261-jspb": '[1,null,null,null,"9ec249fc9ad08861",null,null,0,[4]]'
66
+ "x-goog-ext-525001261-jspb": '[1,null,null,null,"5bf011840784117a",null,null,0,[4],null,null,1]'
64
67
  },
65
68
  False,
66
69
  )
@@ -103,6 +106,7 @@ class ErrorCode(IntEnum):
103
106
  Known error codes returned from server.
104
107
  """
105
108
 
109
+ TEMPORARY_ERROR_1013 = 1013 # Randomly raised when generating with certain models, but disappears soon after
106
110
  USAGE_LIMIT_EXCEEDED = 1037
107
111
  MODEL_INCONSISTENT = 1050
108
112
  MODEL_HEADER_INVALID = 1052
@@ -24,7 +24,9 @@ class Candidate(BaseModel):
24
24
 
25
25
  rcid: str
26
26
  text: str
27
+ text_delta: str | None = None
27
28
  thoughts: str | None = None
29
+ thoughts_delta: str | None = None
28
30
  web_images: list[WebImage] = []
29
31
  generated_images: list[GeneratedImage] = []
30
32
 
@@ -1,8 +1,9 @@
1
1
  import re
2
2
  from pathlib import Path
3
3
  from datetime import datetime
4
+ from typing import Any
4
5
 
5
- from httpx import AsyncClient, HTTPError
6
+ from httpx import AsyncClient, Cookies, HTTPError
6
7
  from pydantic import BaseModel, field_validator
7
8
 
8
9
  from ..utils import logger
@@ -39,7 +40,7 @@ class Image(BaseModel):
39
40
  self,
40
41
  path: str = "temp",
41
42
  filename: str | None = None,
42
- cookies: dict | None = None,
43
+ cookies: dict | Cookies | None = None,
43
44
  verbose: bool = False,
44
45
  skip_invalid_filename: bool = False,
45
46
  ) -> str | None:
@@ -121,16 +122,16 @@ class GeneratedImage(Image):
121
122
 
122
123
  Parameters
123
124
  ----------
124
- cookies: `dict`
125
+ cookies: `dict | httpx.Cookies`
125
126
  Cookies used for requesting the content of the generated image, inherit from GeminiClient object or manually set.
126
127
  Should contain valid "__Secure-1PSID" and "__Secure-1PSIDTS" values.
127
128
  """
128
129
 
129
- cookies: dict[str, str]
130
+ cookies: Any
130
131
 
131
132
  @field_validator("cookies")
132
133
  @classmethod
133
- def validate_cookies(cls, v: dict) -> dict:
134
+ def validate_cookies(cls, v: Any) -> Any:
134
135
  if len(v) == 0:
135
136
  raise ValueError(
136
137
  "GeneratedImage is designed to be initialized with same cookies as GeminiClient."
@@ -142,7 +143,7 @@ class GeneratedImage(Image):
142
143
  self,
143
144
  path: str = "temp",
144
145
  filename: str | None = None,
145
- cookies: dict | None = None,
146
+ cookies: dict | Cookies | None = None,
146
147
  verbose: bool = False,
147
148
  skip_invalid_filename: bool = False,
148
149
  full_size: bool = True,
@@ -32,10 +32,18 @@ class ModelOutput(BaseModel):
32
32
  def text(self) -> str:
33
33
  return self.candidates[self.chosen].text
34
34
 
35
+ @property
36
+ def text_delta(self) -> str:
37
+ return self.candidates[self.chosen].text_delta or ""
38
+
35
39
  @property
36
40
  def thoughts(self) -> str | None:
37
41
  return self.candidates[self.chosen].thoughts
38
42
 
43
+ @property
44
+ def thoughts_delta(self) -> str:
45
+ return self.candidates[self.chosen].thoughts_delta or ""
46
+
39
47
  @property
40
48
  def images(self) -> list[Image]:
41
49
  return self.candidates[self.chosen].images
@@ -1,14 +1,9 @@
1
1
  # flake8: noqa
2
2
 
3
- from asyncio import Task
4
-
5
3
  from .decorators import running
6
4
  from .get_access_token import get_access_token
7
5
  from .load_browser_cookies import load_browser_cookies
8
6
  from .logger import logger, set_log_level
9
- from .parsing import extract_json_from_response, get_nested_value
7
+ from .parsing import extract_json_from_response, get_nested_value, parse_stream_frames
10
8
  from .rotate_1psidts import rotate_1psidts
11
9
  from .upload_file import upload_file, parse_file_name
12
-
13
-
14
- rotate_tasks: dict[str, Task] = {}
@@ -1,13 +1,18 @@
1
1
  import asyncio
2
2
  import functools
3
+ import inspect
3
4
  from collections.abc import Callable
4
5
 
5
- from ..exceptions import APIError, ImageGenerationError
6
+ from ..exceptions import APIError
7
+
8
+
9
+ DELAY_FACTOR = 5
6
10
 
7
11
 
8
12
  def running(retry: int = 0) -> Callable:
9
13
  """
10
14
  Decorator to check if GeminiClient is running before making a request.
15
+ Supports both regular async functions and async generators.
11
16
 
12
17
  Parameters
13
18
  ----------
@@ -16,38 +21,78 @@ def running(retry: int = 0) -> Callable:
16
21
  """
17
22
 
18
23
  def decorator(func):
19
- @functools.wraps(func)
20
- async def wrapper(client, *args, retry=retry, **kwargs):
21
- try:
22
- if not client._running:
23
- await client.init(
24
- timeout=client.timeout,
25
- auto_close=client.auto_close,
26
- close_delay=client.close_delay,
27
- auto_refresh=client.auto_refresh,
28
- refresh_interval=client.refresh_interval,
29
- verbose=False,
30
- )
31
- if client._running:
32
- return await func(client, *args, **kwargs)
33
-
34
- # Should not reach here
35
- raise APIError(
36
- f"Invalid function call: GeminiClient.{func.__name__}. Client initialization failed."
37
- )
38
- else:
24
+ if inspect.isasyncgenfunction(func):
25
+
26
+ @functools.wraps(func)
27
+ async def wrapper(client, *args, current_retry=None, **kwargs):
28
+ if current_retry is None:
29
+ current_retry = retry
30
+
31
+ try:
32
+ if not client._running:
33
+ await client.init(
34
+ timeout=client.timeout,
35
+ auto_close=client.auto_close,
36
+ close_delay=client.close_delay,
37
+ auto_refresh=client.auto_refresh,
38
+ refresh_interval=client.refresh_interval,
39
+ verbose=client.verbose,
40
+ )
41
+
42
+ if not client._running:
43
+ raise APIError(
44
+ f"Invalid function call: GeminiClient.{func.__name__}. Client initialization failed."
45
+ )
46
+
47
+ async for item in func(client, *args, **kwargs):
48
+ yield item
49
+ except APIError:
50
+ if current_retry > 0:
51
+ delay = (retry - current_retry + 1) * DELAY_FACTOR
52
+ await asyncio.sleep(delay)
53
+ async for item in wrapper(
54
+ client, *args, current_retry=current_retry - 1, **kwargs
55
+ ):
56
+ yield item
57
+ else:
58
+ raise
59
+
60
+ return wrapper
61
+ else:
62
+
63
+ @functools.wraps(func)
64
+ async def wrapper(client, *args, current_retry=None, **kwargs):
65
+ if current_retry is None:
66
+ current_retry = retry
67
+
68
+ try:
69
+ if not client._running:
70
+ await client.init(
71
+ timeout=client.timeout,
72
+ auto_close=client.auto_close,
73
+ close_delay=client.close_delay,
74
+ auto_refresh=client.auto_refresh,
75
+ refresh_interval=client.refresh_interval,
76
+ verbose=client.verbose,
77
+ )
78
+
79
+ if not client._running:
80
+ raise APIError(
81
+ f"Invalid function call: GeminiClient.{func.__name__}. Client initialization failed."
82
+ )
83
+
39
84
  return await func(client, *args, **kwargs)
40
- except APIError as e:
41
- # Image generation takes too long, only retry once
42
- if isinstance(e, ImageGenerationError):
43
- retry = min(1, retry)
85
+ except APIError:
86
+ if current_retry > 0:
87
+ delay = (retry - current_retry + 1) * DELAY_FACTOR
88
+ await asyncio.sleep(delay)
44
89
 
45
- if retry > 0:
46
- await asyncio.sleep(1)
47
- return await wrapper(client, *args, retry=retry - 1, **kwargs)
90
+ return await wrapper(
91
+ client, *args, current_retry=current_retry - 1, **kwargs
92
+ )
48
93
 
49
- raise
94
+ raise
50
95
 
51
- return wrapper
96
+ return wrapper
52
97
 
53
98
  return decorator
@@ -4,7 +4,7 @@ import asyncio
4
4
  from asyncio import Task
5
5
  from pathlib import Path
6
6
 
7
- from httpx import AsyncClient, Response
7
+ from httpx import AsyncClient, Cookies, Response
8
8
 
9
9
  from ..constants import Endpoint, Headers
10
10
  from ..exceptions import AuthError
@@ -13,8 +13,8 @@ from .logger import logger
13
13
 
14
14
 
15
15
  async def send_request(
16
- cookies: dict, proxy: str | None = None
17
- ) -> tuple[Response | None, dict]:
16
+ cookies: dict | Cookies, proxy: str | None = None
17
+ ) -> tuple[Response | None, Cookies]:
18
18
  """
19
19
  Send http request with provided cookies.
20
20
  """
@@ -25,16 +25,18 @@ async def send_request(
25
25
  headers=Headers.GEMINI.value,
26
26
  cookies=cookies,
27
27
  follow_redirects=True,
28
- verify=False,
29
28
  ) as client:
30
- response = await client.get(Endpoint.INIT.value)
29
+ response = await client.get(Endpoint.INIT)
31
30
  response.raise_for_status()
32
- return response, cookies
31
+ return response, client.cookies
33
32
 
34
33
 
35
34
  async def get_access_token(
36
- base_cookies: dict, proxy: str | None = None, verbose: bool = False
37
- ) -> tuple[str, dict]:
35
+ base_cookies: dict | Cookies,
36
+ proxy: str | None = None,
37
+ verbose: bool = False,
38
+ verify: bool = True,
39
+ ) -> tuple[str, Cookies, str | None, str | None]:
38
40
  """
39
41
  Send a get request to gemini.google.com for each group of available cookies and return
40
42
  the value of "SNlM0e" as access token on the first successful request.
@@ -46,19 +48,19 @@ async def get_access_token(
46
48
 
47
49
  Parameters
48
50
  ----------
49
- base_cookies : `dict`
51
+ base_cookies : `dict | httpx.Cookies`
50
52
  Base cookies to be used in the request.
51
53
  proxy: `str`, optional
52
54
  Proxy URL.
53
55
  verbose: `bool`, optional
54
56
  If `True`, will print more infomation in logs.
57
+ verify: `bool`, optional
58
+ Whether to verify SSL certificates.
55
59
 
56
60
  Returns
57
61
  -------
58
- `str`
59
- Access token.
60
- `dict`
61
- Cookies of the successful request.
62
+ `tuple[str, str | None, str | None, Cookies]`
63
+ By order: access token; build label; session id; cookies of the successful request.
62
64
 
63
65
  Raises
64
66
  ------
@@ -67,22 +69,22 @@ async def get_access_token(
67
69
  """
68
70
 
69
71
  async with AsyncClient(
70
- http2=True,
71
- proxy=proxy,
72
- follow_redirects=True,
73
- verify=False,
72
+ http2=True, proxy=proxy, follow_redirects=True, verify=verify
74
73
  ) as client:
75
- response = await client.get(Endpoint.GOOGLE.value)
74
+ response = await client.get(Endpoint.GOOGLE)
76
75
 
77
- extra_cookies = {}
76
+ extra_cookies = Cookies()
78
77
  if response.status_code == 200:
79
78
  extra_cookies = response.cookies
80
79
 
81
80
  tasks = []
82
81
 
83
82
  # Base cookies passed directly on initializing client
83
+ # We use a Jar to merge extra_cookies and base_cookies safely (preserving domains)
84
84
  if "__Secure-1PSID" in base_cookies and "__Secure-1PSIDTS" in base_cookies:
85
- tasks.append(Task(send_request({**extra_cookies, **base_cookies}, proxy=proxy)))
85
+ jar = Cookies(extra_cookies)
86
+ jar.update(base_cookies)
87
+ tasks.append(Task(send_request(jar, proxy=proxy)))
86
88
  elif verbose:
87
89
  logger.debug(
88
90
  "Skipping loading base cookies. Either __Secure-1PSID or __Secure-1PSIDTS is not provided."
@@ -94,18 +96,25 @@ async def get_access_token(
94
96
  and Path(GEMINI_COOKIE_PATH)
95
97
  or (Path(__file__).parent / "temp")
96
98
  )
97
- if "__Secure-1PSID" in base_cookies:
98
- filename = f".cached_1psidts_{base_cookies['__Secure-1PSID']}.txt"
99
+
100
+ # Safely get __Secure-1PSID value
101
+ if isinstance(base_cookies, Cookies):
102
+ secure_1psid = base_cookies.get(
103
+ "__Secure-1PSID", domain=".google.com"
104
+ ) or base_cookies.get("__Secure-1PSID")
105
+ else:
106
+ secure_1psid = base_cookies.get("__Secure-1PSID")
107
+
108
+ if secure_1psid:
109
+ filename = f".cached_1psidts_{secure_1psid}.txt"
99
110
  cache_file = cache_dir / filename
100
111
  if cache_file.is_file():
101
112
  cached_1psidts = cache_file.read_text()
102
113
  if cached_1psidts:
103
- cached_cookies = {
104
- **extra_cookies,
105
- **base_cookies,
106
- "__Secure-1PSIDTS": cached_1psidts,
107
- }
108
- tasks.append(Task(send_request(cached_cookies, proxy=proxy)))
114
+ jar = Cookies(extra_cookies)
115
+ jar.update(base_cookies)
116
+ jar.set("__Secure-1PSIDTS", cached_1psidts, domain=".google.com")
117
+ tasks.append(Task(send_request(jar, proxy=proxy)))
109
118
  elif verbose:
110
119
  logger.debug("Skipping loading cached cookies. Cache file is empty.")
111
120
  elif verbose:
@@ -116,12 +125,11 @@ async def get_access_token(
116
125
  for cache_file in cache_files:
117
126
  cached_1psidts = cache_file.read_text()
118
127
  if cached_1psidts:
119
- cached_cookies = {
120
- **extra_cookies,
121
- "__Secure-1PSID": cache_file.stem[16:],
122
- "__Secure-1PSIDTS": cached_1psidts,
123
- }
124
- tasks.append(Task(send_request(cached_cookies, proxy=proxy)))
128
+ jar = Cookies(extra_cookies)
129
+ psid = cache_file.stem[16:]
130
+ jar.set("__Secure-1PSID", psid, domain=".google.com")
131
+ jar.set("__Secure-1PSIDTS", cached_1psidts, domain=".google.com")
132
+ tasks.append(Task(send_request(jar, proxy=proxy)))
125
133
  valid_caches += 1
126
134
 
127
135
  if valid_caches == 0 and verbose:
@@ -180,13 +188,20 @@ async def get_access_token(
180
188
  for i, future in enumerate(asyncio.as_completed(tasks)):
181
189
  try:
182
190
  response, request_cookies = await future
183
- match = re.search(r'"SNlM0e":"(.*?)"', response.text)
184
- if match:
191
+ snlm0e = re.search(r'"SNlM0e":\s*"(.*?)"', response.text)
192
+ if snlm0e:
193
+ cfb2h = re.search(r'"cfb2h":\s*"(.*?)"', response.text)
194
+ fdrfje = re.search(r'"FdrFJe":\s*"(.*?)"', response.text)
185
195
  if verbose:
186
196
  logger.debug(
187
197
  f"Init attempt ({i + 1}/{len(tasks)}) succeeded. Initializing client..."
188
198
  )
189
- return match.group(1), request_cookies
199
+ return (
200
+ snlm0e.group(1),
201
+ cfb2h.group(1) if cfb2h else None,
202
+ fdrfje.group(1) if fdrfje else None,
203
+ request_cookies,
204
+ )
190
205
  elif verbose:
191
206
  logger.debug(
192
207
  f"Init attempt ({i + 1}/{len(tasks)}) failed. Cookies invalid."