gemini-webapi 1.16.0__py3-none-any.whl → 1.17.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.
gemini_webapi/client.py CHANGED
@@ -10,31 +10,33 @@ from httpx import AsyncClient, ReadTimeout, Response
10
10
  from .components import GemMixin
11
11
  from .constants import Endpoint, ErrorCode, Headers, Model
12
12
  from .exceptions import (
13
- AuthError,
14
13
  APIError,
15
- ImageGenerationError,
16
- TimeoutError,
14
+ AuthError,
17
15
  GeminiError,
18
- UsageLimitExceeded,
16
+ ImageGenerationError,
19
17
  ModelInvalid,
20
18
  TemporarilyBlocked,
19
+ TimeoutError,
20
+ UsageLimitExceeded,
21
21
  )
22
22
  from .types import (
23
- WebImage,
24
- GeneratedImage,
25
23
  Candidate,
26
- ModelOutput,
27
24
  Gem,
25
+ GeneratedImage,
26
+ ModelOutput,
28
27
  RPCData,
28
+ WebImage,
29
29
  )
30
30
  from .utils import (
31
- upload_file,
31
+ extract_json_from_response,
32
+ get_access_token,
33
+ get_nested_value,
34
+ logger,
32
35
  parse_file_name,
33
36
  rotate_1psidts,
34
- get_access_token,
35
- running,
36
37
  rotate_tasks,
37
- logger,
38
+ running,
39
+ upload_file,
38
40
  )
39
41
 
40
42
 
@@ -42,7 +44,7 @@ class GeminiClient(GemMixin):
42
44
  """
43
45
  Async httpx client interface for gemini.google.com.
44
46
 
45
- `secure_1psid` must be provided unless the optional dependency `browser-cookie3` is installed and
47
+ `secure_1psid` must be provided unless the optional dependency `browser-cookie3` is installed, and
46
48
  you have logged in to google.com in your local browser.
47
49
 
48
50
  Parameters
@@ -203,6 +205,7 @@ class GeminiClient(GemMixin):
203
205
  if self.close_task:
204
206
  self.close_task.cancel()
205
207
  self.close_task = None
208
+
206
209
  self.close_task = asyncio.create_task(self.close(self.close_delay))
207
210
 
208
211
  async def start_auto_refresh(self) -> None:
@@ -211,18 +214,25 @@ class GeminiClient(GemMixin):
211
214
  """
212
215
 
213
216
  while True:
217
+ new_1psidts: str | None = None
214
218
  try:
215
219
  new_1psidts = await rotate_1psidts(self.cookies, self.proxy)
216
220
  except AuthError:
217
- if task := rotate_tasks.get(self.cookies["__Secure-1PSID"]):
221
+ if task := rotate_tasks.get(self.cookies.get("__Secure-1PSID", "")):
218
222
  task.cancel()
219
223
  logger.warning(
220
- "Failed to refresh cookies. Background auto refresh task canceled."
224
+ "AuthError: Failed to refresh cookies. Auto refresh task canceled."
221
225
  )
226
+ return
227
+ except Exception as exc:
228
+ logger.warning(f"Unexpected error while refreshing cookies: {exc}")
222
229
 
223
- logger.debug(f"Cookies refreshed. New __Secure-1PSIDTS: {new_1psidts}")
224
230
  if new_1psidts:
225
231
  self.cookies["__Secure-1PSIDTS"] = new_1psidts
232
+ if self.running:
233
+ self.client.cookies.set("__Secure-1PSIDTS", new_1psidts)
234
+ logger.debug("Cookies refreshed. New __Secure-1PSIDTS applied.")
235
+
226
236
  await asyncio.sleep(self.refresh_interval)
227
237
 
228
238
  @running(retry=2)
@@ -335,18 +345,24 @@ class GeminiClient(GemMixin):
335
345
  f"Failed to generate contents. Request failed with status code {response.status_code}"
336
346
  )
