gemini-webapi 1.15.2__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,32 +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
- load_browser_cookies,
36
- running,
37
37
  rotate_tasks,
38
- logger,
38
+ running,
39
+ upload_file,
39
40
  )
40
41
 
41
42
 
@@ -43,7 +44,7 @@ class GeminiClient(GemMixin):
43
44
  """
44
45
  Async httpx client interface for gemini.google.com.
45
46
 
46
- `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
47
48
  you have logged in to google.com in your local browser.
48
49
 
49
50
  Parameters
@@ -101,20 +102,10 @@ class GeminiClient(GemMixin):
101
102
  self.refresh_interval: float = 540
102
103
  self.kwargs = kwargs
103
104
 
104
- # Validate cookies
105
105
  if secure_1psid:
106
106
  self.cookies["__Secure-1PSID"] = secure_1psid
107
107
  if secure_1psidts:
108
108
  self.cookies["__Secure-1PSIDTS"] = secure_1psidts
109
- else:
110
- try:
111
- cookies = load_browser_cookies(domain_name="google.com")
112
- if not (cookies and cookies.get("__Secure-1PSID")):
113
- raise ValueError(
114
- "Failed to load cookies from local browser. Please pass cookie values manually."
115
- )
116
- except ImportError:
117
- pass
118
109
 
119
110
  async def init(
120
111
  self,
@@ -214,6 +205,7 @@ class GeminiClient(GemMixin):
214
205
  if self.close_task:
215
206
  self.close_task.cancel()
216
207
  self.close_task = None
208
+
217
209
  self.close_task = asyncio.create_task(self.close(self.close_delay))
218
210
 
219
211
  async def start_auto_refresh(self) -> None:
@@ -222,18 +214,25 @@ class GeminiClient(GemMixin):
222
214
  """
223
215
 
224
216
  while True:
217
+ new_1psidts: str | None = None
225
218
  try:
226
219
  new_1psidts = await rotate_1psidts(self.cookies, self.proxy)
227
220
  except AuthError:
228
- if task := rotate_tasks.get(self.cookies["__Secure-1PSID"]):
221
+ if task := rotate_tasks.get(self.cookies.get("__Secure-1PSID", "")):
229
222
  task.cancel()
230
223
  logger.warning(
231
- "Failed to refresh cookies. Background auto refresh task canceled."
224
+ "AuthError: Failed to refresh cookies. Auto refresh task canceled."
232
225
  )
226
+ return
227
+ except Exception as exc:
228
+ logger.warning(f"Unexpected error while refreshing cookies: {exc}")
233
229
 
234
- logger.debug(f"Cookies refreshed. New __Secure-1PSIDTS: {new_1psidts}")
235
230
  if new_1psidts:
236
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
+
237
236
  await asyncio.sleep(self.refresh_interval)
238
237
 
239
238
  @running(retry=2)
@@ -346,18 +345,24 @@ class GeminiClient(GemMixin):
346
345
  f"Failed to generate contents. Request failed with status code {response.status_code}"
347
346
  )
348
347
  else:
348
+ response_json: list[Any] = []
349
+ body: list[Any] = []
350
+ body_index = 0
351
+
349
352
  try:
350
- response_json = json.loads(response.text.split("\n")[2])
353
+ response_json = extract_json_from_response(response.text)
351
354
 
352
- body = None
353
- body_index = 0
354
355
  for part_index, part in enumerate(response_json):
355
356
  try:
356
- main_part = json.loads(part[2])
357
- if main_part[4]:
358
- 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
359
364
  break
360
- except (IndexError, TypeError, ValueError):
365
+ except (TypeError, ValueError):
361
366
  continue
362
367
 
363
368
  if not body:
@@ -366,7 +371,8 @@ class GeminiClient(GemMixin):
366
371
  await self.close()
367
372
 
368
373
  try:
369
- 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):
370
376
  case ErrorCode.USAGE_LIMIT_EXCEEDED:
371
377
  raise UsageLimitExceeded(
372
378
  f"Failed to generate contents. Usage limit of {model.model_name} model has exceeded. Please try switching to another model."
@@ -396,44 +402,51 @@ class GeminiClient(GemMixin):
396
402
  )
397
403
 
398
404
  try:
399
- candidates = []
400
- for candidate_index, candidate in enumerate(body[4]):
401
- 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], "")
402
415
  if re.match(
403
416
  r"^http://googleusercontent\.com/card_content/\d+", text
404
417
  ):
