gemini-webapi 1.16.0__tar.gz → 1.17.1__tar.gz
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-1.16.0 → gemini_webapi-1.17.1}/.github/workflows/pypi-publish.yml +2 -2
- {gemini_webapi-1.16.0 → gemini_webapi-1.17.1}/.gitignore +1 -1
- {gemini_webapi-1.16.0 → gemini_webapi-1.17.1}/PKG-INFO +26 -13
- {gemini_webapi-1.16.0 → gemini_webapi-1.17.1}/README.md +25 -12
- {gemini_webapi-1.16.0 → gemini_webapi-1.17.1}/src/gemini_webapi/client.py +147 -94
- {gemini_webapi-1.16.0 → gemini_webapi-1.17.1}/src/gemini_webapi/constants.py +32 -13
- {gemini_webapi-1.16.0 → gemini_webapi-1.17.1}/src/gemini_webapi/utils/__init__.py +3 -2
- gemini_webapi-1.17.1/src/gemini_webapi/utils/parsing.py +79 -0
- {gemini_webapi-1.16.0 → gemini_webapi-1.17.1}/src/gemini_webapi.egg-info/PKG-INFO +26 -13
- {gemini_webapi-1.16.0 → gemini_webapi-1.17.1}/src/gemini_webapi.egg-info/SOURCES.txt +1 -0
- {gemini_webapi-1.16.0 → gemini_webapi-1.17.1}/.github/dependabot.yml +0 -0
- {gemini_webapi-1.16.0 → gemini_webapi-1.17.1}/.github/workflows/github-release.yml +0 -0
- {gemini_webapi-1.16.0 → gemini_webapi-1.17.1}/.vscode/launch.json +0 -0
- {gemini_webapi-1.16.0 → gemini_webapi-1.17.1}/.vscode/settings.json +0 -0
- {gemini_webapi-1.16.0 → gemini_webapi-1.17.1}/LICENSE +0 -0
- {gemini_webapi-1.16.0 → gemini_webapi-1.17.1}/assets/banner.png +0 -0
- {gemini_webapi-1.16.0 → gemini_webapi-1.17.1}/assets/favicon.png +0 -0
- {gemini_webapi-1.16.0 → gemini_webapi-1.17.1}/assets/logo.svg +0 -0
- {gemini_webapi-1.16.0 → gemini_webapi-1.17.1}/assets/sample.pdf +0 -0
- {gemini_webapi-1.16.0 → gemini_webapi-1.17.1}/pyproject.toml +0 -0
- {gemini_webapi-1.16.0 → gemini_webapi-1.17.1}/setup.cfg +0 -0
- {gemini_webapi-1.16.0 → gemini_webapi-1.17.1}/src/gemini_webapi/__init__.py +0 -0
- {gemini_webapi-1.16.0 → gemini_webapi-1.17.1}/src/gemini_webapi/components/__init__.py +0 -0
- {gemini_webapi-1.16.0 → gemini_webapi-1.17.1}/src/gemini_webapi/components/gem_mixin.py +0 -0
- {gemini_webapi-1.16.0 → gemini_webapi-1.17.1}/src/gemini_webapi/exceptions.py +0 -0
- {gemini_webapi-1.16.0 → gemini_webapi-1.17.1}/src/gemini_webapi/types/__init__.py +0 -0
- {gemini_webapi-1.16.0 → gemini_webapi-1.17.1}/src/gemini_webapi/types/candidate.py +0 -0
- {gemini_webapi-1.16.0 → gemini_webapi-1.17.1}/src/gemini_webapi/types/gem.py +0 -0
- {gemini_webapi-1.16.0 → gemini_webapi-1.17.1}/src/gemini_webapi/types/grpc.py +0 -0
- {gemini_webapi-1.16.0 → gemini_webapi-1.17.1}/src/gemini_webapi/types/image.py +0 -0
- {gemini_webapi-1.16.0 → gemini_webapi-1.17.1}/src/gemini_webapi/types/modeloutput.py +0 -0
- {gemini_webapi-1.16.0 → gemini_webapi-1.17.1}/src/gemini_webapi/utils/decorators.py +0 -0
- {gemini_webapi-1.16.0 → gemini_webapi-1.17.1}/src/gemini_webapi/utils/get_access_token.py +0 -0
- {gemini_webapi-1.16.0 → gemini_webapi-1.17.1}/src/gemini_webapi/utils/load_browser_cookies.py +0 -0
- {gemini_webapi-1.16.0 → gemini_webapi-1.17.1}/src/gemini_webapi/utils/logger.py +0 -0
- {gemini_webapi-1.16.0 → gemini_webapi-1.17.1}/src/gemini_webapi/utils/rotate_1psidts.py +0 -0
- {gemini_webapi-1.16.0 → gemini_webapi-1.17.1}/src/gemini_webapi/utils/upload_file.py +0 -0
- {gemini_webapi-1.16.0 → gemini_webapi-1.17.1}/src/gemini_webapi.egg-info/dependency_links.txt +0 -0
- {gemini_webapi-1.16.0 → gemini_webapi-1.17.1}/src/gemini_webapi.egg-info/requires.txt +0 -0
- {gemini_webapi-1.16.0 → gemini_webapi-1.17.1}/src/gemini_webapi.egg-info/top_level.txt +0 -0
- {gemini_webapi-1.16.0 → gemini_webapi-1.17.1}/tests/test_client_features.py +0 -0
- {gemini_webapi-1.16.0 → gemini_webapi-1.17.1}/tests/test_gem_mixin.py +0 -0
- {gemini_webapi-1.16.0 → gemini_webapi-1.17.1}/tests/test_save_image.py +0 -0
|
@@ -36,7 +36,7 @@ jobs:
|
|
|
36
36
|
- name: Build package
|
|
37
37
|
run: python -m build
|
|
38
38
|
- name: Archive production artifacts
|
|
39
|
-
uses: actions/upload-artifact@
|
|
39
|
+
uses: actions/upload-artifact@v5.0.0
|
|
40
40
|
with:
|
|
41
41
|
name: dist
|
|
42
42
|
path: dist
|
|
@@ -52,7 +52,7 @@ jobs:
|
|
|
52
52
|
id-token: write # IMPORTANT: this permission is mandatory for trusted publishing
|
|
53
53
|
steps:
|
|
54
54
|
- name: Retrieve built artifacts
|
|
55
|
-
uses: actions/download-artifact@
|
|
55
|
+
uses: actions/download-artifact@v6.0.0
|
|
56
56
|
with:
|
|
57
57
|
name: dist
|
|
58
58
|
path: dist
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gemini-webapi
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.17.1
|
|
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
|
|
@@ -783,11 +783,11 @@ pip install -U browser-cookie3
|
|
|
783
783
|
|
|
784
784
|
```yaml
|
|
785
785
|
services:
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
786
|
+
main:
|
|
787
|
+
environment:
|
|
788
|
+
GEMINI_COOKIE_PATH: /tmp/gemini_webapi
|
|
789
|
+
volumes:
|
|
790
|
+
- ./gemini_cookies:/tmp/gemini_webapi
|
|
791
791
|
```
|
|
792
792
|
|
|
793
793
|
> [!NOTE]
|
|
@@ -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
|
|
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
|
|
@@ -933,6 +929,23 @@ async def main():
|
|
|
933
929
|
asyncio.run(main())
|
|
934
930
|
```
|
|
935
931
|
|
|
932
|
+
You can also pass custom model header strings directly to access models which are not listed above.
|
|
933
|
+
|
|
934
|
+
```python
|
|
935
|
+
# "model_name" and "model_header" keys must be present
|
|
936
|
+
custom_model = {
|
|
937
|
+
"model_name": "xxx",
|
|
938
|
+
"model_header": {
|
|
939
|
+
"x-goog-ext-525001261-jspb": "[1,null,null,null,'e6fa609c3fa255c0',null,null,null,[4]]"
|
|
940
|
+
},
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
response = await client.generate_content(
|
|
944
|
+
"What's your model version?",
|
|
945
|
+
model=custom_model
|
|
946
|
+
)
|
|
947
|
+
```
|
|
948
|
+
|
|
936
949
|
### Apply system prompt with Gemini Gems
|
|
937
950
|
|
|
938
951
|
System prompt can be applied to conversations via [Gemini Gems](https://gemini.google.com/gems/view). To use a gem, you can pass `gem` argument to `GeminiClient.generate_content` or `GeminiClient.start_chat`. `gem` can be either a string of gem id or a `gemini_webapi.Gem` object. Only one gem can be applied to a single conversation.
|
|
@@ -99,11 +99,11 @@ pip install -U browser-cookie3
|
|
|
99
99
|
|
|
100
100
|
```yaml
|
|
101
101
|
services:
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
102
|
+
main:
|
|
103
|
+
environment:
|
|
104
|
+
GEMINI_COOKIE_PATH: /tmp/gemini_webapi
|
|
105
|
+
volumes:
|
|
106
|
+
- ./gemini_cookies:/tmp/gemini_webapi
|
|
107
107
|
```
|
|
108
108
|
|
|
109
109
|
> [!NOTE]
|
|
@@ -221,16 +221,12 @@ asyncio.run(main())
|
|
|
221
221
|
|
|
222
222
|
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`.
|
|
223
223
|
|
|
224
|
-
Currently available models (as of
|
|
224
|
+
Currently available models (as of November 20, 2025):
|
|
225
225
|
|
|
226
226
|
- `unspecified` - Default model
|
|
227
|
+
- `gemini-3.0-pro` - Gemini 3.0 Pro
|
|
228
|
+
- `gemini-2.5-pro` - Gemini 2.5 Pro
|
|
227
229
|
- `gemini-2.5-flash` - Gemini 2.5 Flash
|
|
228
|
-
- `gemini-2.5-pro` - Gemini 2.5 Pro (daily usage limit imposed)
|
|
229
|
-
|
|
230
|
-
Deprecated models (yet still working):
|
|
231
|
-
|
|
232
|
-
- `gemini-2.0-flash` - Gemini 2.0 Flash
|
|
233
|
-
- `gemini-2.0-flash-thinking` - Gemini 2.0 Flash Thinking
|
|
234
230
|
|
|
235
231
|
```python
|
|
236
232
|
from gemini_webapi.constants import Model
|
|
@@ -249,6 +245,23 @@ async def main():
|
|
|
249
245
|
asyncio.run(main())
|
|
250
246
|
```
|
|
251
247
|
|
|
248
|
+
You can also pass custom model header strings directly to access models which are not listed above.
|
|
249
|
+
|
|
250
|
+
```python
|
|
251
|
+
# "model_name" and "model_header" keys must be present
|
|
252
|
+
custom_model = {
|
|
253
|
+
"model_name": "xxx",
|
|
254
|
+
"model_header": {
|
|
255
|
+
"x-goog-ext-525001261-jspb": "[1,null,null,null,'e6fa609c3fa255c0',null,null,null,[4]]"
|
|
256
|
+
},
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
response = await client.generate_content(
|
|
260
|
+
"What's your model version?",
|
|
261
|
+
model=custom_model
|
|
262
|
+
)
|
|
263
|
+
```
|
|
264
|
+
|
|
252
265
|
### Apply system prompt with Gemini Gems
|
|
253
266
|
|
|
254
267
|
System prompt can be applied to conversations via [Gemini Gems](https://gemini.google.com/gems/view). To use a gem, you can pass `gem` argument to `GeminiClient.generate_content` or `GeminiClient.start_chat`. `gem` can be either a string of gem id or a `gemini_webapi.Gem` object. Only one gem can be applied to a single conversation.
|
|
@@ -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
|
-
|
|
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
|
-
running,
|
|
36
37
|
rotate_tasks,
|
|
37
|
-
|
|
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
|
|
221
|
+
if task := rotate_tasks.get(self.cookies.get("__Secure-1PSID", "")):
|
|
218
222
|
task.cancel()
|
|
219
223
|
logger.warning(
|
|
220
|
-
"Failed to refresh cookies.
|
|
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)
|
|
@@ -230,7 +240,7 @@ class GeminiClient(GemMixin):
|
|
|
230
240
|
self,
|
|
231
241
|
prompt: str,
|
|
232
242
|
files: list[str | Path] | None = None,
|
|
233
|
-
model: Model | str = Model.UNSPECIFIED,
|
|
243
|
+
model: Model | str | dict = Model.UNSPECIFIED,
|
|
234
244
|
gem: Gem | str | None = None,
|
|
235
245
|
chat: Optional["ChatSession"] = None,
|
|
236
246
|
**kwargs,
|
|
@@ -244,9 +254,10 @@ class GeminiClient(GemMixin):
|
|
|
244
254
|
Prompt provided by user.
|
|
245
255
|
files: `list[str | Path]`, optional
|
|
246
256
|
List of file paths to be attached.
|
|
247
|
-
model: `Model
|
|
257
|
+
model: `Model | str | dict`, optional
|
|
248
258
|
Specify the model to use for generation.
|
|
249
|
-
Pass either a `gemini_webapi.constants.Model` enum or a model name string.
|
|
259
|
+
Pass either a `gemini_webapi.constants.Model` enum or a model name string to use predefined models.
|
|
260
|
+
Pass a dictionary to use custom model header strings ("model_name" and "model_header" keys must be provided).
|
|
250
261
|
gem: `Gem | str`, optional
|
|
251
262
|
Specify a gem to use as system prompt for the chat session.
|
|
252
263
|
Pass either a `gemini_webapi.types.Gem` object or a gem id string.
|
|
@@ -277,8 +288,15 @@ class GeminiClient(GemMixin):
|
|
|
277
288
|
|
|
278
289
|
assert prompt, "Prompt cannot be empty."
|
|
279
290
|
|
|
280
|
-
if
|
|
291
|
+
if isinstance(model, str):
|
|
281
292
|
model = Model.from_name(model)
|
|
293
|
+
elif isinstance(model, dict):
|
|
294
|
+
model = Model.from_dict(model)
|
|
295
|
+
elif not isinstance(model, Model):
|
|
296
|
+
raise TypeError(
|
|
297
|
+
f"'model' must be a `gemini_webapi.constants.Model` instance, "
|
|
298
|
+
f"string, or dictionary; got `{type(model).__name__}`"
|
|
299
|
+
)
|
|
282
300
|
|
|
283
301
|
if isinstance(gem, Gem):
|
|
284
302
|
gem_id = gem.id
|
|
@@ -335,18 +353,24 @@ class GeminiClient(GemMixin):
|
|
|
335
353
|
f"Failed to generate contents. Request failed with status code {response.status_code}"
|
|
336
354
|
)
|
|
337
355
|
else:
|
|
356
|
+
response_json: list[Any] = []
|
|
357
|
+
body: list[Any] = []
|
|
358
|
+
body_index = 0
|
|
359
|
+
|
|
338
360
|
try:
|
|
339
|
-
response_json =
|
|
361
|
+
response_json = extract_json_from_response(response.text)
|
|
340
362
|
|
|
341
|
-
body = None
|
|
342
|
-
body_index = 0
|
|
343
363
|
for part_index, part in enumerate(response_json):
|
|
344
364
|
try:
|
|
345
|
-
|
|
346
|
-
if
|
|
347
|
-
|
|
365
|
+
part_body = get_nested_value(part, [2])
|
|
366
|
+
if not part_body:
|
|
367
|
+
continue
|
|
368
|
+
|
|
369
|
+
part_json = json.loads(part_body)
|
|
370
|
+
if get_nested_value(part_json, [4]):
|
|
371
|
+
body_index, body = part_index, part_json
|
|
348
372
|
break
|
|
349
|
-
except
|
|
373
|
+
except json.JSONDecodeError:
|
|
350
374
|
continue
|
|
351
375
|
|
|
352
376
|
if not body:
|
|
@@ -355,7 +379,8 @@ class GeminiClient(GemMixin):
|
|
|
355
379
|
await self.close()
|
|
356
380
|
|
|
357
381
|
try:
|
|
358
|
-
|
|
382
|
+
error_code = get_nested_value(response_json, [0, 5, 2, 0, 1, 0], -1)
|
|
383
|
+
match ErrorCode(error_code):
|
|
359
384
|
case ErrorCode.USAGE_LIMIT_EXCEEDED:
|
|
360
385
|
raise UsageLimitExceeded(
|
|
361
386
|
f"Failed to generate contents. Usage limit of {model.model_name} model has exceeded. Please try switching to another model."
|
|
@@ -385,47 +410,58 @@ class GeminiClient(GemMixin):
|
|
|
385
410
|
)
|
|
386
411
|
|
|
387
412
|
try:
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
413
|
+
candidate_list: list[Any] = get_nested_value(body, [4], [])
|
|
414
|
+
output_candidates: list[Candidate] = []
|
|
415
|
+
|
|
416
|
+
for candidate_index, candidate in enumerate(candidate_list):
|
|
417
|
+
rcid = get_nested_value(candidate, [0])
|
|
418
|
+
if not rcid:
|
|
419
|
+
continue # Skip candidate if it has no rcid
|
|
420
|
+
|
|
421
|
+
# Text output and thoughts
|
|
422
|
+
text = get_nested_value(candidate, [1, 0], "")
|
|
391
423
|
if re.match(
|
|
392
424
|
r"^http://googleusercontent\.com/card_content/\d+", text
|
|
393
425
|
):
|
|
394
|
-
text = candidate[22
|
|
426
|
+
text = get_nested_value(candidate, [22, 0]) or text
|
|
395
427
|
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
428
|
+
thoughts = get_nested_value(candidate, [37, 0, 0])
|
|
429
|
+
|
|
430
|
+
# Web images
|
|
431
|
+
web_images = []
|
|
432
|
+
for web_img_data in get_nested_value(candidate, [12, 1], []):
|
|
433
|
+
url = get_nested_value(web_img_data, [0, 0, 0])
|
|
434
|
+
if not url:
|
|
435
|
+
continue
|
|
436
|
+
|
|
437
|
+
web_images.append(
|
|
405
438
|
WebImage(
|
|
406
|
-
url=
|
|
407
|
-
title=
|
|
408
|
-
alt=
|
|
439
|
+
url=url,
|
|
440
|
+
title=get_nested_value(web_img_data, [7, 0], ""),
|
|
441
|
+
alt=get_nested_value(web_img_data, [0, 4], ""),
|
|
409
442
|
proxy=self.proxy,
|
|
410
443
|
)
|
|
411
|
-
|
|
412
|
-
]
|
|
413
|
-
or []
|
|
414
|
-
)
|
|
444
|
+
)
|
|
415
445
|
|
|
446
|
+
# Generated images
|
|
416
447
|
generated_images = []
|
|
417
|
-
if candidate[12
|
|
448
|
+
if get_nested_value(candidate, [12, 7, 0]):
|
|
418
449
|
img_body = None
|
|
419
450
|
for img_part_index, part in enumerate(response_json):
|
|
420
451
|
if img_part_index < body_index:
|
|
421
452
|
continue
|
|
422
|
-
|
|
423
453
|
try:
|
|
424
|
-
|
|
425
|
-
if
|
|
426
|
-
|
|
454
|
+
img_part_body = get_nested_value(part, [2])
|
|
455
|
+
if not img_part_body:
|
|
456
|
+
continue
|
|
457
|
+
|
|
458
|
+
img_part_json = json.loads(img_part_body)
|
|
459
|
+
if get_nested_value(
|
|
460
|
+
img_part_json, [4, candidate_index, 12, 7, 0]
|
|
461
|
+
):
|
|
462
|
+
img_body = img_part_json
|
|
427
463
|
break
|
|
428
|
-
except
|
|
464
|
+
except json.JSONDecodeError:
|
|
429
465
|
continue
|
|
430
466
|
|
|
431
467
|
if not img_body:
|
|
@@ -434,57 +470,73 @@ class GeminiClient(GemMixin):
|
|
|
434
470
|
"If the error persists and is caused by the package, please report it on GitHub."
|
|
435
471
|
)
|
|
436
472
|
|
|
437
|
-
img_candidate =
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
img_candidate[1
|
|
443
|
-
)
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
473
|
+
img_candidate = get_nested_value(
|
|
474
|
+
img_body, [4, candidate_index], []
|
|
475
|
+
)
|
|
476
|
+
|
|
477
|
+
if finished_text := get_nested_value(
|
|
478
|
+
img_candidate, [1, 0]
|
|
479
|
+
): # Only overwrite if new text is returned after image generation
|
|
480
|
+
text = re.sub(
|
|
481
|
+
r"http://googleusercontent\.com/image_generation_content/\d+",
|
|
482
|
+
"",
|
|
483
|
+
finished_text,
|
|
484
|
+
).rstrip()
|
|
485
|
+
|
|
486
|
+
for img_index, gen_img_data in enumerate(
|
|
487
|
+
get_nested_value(img_candidate, [12, 7, 0], [])
|
|
488
|
+
):
|
|
489
|
+
url = get_nested_value(gen_img_data, [0, 3, 3])
|
|
490
|
+
if not url:
|
|
491
|
+
continue
|
|
492
|
+
|
|
493
|
+
img_num = get_nested_value(gen_img_data, [3, 6])
|
|
494
|
+
title = (
|
|
495
|
+
f"[Generated Image {img_num}]"
|
|
496
|
+
if img_num
|
|
497
|
+
else "[Generated Image]"
|
|
498
|
+
)
|
|
499
|
+
|
|
500
|
+
alt_list = get_nested_value(gen_img_data, [3, 5], [])
|
|
501
|
+
alt = (
|
|
502
|
+
get_nested_value(alt_list, [img_index])
|
|
503
|
+
or get_nested_value(alt_list, [0])
|
|
504
|
+
or ""
|
|
465
505
|
)
|
|
466
|
-
|
|
467
|
-
|
|
506
|
+
|
|
507
|
+
generated_images.append(
|
|
508
|
+
GeneratedImage(
|
|
509
|
+
url=url,
|
|
510
|
+
title=title,
|
|
511
|
+
alt=alt,
|
|
512
|
+
proxy=self.proxy,
|
|
513
|
+
cookies=self.cookies,
|
|
514
|
+
)
|
|
468
515
|
)
|
|
469
|
-
]
|
|
470
516
|
|
|
471
|
-
|
|
517
|
+
output_candidates.append(
|
|
472
518
|
Candidate(
|
|
473
|
-
rcid=
|
|
519
|
+
rcid=rcid,
|
|
474
520
|
text=text,
|
|
475
521
|
thoughts=thoughts,
|
|
476
522
|
web_images=web_images,
|
|
477
523
|
generated_images=generated_images,
|
|
478
524
|
)
|
|
479
525
|
)
|
|
480
|
-
|
|
526
|
+
|
|
527
|
+
if not output_candidates:
|
|
481
528
|
raise GeminiError(
|
|
482
529
|
"Failed to generate contents. No output data found in response."
|
|
483
530
|
)
|
|
484
531
|
|
|
485
|
-
output = ModelOutput(
|
|
486
|
-
|
|
487
|
-
|
|
532
|
+
output = ModelOutput(
|
|
533
|
+
metadata=get_nested_value(body, [1], []),
|
|
534
|
+
candidates=output_candidates,
|
|
535
|
+
)
|
|
536
|
+
except (TypeError, IndexError) as e:
|
|
537
|
+
logger.debug(
|
|
538
|
+
f"{type(e).__name__}: {e}; Invalid response structure: {response.text}"
|
|
539
|
+
)
|
|
488
540
|
raise APIError(
|
|
489
541
|
"Failed to parse response body. Data structure is invalid."
|
|
490
542
|
)
|
|
@@ -574,9 +626,10 @@ class ChatSession:
|
|
|
574
626
|
Reply id, if provided together with metadata, will override the second value in it.
|
|
575
627
|
rcid: `str`, optional
|
|
576
628
|
Reply candidate id, if provided together with metadata, will override the third value in it.
|
|
577
|
-
model: `Model
|
|
629
|
+
model: `Model | str | dict`, optional
|
|
578
630
|
Specify the model to use for generation.
|
|
579
|
-
Pass either a `gemini_webapi.constants.Model` enum or a model name string.
|
|
631
|
+
Pass either a `gemini_webapi.constants.Model` enum or a model name string to use predefined models.
|
|
632
|
+
Pass a dictionary to use custom model header strings ("model_name" and "model_header" keys must be provided).
|
|
580
633
|
gem: `Gem | str`, optional
|
|
581
634
|
Specify a gem to use as system prompt for the chat session.
|
|
582
635
|
Pass either a `gemini_webapi.types.Gem` object or a gem id string.
|
|
@@ -597,13 +650,13 @@ class ChatSession:
|
|
|
597
650
|
cid: str | None = None, # chat id
|
|
598
651
|
rid: str | None = None, # reply id
|
|
599
652
|
rcid: str | None = None, # reply candidate id
|
|
600
|
-
model: Model | str = Model.UNSPECIFIED,
|
|
653
|
+
model: Model | str | dict = Model.UNSPECIFIED,
|
|
601
654
|
gem: Gem | str | None = None,
|
|
602
655
|
):
|
|
603
656
|
self.__metadata: list[str | None] = [None, None, None]
|
|
604
657
|
self.geminiclient: GeminiClient = geminiclient
|
|
605
658
|
self.last_output: ModelOutput | None = None
|
|
606
|
-
self.model: Model | str = model
|
|
659
|
+
self.model: Model | str | dict = model
|
|
607
660
|
self.gem: Gem | str | None = gem
|
|
608
661
|
|
|
609
662
|
if metadata:
|
|
@@ -43,26 +43,27 @@ class Headers(Enum):
|
|
|
43
43
|
|
|
44
44
|
class Model(Enum):
|
|
45
45
|
UNSPECIFIED = ("unspecified", {}, False)
|
|
46
|
-
|
|
47
|
-
"gemini-
|
|
48
|
-
{
|
|
46
|
+
G_3_0_PRO = (
|
|
47
|
+
"gemini-3.0-pro",
|
|
48
|
+
{
|
|
49
|
+
"x-goog-ext-525001261-jspb": '[1,null,null,null,"9d8ca3786ebdfbea",null,null,0,[4]]'
|
|
50
|
+
},
|
|
49
51
|
False,
|
|
50
52
|
)
|
|
51
53
|
G_2_5_PRO = (
|
|
52
54
|
"gemini-2.5-pro",
|
|
53
|
-
{
|
|
55
|
+
{
|
|
56
|
+
"x-goog-ext-525001261-jspb": '[1,null,null,null,"4af6c7f5da75d65d",null,null,0,[4]]'
|
|
57
|
+
},
|
|
54
58
|
False,
|
|
55
59
|
)
|
|
56
|
-
|
|
57
|
-
"gemini-2.
|
|
58
|
-
{
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
G_2_0_FLASH_THINKING = (
|
|
62
|
-
"gemini-2.0-flash-thinking",
|
|
63
|
-
{"x-goog-ext-525001261-jspb": '[null,null,null,null,"7ca48d02d802f20a"]'},
|
|
60
|
+
G_2_5_FLASH = (
|
|
61
|
+
"gemini-2.5-flash",
|
|
62
|
+
{
|
|
63
|
+
"x-goog-ext-525001261-jspb": '[1,null,null,null,"9ec249fc9ad08861",null,null,0,[4]]'
|
|
64
|
+
},
|
|
64
65
|
False,
|
|
65
|
-
)
|
|
66
|
+
)
|
|
66
67
|
|
|
67
68
|
def __init__(self, name, header, advanced_only):
|
|
68
69
|
self.model_name = name
|
|
@@ -74,10 +75,28 @@ class Model(Enum):
|
|
|
74
75
|
for model in cls:
|
|
75
76
|
if model.model_name == name:
|
|
76
77
|
return model
|
|
78
|
+
|
|
77
79
|
raise ValueError(
|
|
78
80
|
f"Unknown model name: {name}. Available models: {', '.join([model.model_name for model in cls])}"
|
|
79
81
|
)
|
|
80
82
|
|
|
83
|
+
@classmethod
|
|
84
|
+
def from_dict(cls, model_dict: dict):
|
|
85
|
+
if "model_name" not in model_dict or "model_header" not in model_dict:
|
|
86
|
+
raise ValueError(
|
|
87
|
+
"When passing a custom model as a dictionary, 'model_name' and 'model_header' keys must be provided."
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
if not isinstance(model_dict["model_header"], dict):
|
|
91
|
+
raise ValueError(
|
|
92
|
+
"When passing a custom model as a dictionary, 'model_header' must be a dictionary containing valid header strings."
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
custom_model = cls.UNSPECIFIED
|
|
96
|
+
custom_model.model_name = model_dict["model_name"]
|
|
97
|
+
custom_model.model_header = model_dict["model_header"]
|
|
98
|
+
return custom_model
|
|
99
|
+
|
|
81
100
|
|
|
82
101
|
class ErrorCode(IntEnum):
|
|
83
102
|
"""
|
|
@@ -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.
|
|
3
|
+
Version: 1.17.1
|
|
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
|
|
@@ -783,11 +783,11 @@ pip install -U browser-cookie3
|
|
|
783
783
|
|
|
784
784
|
```yaml
|
|
785
785
|
services:
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
786
|
+
main:
|
|
787
|
+
environment:
|
|
788
|
+
GEMINI_COOKIE_PATH: /tmp/gemini_webapi
|
|
789
|
+
volumes:
|
|
790
|
+
- ./gemini_cookies:/tmp/gemini_webapi
|
|
791
791
|
```
|
|
792
792
|
|
|
793
793
|
> [!NOTE]
|
|
@@ -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
|
|
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
|
|
@@ -933,6 +929,23 @@ async def main():
|
|
|
933
929
|
asyncio.run(main())
|
|
934
930
|
```
|
|
935
931
|
|
|
932
|
+
You can also pass custom model header strings directly to access models which are not listed above.
|
|
933
|
+
|
|
934
|
+
```python
|
|
935
|
+
# "model_name" and "model_header" keys must be present
|
|
936
|
+
custom_model = {
|
|
937
|
+
"model_name": "xxx",
|
|
938
|
+
"model_header": {
|
|
939
|
+
"x-goog-ext-525001261-jspb": "[1,null,null,null,'e6fa609c3fa255c0',null,null,null,[4]]"
|
|
940
|
+
},
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
response = await client.generate_content(
|
|
944
|
+
"What's your model version?",
|
|
945
|
+
model=custom_model
|
|
946
|
+
)
|
|
947
|
+
```
|
|
948
|
+
|
|
936
949
|
### Apply system prompt with Gemini Gems
|
|
937
950
|
|
|
938
951
|
System prompt can be applied to conversations via [Gemini Gems](https://gemini.google.com/gems/view). To use a gem, you can pass `gem` argument to `GeminiClient.generate_content` or `GeminiClient.start_chat`. `gem` can be either a string of gem id or a `gemini_webapi.Gem` object. Only one gem can be applied to a single conversation.
|
|
@@ -33,6 +33,7 @@ src/gemini_webapi/utils/decorators.py
|
|
|
33
33
|
src/gemini_webapi/utils/get_access_token.py
|
|
34
34
|
src/gemini_webapi/utils/load_browser_cookies.py
|
|
35
35
|
src/gemini_webapi/utils/logger.py
|
|
36
|
+
src/gemini_webapi/utils/parsing.py
|
|
36
37
|
src/gemini_webapi/utils/rotate_1psidts.py
|
|
37
38
|
src/gemini_webapi/utils/upload_file.py
|
|
38
39
|
tests/test_client_features.py
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{gemini_webapi-1.16.0 → gemini_webapi-1.17.1}/src/gemini_webapi/utils/load_browser_cookies.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{gemini_webapi-1.16.0 → gemini_webapi-1.17.1}/src/gemini_webapi.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|