337
347
  else:
348
+ response_json: list[Any] = []
349
+ body: list[Any] = []
350
+ body_index = 0
351
+
338
352
  try:
339
- response_json = json.loads(response.text.split("\n")[2])
353
+ response_json = extract_json_from_response(response.text)
340
354
 
341
- body = None
342
- body_index = 0
343
355
  for part_index, part in enumerate(response_json):
344
356
  try:
345
- main_part = json.loads(part[2])
346
- if main_part[4]:
347
- body_index, body = part_index, main_part
357
+ part_body = get_nested_value(part, [2])
358
+ if not part_body:
359
+ continue
360
+
361
+ part_json = json.loads(part_body)
362
+ if get_nested_value(part_json, [4]):
363
+ body_index, body = part_index, part_json
348
364
  break
349
- except (IndexError, TypeError, ValueError):
365
+ except (TypeError, ValueError):
350
366
  continue
351
367
 
352
368
  if not body:
@@ -355,7 +371,8 @@ class GeminiClient(GemMixin):
355
371
  await self.close()
356
372
 
357
373
  try:
358
- match ErrorCode(response_json[0][5][2][0][1][0]):
374
+ error_code = get_nested_value(response_json, [0, 5, 2, 0, 1, 0], -1)
375
+ match ErrorCode(error_code):
359
376
  case ErrorCode.USAGE_LIMIT_EXCEEDED:
360
377
  raise UsageLimitExceeded(
361
378
  f"Failed to generate contents. Usage limit of {model.model_name} model has exceeded. Please try switching to another model."
@@ -385,44 +402,51 @@ class GeminiClient(GemMixin):
385
402
  )
386
403
 
387
404
  try:
388
- candidates = []
389
- for candidate_index, candidate in enumerate(body[4]):
390
- text = candidate[1][0]
405
+ candidate_list: list[Any] = get_nested_value(body, [4], [])
406
+ output_candidates: list[Candidate] = []
407
+
408
+ for candidate_index, candidate in enumerate(candidate_list):
409
+ rcid = get_nested_value(candidate, [0])
410
+ if not rcid:
411
+ continue # Skip candidate if it has no rcid
412
+
413
+ # Text output and thoughts
414
+ text = get_nested_value(candidate, [1, 0], "")
391
415
  if re.match(
392
416
  r"^http://googleusercontent\.com/card_content/\d+", text
393
417
  ):
394
- text = candidate[22] and candidate[22][0] or text
418
+ text = get_nested_value(candidate, [22, 0]) or text
395
419
 
396
- try:
397
- thoughts = candidate[37][0][0]
398
- except (TypeError, IndexError):
399
- thoughts = None
400
-
401
- web_images = (
402
- candidate[12]
403
- and candidate[12][1]
404
- and [
420
+ thoughts = get_nested_value(candidate, [37, 0, 0])
421
+
422
+ # Web images
423
+ web_images = []
424
+ for web_img_data in get_nested_value(candidate, [12, 1], []):
425
+ url = get_nested_value(web_img_data, [0, 0, 0])
426
+ if not url:
427
+ continue
428
+
429
+ web_images.append(
405
430
  WebImage(
406
- url=web_image[0][0][0],
407
- title=web_image[7][0],
408
- alt=web_image[0][4],
431
+ url=url,
432
+ title=get_nested_value(web_img_data, [7, 0], ""),
433
+ alt=get_nested_value(web_img_data, [0, 4], ""),
409
434
  proxy=self.proxy,
410
435
  )
411
- for web_image in candidate[12][1]
412
- ]
413
- or []
414
- )
436
+ )
415
437
 
438
+ # Generated images
416
439
  generated_images = []
417
- if candidate[12] and candidate[12][7] and candidate[12][7][0]:
440
+ if get_nested_value(candidate, [12, 7, 0]):
418
441
  img_body = None
419
442
  for img_part_index, part in enumerate(response_json):