405
- text = candidate[22] and candidate[22][0] or text
418
+ text = get_nested_value(candidate, [22, 0]) or text
406
419
 
407
- try:
408
- thoughts = candidate[37][0][0]
409
- except (TypeError, IndexError):
410
- thoughts = None
411
-
412
- web_images = (
413
- candidate[12]
414
- and candidate[12][1]
415
- 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(
416
430
  WebImage(
417
- url=web_image[0][0][0],
418
- title=web_image[7][0],
419
- 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], ""),
420
434
  proxy=self.proxy,
421
435
  )
422
- for web_image in candidate[12][1]
423
- ]
424
- or []
425
- )
436
+ )
426
437
 
438
+ # Generated images
427
439
  generated_images = []
428
- if candidate[12] and candidate[12][7] and candidate[12][7][0]:
440
+ if get_nested_value(candidate, [12, 7, 0]):
429
441
  img_body = None
430
442
  for img_part_index, part in enumerate(response_json):
431
443
  if img_part_index < body_index:
432
444
  continue
433
-
434
445
  try:
435
446
  img_part = json.loads(part[2])
436
- if img_part[4][candidate_index][12][7][0]:
447
+ if get_nested_value(
448
+ img_part, [4, candidate_index, 12, 7, 0]
449
+ ):
437
450
  img_body = img_part
438
451
  break
439
452
  except (IndexError, TypeError, ValueError):
@@ -445,57 +458,73 @@ class GeminiClient(GemMixin):
445
458
  "If the error persists and is caused by the package, please report it on GitHub."
446
459
  )
447
460
 
448
- img_candidate = img_body[4][candidate_index]
449
-
450
- text = re.sub(
451
- r"http://googleusercontent\.com/image_generation_content/\d+",
452
- "",
453
- img_candidate[1][0],
454
- ).rstrip()
455
-
456
- generated_images = [
457
- GeneratedImage(
458
- url=generated_image[0][3][3],
459
- title=(
460
- f"[Generated Image {generated_image[3][6]}]"
461
- if generated_image[3][6]
462
- else "[Generated Image]"
463
- ),
464
- alt=(
465
- generated_image[3][5][image_index]
466
- if generated_image[3][5]
467
- and len(generated_image[3][5]) > image_index
468
- else (
469
- generated_image[3][5][0]
470
- if generated_image[3][5]
471
- else ""
472
- )
473
- ),
474
- proxy=self.proxy,
475
- 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]"
486
+ )
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 ""
476
493
  )
477
- for image_index, generated_image in enumerate(
478
- img_candidate[12][7][0]
494
+
495
+ generated_images.append(
496
+ GeneratedImage(
497
+ url=url,
498
+ title=title,
499
+ alt=alt,
500
+ proxy=self.proxy,
501
+ cookies=self.cookies,
502
+ )
479
503
  )
480
- ]
481
504
 
482
- candidates.append(
505
+ output_candidates.append(
483
506
  Candidate(
484
- rcid=candidate[0],
507
+ rcid=rcid,
485
508
  text=text,
486
509
  thoughts=thoughts,
487
510
  web_images=web_images,
488
511
  generated_images=generated_images,
489
512
  )
490
513
  )
491
- if not candidates:
514
+
515
+ if not output_candidates:
492
516
  raise GeminiError(
493
517
  "Failed to generate contents. No output data found in response."
494
518
  )
495
519
 
496
- output = ModelOutput(metadata=body[1], candidates=candidates)
497
- except (TypeError, IndexError):
498
- 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
+ )
499
528
  raise APIError(
500
529
  "Failed to parse response body. Data structure is invalid."
501
530
  )
