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 +123 -94
- gemini_webapi/constants.py +7 -11
- gemini_webapi/utils/__init__.py +3 -2
- gemini_webapi/utils/get_access_token.py +37 -9
- gemini_webapi/utils/load_browser_cookies.py +10 -4
- gemini_webapi/utils/parsing.py +79 -0
- gemini_webapi/utils/rotate_1psidts.py +5 -1
- {gemini_webapi-1.15.2.dist-info → gemini_webapi-1.17.0.dist-info}/METADATA +16 -18
- {gemini_webapi-1.15.2.dist-info → gemini_webapi-1.17.0.dist-info}/RECORD +12 -11
- {gemini_webapi-1.15.2.dist-info → gemini_webapi-1.17.0.dist-info}/WHEEL +0 -0
- {gemini_webapi-1.15.2.dist-info → gemini_webapi-1.17.0.dist-info}/licenses/LICENSE +0 -0
- {gemini_webapi-1.15.2.dist-info → gemini_webapi-1.17.0.dist-info}/top_level.txt +0 -0
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
|
-
|
|
16
|
-
TimeoutError,
|
|
14
|
+
AuthError,
|
|
17
15
|
GeminiError,
|
|
18
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
221
|
+
if task := rotate_tasks.get(self.cookies.get("__Secure-1PSID", "")):
|
|
229
222
|
task.cancel()
|
|
230
223
|
logger.warning(
|
|
231
|
-
"Failed to refresh cookies.
|
|
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 =
|
|
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
|
-
|
|
357
|
-
if
|
|
358
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
400
|
-
|
|
401
|
-
|
|
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
|
|
418
|
+
text = get_nested_value(candidate, [22, 0]) or text
|
|
406
419
|
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
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=
|
|
418
|
-
title=
|
|
419
|
-
alt=
|
|
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
|
-
|
|
423
|
-
]
|
|
424
|
-
or []
|
|
425
|
-
)
|
|
436
|
+
)
|
|
426
437
|
|
|
438
|
+
# Generated images
|
|
427
439
|
generated_images = []
|
|
428
|
-
if candidate[12
|
|
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
|
|
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 =
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
img_candidate[1
|
|
454
|
-
)
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
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
|
-
|
|
478
|
-
|
|
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
|
-
|
|
505
|
+
output_candidates.append(
|
|
483
506
|
Candidate(
|
|
484
|
-
rcid=
|
|
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
|
-
|
|
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(
|
|
497
|
-
|
|
498
|
-
|
|
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
|
)
|
gemini_webapi/constants.py
CHANGED
|
@@ -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,"
|
|
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
|
gemini_webapi/utils/__init__.py
CHANGED
|
@@ -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 =
|
|
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
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
|
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
|
-
|
|
39
|
-
|
|
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 =
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
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
|
-
|
|
787
|
-
|
|
788
|
-
|
|
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
|
|
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
|
|
1065
|
+
### Generate and edit images
|
|
1068
1066
|
|
|
1069
|
-
You can ask Gemini to generate and
|
|
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
|
|
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=
|
|
3
|
-
gemini_webapi/constants.py,sha256=
|
|
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=
|
|
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=
|
|
16
|
-
gemini_webapi/utils/load_browser_cookies.py,sha256=
|
|
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/
|
|
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.
|
|
21
|
-
gemini_webapi-1.
|
|
22
|
-
gemini_webapi-1.
|
|
23
|
-
gemini_webapi-1.
|
|
24
|
-
gemini_webapi-1.
|
|
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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|