420
443
  if img_part_index < body_index:
421
444
  continue
422
-
423
445
  try:
424
446
  img_part = json.loads(part[2])
425
- if img_part[4][candidate_index][12][7][0]:
447
+ if get_nested_value(
448
+ img_part, [4, candidate_index, 12, 7, 0]
449
+ ):
426
450
  img_body = img_part
427
451
  break
428
452
  except (IndexError, TypeError, ValueError):
@@ -434,57 +458,73 @@ class GeminiClient(GemMixin):
434
458
  "If the error persists and is caused by the package, please report it on GitHub."
435
459
  )
436
460
 
437
- img_candidate = img_body[4][candidate_index]
438
-
439
- text = re.sub(
440
- r"http://googleusercontent\.com/image_generation_content/\d+",
441
- "",
442
- img_candidate[1][0],
443
- ).rstrip()
444
-
445
- generated_images = [
446
- GeneratedImage(
447
- url=generated_image[0][3][3],
448
- title=(
449
- f"[Generated Image {generated_image[3][6]}]"
450
- if generated_image[3][6]
451
- else "[Generated Image]"
452
- ),
453
- alt=(
454
- generated_image[3][5][image_index]
455
- if generated_image[3][5]
456
- and len(generated_image[3][5]) > image_index
457
- else (
458
- generated_image[3][5][0]
459
- if generated_image[3][5]
460
- else ""
461
- )
462
- ),
463
- proxy=self.proxy,
464
- cookies=self.cookies,
461
+ img_candidate = get_nested_value(
462
+ img_body, [4, candidate_index], []
463
+ )
464
+
465
+ if finished_text := get_nested_value(
466
+ img_candidate, [1, 0]
467
+ ): # Only overwrite if new text is returned after image generation
468
+ text = re.sub(
469
+ r"http://googleusercontent\.com/image_generation_content/\d+",
470
+ "",
471
+ finished_text,
472
+ ).rstrip()
473
+
474
+ for img_index, gen_img_data in enumerate(
475
+ get_nested_value(img_candidate, [12, 7, 0], [])
476
+ ):
477
+ url = get_nested_value(gen_img_data, [0, 3, 3])
478
+ if not url:
479
+ continue
480
+
481
+ img_num = get_nested_value(gen_img_data, [3, 6])
482
+ title = (
483
+ f"[Generated Image {img_num}]"
484
+ if img_num
485
+ else "[Generated Image]"
465
486
  )
466
- for image_index, generated_image in enumerate(
467
- img_candidate[12][7][0]
487
+
488
+ alt_list = get_nested_value(gen_img_data, [3, 5], [])
489
+ alt = (
490
+ get_nested_value(alt_list, [img_index])
491
+ or get_nested_value(alt_list, [0])
492
+ or ""
468
493
  )
469
- ]
470
494
 
471
- candidates.append(
495
+ generated_images.append(
496
+ GeneratedImage(
497
+ url=url,
498
+ title=title,
499
+ alt=alt,
500
+ proxy=self.proxy,
501
+ cookies=self.cookies,
502
+ )
503
+ )
504
+
505
+ output_candidates.append(
472
506
  Candidate(
473
- rcid=candidate[0],
507
+ rcid=rcid,
474
508
  text=text,
475
509
  thoughts=thoughts,
476
510
  web_images=web_images,
477
511
  generated_images=generated_images,
478
512
  )
479
513
  )
480
- if not candidates:
514
+
515
+ if not output_candidates:
481
516
  raise GeminiError(
482
517
  "Failed to generate contents. No output data found in response."
483
518
  )
484
519
 
485
- output = ModelOutput(metadata=body[1], candidates=candidates)
486
- except (TypeError, IndexError):
487
- logger.debug(f"Invalid response: {response.text}")
520
+ output = ModelOutput(
521
+ metadata=get_nested_value(body, [1], []),
522
+ candidates=output_candidates,
523
+ )
524
+ except (TypeError, IndexError) as e:
525
+ logger.debug(
526
+ f"{type(e).__name__}: {e}; Invalid response structure: {response.text}"
527
+ )
488
528
  raise APIError(
489
529
  "Failed to parse response body. Data structure is invalid."
490
530
  )
@@ -43,9 +43,14 @@ class Headers(Enum):
43
43
 
44
44
  class Model(Enum):
45
45
  UNSPECIFIED = ("unspecified", {}, False)
46
+ G_3_0_PRO = (
47
+ "gemini-3.0-pro",
48
+ {"x-goog-ext-525001261-jspb": '[1,null,null,null,"9d8ca3786ebdfbea",null,null,null,[4]]'},
49
+ False,
50
+ )
46
51
  G_2_5_FLASH = (
47
52
  "gemini-2.5-flash",
48
- {"x-goog-ext-525001261-jspb": '[1,null,null,null,"71c2d248d3b102ff",null,null,0,[4]]'},
53
+ {"x-goog-ext-525001261-jspb": '[1,null,null,null,"9ec249fc9ad08861",null,null,0,[4]]'},
49
54
  False,
50
55
  )
51
56
  G_2_5_PRO = (
@@ -53,16 +58,6 @@ class Model(Enum):
53
58
  {"x-goog-ext-525001261-jspb": '[1,null,null,null,"4af6c7f5da75d65d",null,null,0,[4]]'},
54
59
  False,
55
60
  )
56
- G_2_0_FLASH = (
57
- "gemini-2.0-flash",
58
- {"x-goog-ext-525001261-jspb": '[1,null,null,null,"f299729663a2343f"]'},
59
- False,
60
- ) # Deprecated
61
- G_2_0_FLASH_THINKING = (
62
- "gemini-2.0-flash-thinking",
63
- {"x-goog-ext-525001261-jspb": '[null,null,null,null,"7ca48d02d802f20a"]'},
64
- False,
65
- ) # Deprecated
66
61
 
67
62
  def __init__(self, name, header, advanced_only):
68
63
  self.model_name = name
@@ -3,11 +3,12 @@
3
3
  from asyncio import Task
4
4
 
5
5
  from .decorators import running
6
- from .upload_file import upload_file, parse_file_name
7
- from .rotate_1psidts import rotate_1psidts
8
6
  from .get_access_token import get_access_token
9
7
  from .load_browser_cookies import load_browser_cookies
10
8
  from .logger import logger, set_log_level
9
+ from .parsing import extract_json_from_response, get_nested_value
10
+ from .rotate_1psidts import rotate_1psidts
11
+ from .upload_file import upload_file, parse_file_name
11
12
 
12
13
 
13
14
  rotate_tasks: dict[str, Task] = {}
@@ -0,0 +1,79 @@
1
+ from typing import Any
2
+
3
+ import orjson as json
4
+
5
+ from .logger import logger
6
+
7
+
8
+ def get_nested_value(data: list, path: list[int], default: Any = None) -> Any:
9
+ """
10
+ Safely get a value from a nested list by a sequence of indices.
11
+
12
+ Parameters
13
+ ----------
14
+ data: `list`
15
+ The nested list to traverse.
16
+ path: `list[int]`
17
+ A list of indices representing the path to the desired value.
18
+ default: `Any`, optional
19
+ The default value to return if the path is not found.
20
+ """
21
+
22
+ current = data
23
+
24
+ for i, key in enumerate(path):
25
+ try:
26
+ current = current[key]
27
+ except (IndexError, TypeError, KeyError) as e:
28
+ current_repr = repr(current)
29
+ if len(current_repr) > 200:
30
+ current_repr = f"{current_repr[:197]}..."
31
+
32
+ logger.debug(
33
+ f"{type(e).__name__}: parsing failed at path {path} (index {i}, key '{key}') "
34
+ f"while attempting to get value from `{current_repr}`"
35
+ )
36
+ return default
37
+
38
+ if current is None and default is not None:
39
+ return default
40
+
41
+ return current
42
+
43
+
44
+ def extract_json_from_response(text: str) -> list:
45
+ """
46
+ Clean and extract the JSON content from a Google API response.
47
+
48
+ Parameters
49
+ ----------
50
+ text: `str`
51
+ The raw response text from a Google API.
52
+
53
+ Returns
54
+ -------
55
+ `list`
56
+ The extracted JSON array or object (should be an array).
57
+
58
+ Raises
59
+ ------
60
+ `TypeError`
61
+ If the input is not a string.
62
+ `ValueError`
63
+ If no JSON object is found or the response is empty.
64
+ """
65
+
66
+ if not isinstance(text, str):
67
+ raise TypeError(
68
+ f"Input text is expected to be a string, got {type(text).__name__} instead."
69
+ )
70
+
71
+ # Find the first line which is valid JSON
72
+ for line in text.splitlines():
73
+ try:
74
+ return json.loads(line.strip())
75
+ except json.JSONDecodeError:
76
+ continue
77
+
78
+ # If no JSON is found, raise ValueError
79
+ raise ValueError("Could not find a valid JSON object or array in the response.")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gemini-webapi
3
- Version: 1.16.0
3
+ Version: 1.17.0
4
4
  Summary: ✨ An elegant async Python wrapper for Google Gemini web app
5
5
  Author: UZQueen
6
6
  License: GNU AFFERO GENERAL PUBLIC LICENSE
@@ -905,16 +905,12 @@ asyncio.run(main())
905
905
 
906
906
  You can specify which language model to use by passing `model` argument to `GeminiClient.generate_content` or `GeminiClient.start_chat`. The default value is `unspecified`.
907
907
 
908
- Currently available models (as of June 12, 2025):
908
+ Currently available models (as of November 20, 2025):
909
909
 
910
910
  - `unspecified` - Default model
911
+ - `gemini-3.0-pro` - Gemini 3.0 Pro
912
+ - `gemini-2.5-pro` - Gemini 2.5 Pro
911
913
  - `gemini-2.5-flash` - Gemini 2.5 Flash
912
- - `gemini-2.5-pro` - Gemini 2.5 Pro (daily usage limit imposed)
913
-
914
- Deprecated models (yet still working):
915
-
916
- - `gemini-2.0-flash` - Gemini 2.0 Flash
917
- - `gemini-2.0-flash-thinking` - Gemini 2.0 Flash Thinking
918
914
 
919
915
  ```python
920
916
  from gemini_webapi.constants import Model
@@ -1,6 +1,6 @@
1
1
  gemini_webapi/__init__.py,sha256=7ELCiUoI10ea3daeJxnv0UwqLVKpM7rxsgOZsPMstO8,150
2
- gemini_webapi/client.py,sha256=YlgBiIQWkxCjPaqMQ1Zs-MvRz6ww0U3uqdS__azqvwA,26827
3
- gemini_webapi/constants.py,sha256=ZzyZRqwJsqjvDdj3GYIdk0SpQVbKa1Vd5NwEsvIz6pI,2685
2
+ gemini_webapi/client.py,sha256=9hcf0MOSiYbysv9rtcPm6wsRnb2aRLv8c8LL9Kxf8k4,28476
3
+ gemini_webapi/constants.py,sha256=frgypJCzQvbtfNxYAOntyc9WSP0MiQ_RpsK4bJ-N6VI,2502
4
4
  gemini_webapi/exceptions.py,sha256=qkXrIpr0L7LtGbq3VcTO8D1xZ50pJtt0dDRp5I3uDSg,1038
5
5
  gemini_webapi/components/__init__.py,sha256=wolxuAJJ32-jmHOKgpsesexP7hXea1JMo5vI52wysTI,48
6
6
  gemini_webapi/components/gem_mixin.py,sha256=WPJkYDS4yQpLMBNQ94LQo5w59RgkllWaSiHsFG1k5GU,8795
@@ -10,15 +10,16 @@ gemini_webapi/types/gem.py,sha256=3Ppjq9V22Zp4Lb9a9ZnDviDKQpfSQf8UZxqOEjeEWd4,40
10
10
  gemini_webapi/types/grpc.py,sha256=S64h1oeC7ZJC50kmS_C2CQ7WVTanhJ4kqTFx5ZYayXI,917
11
11
  gemini_webapi/types/image.py,sha256=NhW1QY2Agzb6UbrIjZLnqxqukabDSGbmoCsVguzko10,5395
12
12
  gemini_webapi/types/modeloutput.py,sha256=h07kQOkL5r-oPLvZ59uVtO1eP4FGy5ZpzuYQzAeQdr8,1196
13
- gemini_webapi/utils/__init__.py,sha256=RIU1MBqYNjih0XMMt7wCxAA-X9KVF53nDbAjaxaGTo8,352
13
+ gemini_webapi/utils/__init__.py,sha256=k8hV2zn6tD_BEpd1Xya6ED0deijsmzb1e9XxdFhJzIE,418
14
14
  gemini_webapi/utils/decorators.py,sha256=AuY6sU1_6_ZqeL92dTAi3eRPQ7zubB5VuBZEiz16aBM,1741
15
15
  gemini_webapi/utils/get_access_token.py,sha256=VjrHW8VMN3LPs6zCdXQtraWygurOjTY0SJZhe49aQwc,7265
16
16
  gemini_webapi/utils/load_browser_cookies.py,sha256=OHCfe27DpV_rloIDgW9Xpeb0mkfzbYONNiholw0ElXU,1791
17
17
  gemini_webapi/utils/logger.py,sha256=0VcxhVLhHBRDQutNCpapP1y_MhPoQ2ud1uIFLqxC3Z8,958
18
+ gemini_webapi/utils/parsing.py,sha256=z-t0bDbXVIa0-3_ZmTK19PvL6zBM5vuy56z2jv1Yu1I,2105
18
19
  gemini_webapi/utils/rotate_1psidts.py,sha256=XjEeQnZS3ZI6wOl0Zb5CvsbIrg0BVVNas7cE6f3x_XE,1802
19
20
  gemini_webapi/utils/upload_file.py,sha256=SJOMr6kryK_ClrKmqI96fqZBNFOMPsyAvFINAGAU3rk,1468
20
- gemini_webapi-1.16.0.dist-info/licenses/LICENSE,sha256=hIahDEOTzuHCU5J2nd07LWwkLW7Hko4UFO__ffsvB-8,34523
21
- gemini_webapi-1.16.0.dist-info/METADATA,sha256=vRnA79JpF9eMcgjzCqbpok_UGTvw5EPYKRO1bFtjZ5Q,61426
22
- gemini_webapi-1.16.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
23
- gemini_webapi-1.16.0.dist-info/top_level.txt,sha256=dtWtug_ZrmnUqCYuu8NmGzTgWglHeNzhHU_hXmqZGWE,14
24
- gemini_webapi-1.16.0.dist-info/RECORD,,
21
+ gemini_webapi-1.17.0.dist-info/licenses/LICENSE,sha256=hIahDEOTzuHCU5J2nd07LWwkLW7Hko4UFO__ffsvB-8,34523
22
+ gemini_webapi-1.17.0.dist-info/METADATA,sha256=EuMINxma9gp-2YC-cd2w_mOecbBNAyJzUcIiZ-oT_1A,61299
23
+ gemini_webapi-1.17.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
24
+ gemini_webapi-1.17.0.dist-info/top_level.txt,sha256=dtWtug_ZrmnUqCYuu8NmGzTgWglHeNzhHU_hXmqZGWE,14
25
+ gemini_webapi-1.17.0.dist-info/RECORD,,