@@ -16,6 +16,7 @@ class GRPC(StrEnum):
16
16
  """
17
17
 
18
18
  # Chat methods
19
+ LIST_CHATS = "MaZiqc"
19
20
  READ_CHAT = "hNvQHb"
20
21
 
21
22
  # Gem methods
@@ -42,9 +43,14 @@ class Headers(Enum):
42
43
 
43
44
  class Model(Enum):
44
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
+ )
45
51
  G_2_5_FLASH = (
46
52
  "gemini-2.5-flash",
47
- {"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]]'},
48
54
  False,
49
55
  )
50
56
  G_2_5_PRO = (
@@ -52,16 +58,6 @@ class Model(Enum):
52
58
  {"x-goog-ext-525001261-jspb": '[1,null,null,null,"4af6c7f5da75d65d",null,null,0,[4]]'},
53
59
  False,
54
60
  )
55
- G_2_0_FLASH = (
56
- "gemini-2.0-flash",
57
- {"x-goog-ext-525001261-jspb": '[1,null,null,null,"f299729663a2343f"]'},
58
- False,
59
- ) # Deprecated
60
- G_2_0_FLASH_THINKING = (
61
- "gemini-2.0-flash-thinking",
62
- {"x-goog-ext-525001261-jspb": '[null,null,null,null,"7ca48d02d802f20a"]'},
63
- False,
64
- ) # Deprecated
65
61
 
66
62
  def __init__(self, name, header, advanced_only):
67
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] = {}
@@ -1,3 +1,4 @@
1
+ import os
1
2
  import re
2
3
  import asyncio
3
4
  from asyncio import Task
@@ -88,7 +89,11 @@ async def get_access_token(
88
89
  )
89
90
 
90
91
  # Cached cookies in local file
91
- cache_dir = Path(__file__).parent / "temp"
92
+ cache_dir = (
93
+ (GEMINI_COOKIE_PATH := os.getenv("GEMINI_COOKIE_PATH"))
94
+ and Path(GEMINI_COOKIE_PATH)
95
+ or (Path(__file__).parent / "temp")
96
+ )
92
97
  if "__Secure-1PSID" in base_cookies:
93
98
  filename = f".cached_1psidts_{base_cookies['__Secure-1PSID']}.txt"
94
99
  cache_file = cache_dir / filename
@@ -126,17 +131,35 @@ async def get_access_token(
126
131
 
127
132
  # Browser cookies (if browser-cookie3 is installed)
128
133
  try:
134
+ valid_browser_cookies = 0
129
135
  browser_cookies = load_browser_cookies(
130
136
  domain_name="google.com", verbose=verbose
131
137
  )
132
- if browser_cookies and (secure_1psid := browser_cookies.get("__Secure-1PSID")):
133
- local_cookies = {"__Secure-1PSID": secure_1psid}
134
- if secure_1psidts := browser_cookies.get("__Secure-1PSIDTS"):
135
- local_cookies["__Secure-1PSIDTS"] = secure_1psidts
136
- if nid := browser_cookies.get("NID"):
137
- local_cookies["NID"] = nid
138
- tasks.append(Task(send_request(local_cookies, proxy=proxy)))
139
- elif verbose:
138
+ if browser_cookies:
139
+ for browser, cookies in browser_cookies.items():
140
+ if secure_1psid := cookies.get("__Secure-1PSID"):
141
+ if (
142
+ "__Secure-1PSID" in base_cookies
143
+ and base_cookies["__Secure-1PSID"] != secure_1psid
144
+ ):
145
+ if verbose:
146
+ logger.debug(
147
+ f"Skipping loading local browser cookies from {browser}. "
148
+ f"__Secure-1PSID does not match the one provided."
149
+ )
150
+ continue
151
+
152
+ local_cookies = {"__Secure-1PSID": secure_1psid}
153
+ if secure_1psidts := cookies.get("__Secure-1PSIDTS"):
154
+ local_cookies["__Secure-1PSIDTS"] = secure_1psidts
155
+ if nid := cookies.get("NID"):
156
+ local_cookies["NID"] = nid
157
+ tasks.append(Task(send_request(local_cookies, proxy=proxy)))
158
+ valid_browser_cookies += 1
159
+ if verbose:
160
+ logger.debug(f"Loaded local browser cookies from {browser}")
161
+
162
+ if valid_browser_cookies == 0 and verbose:
140
163
  logger.debug(
141
164
  "Skipping loading local browser cookies. Login to gemini.google.com in your browser first."
142
165
  )
@@ -149,6 +172,11 @@ async def get_access_token(
149
172
  if verbose:
150
173
  logger.warning(f"Skipping loading local browser cookies. {e}")
151
174
 
175
+ if not tasks:
176
+ raise AuthError(
177
+ "No valid cookies available for initialization. Please pass __Secure-1PSID and __Secure-1PSIDTS manually."
178
+ )
179
+
152
180
  for i, future in enumerate(asyncio.as_completed(tasks)):
153
181
  try:
154
182
  response, request_cookies = await future
@@ -1,3 +1,5 @@
1
+ from http.cookiejar import CookieJar
2
+
1
3
  from .logger import logger
2
4
 
3
5
 
@@ -15,8 +17,9 @@ def load_browser_cookies(domain_name: str = "", verbose=True) -> dict:
15
17
 
16
18
  Returns
17
19
  -------
18
- `dict`
19
- Dictionary with cookie name as key and cookie value as value.
20
+ `dict[str, dict]`
21
+ Dictionary with browser as keys and their cookies for the specified domain as values.
22
+ Only browsers that have cookies for the specified domain will be included.
20
23
  """
21
24
 
22
25
  import browser_cookie3 as bc3
@@ -35,8 +38,11 @@ def load_browser_cookies(domain_name: str = "", verbose=True) -> dict:
35
38
  bc3.safari,
36
39
  ]:
37
40
  try:
38
- for cookie in cookie_fn(domain_name=domain_name):
39
- cookies[cookie.name] = cookie.value
41
+ jar: CookieJar = cookie_fn(domain_name=domain_name)
42
+ if jar:
43
+ cookies[cookie_fn.__name__] = {
44
+ cookie.name: cookie.value for cookie in jar
45
+ }
40
46
  except bc3.BrowserCookieError:
41
47
  pass
42
48
  except PermissionError as e:
@@ -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.")
@@ -32,7 +32,11 @@ async def rotate_1psidts(cookies: dict, proxy: str | None = None) -> str:
32
32
  If request failed with other status codes.
33
33
  """
34
34
 
35
- path = Path(__file__).parent / "temp"
35
+ path = (
36
+ (GEMINI_COOKIE_PATH := os.getenv("GEMINI_COOKIE_PATH"))
37
+ and Path(GEMINI_COOKIE_PATH)
38
+ or (Path(__file__).parent / "temp")
39
+ )
36
40
  path.mkdir(parents=True, exist_ok=True)
37
41
  filename = f".cached_1psidts_{cookies['__Secure-1PSID']}.txt"
38
42
  path = path / filename
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gemini-webapi
3
- Version: 1.15.2
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
@@ -679,7 +679,7 @@ License-File: LICENSE
679
679
  Requires-Dist: httpx[http2]~=0.28.1
680
680
  Requires-Dist: loguru~=0.7.3
681
681
  Requires-Dist: orjson~=3.11.1
682
- Requires-Dist: pydantic~=2.11.5
682
+ Requires-Dist: pydantic~=2.12.2
683
683
  Dynamic: license-file
684
684
 
685
685
  <p align="center">
@@ -713,7 +713,7 @@ A reverse-engineered asynchronous python wrapper for [Google Gemini](https://gem
713
713
  ## Features
714
714
 
715
715
  - **Persistent Cookies** - Automatically refreshes cookies in background. Optimized for always-on services.
716
- - **Image Generation** - Natively supports generating and modifying images with natural language.
716
+ - **Image Generation** - Natively supports generating and editing images with natural language.
717
717
  - **System Prompt** - Supports customizing model's system prompt with [Gemini Gems](https://gemini.google.com/gems/view).
718
718
  - **Extension Support** - Supports generating contents with [Gemini extensions](https://gemini.google.com/extensions) on, like YouTube and Gmail.
719
719
  - **Classified Outputs** - Categorizes texts, thoughts, web images and AI generated images in the response.
@@ -740,7 +740,7 @@ A reverse-engineered asynchronous python wrapper for [Google Gemini](https://gem
740
740
  - [Delete a custom gem](#delete-a-custom-gem)
741
741
  - [Retrieve model's thought process](#retrieve-models-thought-process)
742
742
  - [Retrieve images in response](#retrieve-images-in-response)
743
- - [Generate images with Imagen4](#generate-images-with-imagen4)
743
+ - [Generate and edit images](#generate-and-edit-images)
744
744
  - [Generate contents with Gemini extensions](#generate-contents-with-gemini-extensions)
745
745
  - [Check and switch to other reply candidates](#check-and-switch-to-other-reply-candidates)
746
746
  - [Logging Configuration](#logging-configuration)
@@ -777,15 +777,17 @@ pip install -U browser-cookie3
777
777
 
778
778
  > [!NOTE]
779
779
  >
780
- > If your application is deployed in a containerized environment (e.g. Docker), you may want to persist the cookies with a volume to avoid re-authentication every time the container rebuilds.
780
+ > If your application is deployed in a containerized environment (e.g. Docker), you may want to persist the cookies with a volume to avoid re-authentication every time the container rebuilds. You can set `GEMINI_COOKIE_PATH` environment variable to specify the path where auto-refreshed cookies are stored. Make sure the path is writable by the application.
781
781
  >
782
782
  > Here's part of a sample `docker-compose.yml` file:
783
783
 
784
784
  ```yaml
785
785
  services:
786
- main:
787
- volumes:
788
- - ./gemini_cookies:/usr/local/lib/python3.12/site-packages/gemini_webapi/utils/temp
786
+ main:
787
+ environment:
788
+ GEMINI_COOKIE_PATH: /tmp/gemini_webapi
789
+ volumes:
790
+ - ./gemini_cookies:/tmp/gemini_webapi
789
791
  ```
790
792
 
791
793
  > [!NOTE]
@@ -903,16 +905,12 @@ asyncio.run(main())
903
905
 
904
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`.
905
907
 
906
- Currently available models (as of June 12, 2025):
908
+ Currently available models (as of November 20, 2025):
907
909
 
908
910
  - `unspecified` - Default model
911
+ - `gemini-3.0-pro` - Gemini 3.0 Pro
912
+ - `gemini-2.5-pro` - Gemini 2.5 Pro
909
913
  - `gemini-2.5-flash` - Gemini 2.5 Flash
910
- - `gemini-2.5-pro` - Gemini 2.5 Pro (daily usage limit imposed)
911
-
912
- Deprecated models (yet still working):
913
-
914
- - `gemini-2.0-flash` - Gemini 2.0 Flash
915
- - `gemini-2.0-flash-thinking` - Gemini 2.0 Flash Thinking
916
914
 
917
915
  ```python
918
916
  from gemini_webapi.constants import Model
@@ -1064,13 +1062,13 @@ async def main():
1064
1062
  asyncio.run(main())
1065
1063
  ```
1066
1064
 
1067
- ### Generate images with Imagen4
1065
+ ### Generate and edit images
1068
1066
 
1069
- You can ask Gemini to generate and modify images with Imagen4, Google's latest AI image generator, simply by natural language.
1067
+ You can ask Gemini to generate and edit images with Nano Banana, Google's latest image model, simply by natural language.
1070
1068
 
1071
1069
  > [!IMPORTANT]
1072
1070
  >
1073
- > Google has some limitations on the image generation feature in Gemini, so its availability could be different per region/account. Here's a summary copied from [official documentation](https://support.google.com/gemini/answer/14286560) (as of March 19th, 2025):
1071
+ > Google has some limitations on the image generation feature in Gemini, so its availability could be different per region/account. Here's a summary copied from [official documentation](https://support.google.com/gemini/answer/14286560) (as of Sep 10, 2025):
1074
1072
  >
1075
1073
  > > This feature’s availability in any specific Gemini app is also limited to the supported languages and countries of that app.
1076
1074
  > >
@@ -1,6 +1,6 @@
1
1
  gemini_webapi/__init__.py,sha256=7ELCiUoI10ea3daeJxnv0UwqLVKpM7rxsgOZsPMstO8,150
2
- gemini_webapi/client.py,sha256=WIAy0vTmjpDuJNhVFbNEl1g1RG8TcY11sXIjJ1rY_y4,27270
3
- gemini_webapi/constants.py,sha256=iVT429Tfz5UVc04962jxWDL_IP8KAtf6z-4-KC2qV_A,2659
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
- gemini_webapi/utils/get_access_token.py,sha256=eNn1omFO41wWXco1eM-KXR2CEi0Tb-chlph7H-PCNjg,6137
16
- gemini_webapi/utils/load_browser_cookies.py,sha256=A5n_VsB7Rm8ck5lpy856UNJEhv30l3dvQ3j0g3ln1fE,1535
15
+ gemini_webapi/utils/get_access_token.py,sha256=VjrHW8VMN3LPs6zCdXQtraWygurOjTY0SJZhe49aQwc,7265
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/rotate_1psidts.py,sha256=NyQ9OYPLBOcvpc8bodvEYDIVFrsYN0kdfc831lPEctM,1680
18
+ gemini_webapi/utils/parsing.py,sha256=z-t0bDbXVIa0-3_ZmTK19PvL6zBM5vuy56z2jv1Yu1I,2105
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.15.2.dist-info/licenses/LICENSE,sha256=hIahDEOTzuHCU5J2nd07LWwkLW7Hko4UFO__ffsvB-8,34523
21
- gemini_webapi-1.15.2.dist-info/METADATA,sha256=JEvwvyJhfIVmhqKzxxduFD6YaZQmbNXxoNd_6zSQ5SE,61279
22
- gemini_webapi-1.15.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
23
- gemini_webapi-1.15.2.dist-info/top_level.txt,sha256=dtWtug_ZrmnUqCYuu8NmGzTgWglHeNzhHU_hXmqZGWE,14
24
- gemini_webapi-1.15.2.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,,