返回提交历史
Modified
.gitignore
+1
-0
Modified
g4f/Provider/needs_auth/Gemini.py
+65
-6
Modified
g4f/Provider/needs_auth/gemini_utils.py
+5
-1
Modified
g4f/api/__init__.py
+3
-9
Modified
g4f/gui/server/api.py
+9
-12
Modified
g4f/gui/server/backend_api.py
+2
-2
Deleted
g4f/gui/server/js_api.py
+0
-96
Deleted
g4f/gui/webview.py
+0
-55
Modified
projects/discord-bot/.env.example
+10
-0
Modified
projects/discord-bot/bot.py
+97
-3
XFEstudio/gpt4free
Update Gemini Models
81bad36d
代码差异
10 个文件
+192
-184
@@ -17,3 +17,4 @@ har_and_cookies
17
17
playground
18
18
.vscode
19
19
20
build
@@ -33,6 +33,7 @@ from ...providers.response import (
33
33
RequestLogin,
34
34
TitleGeneration,
35
35
YouTubeResponse,
36
ProviderInfo,
36
37
)
37
38
from ...requests.raise_for_status import raise_for_status
38
39
from ...requests.aiohttp import get_connector
@@ -420,7 +421,7 @@ class Gemini(AsyncGeneratorProvider, ProviderModelMixin):
420
421
cls,
421
422
session: ClientSession,
422
423
cookies: Cookies,
423
auth_user: int | str = None,
424
auth_user: int | str | None = None,
424
425
) -> None:
425
426
prefix = _account_prefix(auth_user)
426
427
params = {
@@ -462,6 +463,8 @@ class Gemini(AsyncGeneratorProvider, ProviderModelMixin):
462
463
463
464
@classmethod
464
465
def get_model_headers(cls, model: str) -> dict[str, str]:
466
if model in cls._account_models:
467
return cls._account_models[model].get("headers", {})
465
468
family = MODEL_FAMILIES.get(model)
466
469
# Request field 79 selects the model. Pro additionally needs the
467
470
# account-specific model header or Google silently routes it to Flash.
@@ -478,6 +481,8 @@ class Gemini(AsyncGeneratorProvider, ProviderModelMixin):
478
481
model: str,
479
482
allow_model_fallback: bool = False,
480
483
) -> None:
484
if model in cls._account_models:
485
return
481
486
model = MODEL_ALIASES.get(model, model)
482
487
if allow_model_fallback or cls._account_status is None:
483
488
return
@@ -515,14 +520,55 @@ class Gemini(AsyncGeneratorProvider, ProviderModelMixin):
515
520
await cls.fetch_snlm0e(session, cls._cookies)
516
521
return cls._snlm0e
517
522
523
@classmethod
524
async def get_models(
525
cls,
526
proxy: str | None = None,
527
cookies: Cookies | None = None,
528
connector: BaseConnector | None = None,
529
auth_user: int | str | None = None,
530
**kwargs,
531
) -> dict[str, dict]:
532
if cookies is not None:
533
cls._cookies = cookies
534
elif cls._cookies is None:
535
cls._cookies = get_cookies(GOOGLE_COOKIE_DOMAIN, False, True)
536
request_cookies = dict(cls._cookies or {})
537
base_connector = get_connector(connector, proxy)
538
539
async with ClientSession(
540
headers=REQUEST_HEADERS,
541
connector=base_connector,
542
**RESPONSE_HEADER_LIMITS,
543
) as session:
544
metadata_expired = (
545
time.time() - cls._metadata_fetched_at >= METADATA_CACHE_SECONDS
546
)
547
if not cls._metadata_fetched_at or metadata_expired:
548
try:
549
await cls.fetch_snlm0e(session, request_cookies, auth_user)
550
except (ClientError, MissingAuthError, ResponseError) as error:
551
cls._metadata_fetched_at = time.time()
552
debug.log(f"Gemini metadata discovery failed: {error}")
553
models_expired = (
554
time.time() - cls._account_models_fetched_at >= MODEL_CACHE_SECONDS
555
)
556
if not cls._account_models_fetched_at or models_expired:
557
try:
558
await cls.fetch_account_models(session, request_cookies, auth_user)
559
except (ClientError, ResponseError, ValueError) as error:
560
cls._account_models_fetched_at = time.time()
561
debug.log(f"Gemini model discovery failed: {error}")
562
return {key: value for key, value in cls._account_models.items() if value.get("available")}
563
518
564
@classmethod
519
565
async def create_async_generator(
520
566
cls,
521
567
model: str,
522
568
messages: Messages,
523
proxy: str = None,
524
cookies: Cookies = None,
525
connector: BaseConnector = None,
569
proxy: str | None = None,
570
cookies: Cookies | None = None,
571
connector: BaseConnector | None = None,
526
572
media: MediaListType = None,
527
573
return_conversation: bool = True,
528
574
conversation: Conversation = None,
@@ -559,7 +605,6 @@ class Gemini(AsyncGeneratorProvider, ProviderModelMixin):
559
605
"high": 1,
560
606
"xhigh": 0,
561
607
}.get(kwargs.get("reasoning_effort"))
562
model, expanded_thinking = _resolve_model(model, think_override)
563
608
if cookies is not None:
564
609
cls._cookies = cookies
565
610
elif cls._cookies is None:
@@ -619,6 +664,15 @@ class Gemini(AsyncGeneratorProvider, ProviderModelMixin):
619
664
except (ClientError, ResponseError, ValueError) as error:
620
665
cls._account_models_fetched_at = time.time()
621
666
debug.log(f"Gemini model discovery failed: {error}")
667
if model not in cls._account_models:
668
model, expanded_thinking = _resolve_model(model, think_override)
669
else:
670
yield ProviderInfo(**cls.get_dict(), model=cls._account_models[model]["label"])
671
expanded_thinking = (
672
model in EXPANDED_MODEL_ALIASES
673
if think_override is None
674
else think_override <= 2
675
)
622
676
cls.validate_model_access(
623
677
model,
624
678
allow_model_fallback=bool(kwargs.get("allow_model_fallback", False)),
@@ -978,7 +1032,9 @@ class Gemini(AsyncGeneratorProvider, ProviderModelMixin):
978
1032
async for chunk in iter_base64_decode(iter_base64_response):
979
1033
yield chunk
980
1034
1035
@classmethod
981
1036
def build_request(
1037
cls,
982
1038
prompt: str,
983
1039
language: str,
984
1040
model: str,
@@ -1026,7 +1082,10 @@ class Gemini(AsyncGeneratorProvider, ProviderModelMixin):
1026
1082
request[59] = request_uuid or str(uuid.uuid4())
1027
1083
request[61] = []
1028
1084
request[68] = 2
1029
request[79] = models[model]["mode"]
1085
if model in cls._account_models:
1086
request[79] = cls._account_models[model]["mode"]
1087
else:
1088
request[79] = models[model]["mode"]
1030
1089
request[80] = 2 if expanded_thinking else 1
1031
1090
request[91] = 0
1032
1091
# Gemini Web marks the first turn with 1 and follow-up turns with 0.
@@ -202,9 +202,11 @@ def parse_account_models(content: str) -> tuple[int | None, dict[str, dict]]:
202
202
for model_data in models_list:
203
203
if not isinstance(model_data, list):
204
204
continue
205
label = get_nested_value(model_data, [-1], "")
205
206
model_id = get_nested_value(model_data, [0], "")
206
207
display_name = get_nested_value(model_data, [1], "")
207
208
description = get_nested_value(model_data, [2], "")
209
mode = get_nested_value(model_data, [17], "")
208
210
if not isinstance(model_id, str) or not model_id:
209
211
continue
210
212
family = _model_family(model_id, display_name, description)
@@ -215,7 +217,9 @@ def parse_account_models(content: str) -> tuple[int | None, dict[str, dict]]:
215
217
if status_code == ACCOUNT_STATUS_UNAUTHENTICATED:
216
218
available = family == "flash"
217
219
registry[model_id] = {
218
"model_id": model_id,
220
"id": model_id,
221
"label": label,
222
"mode": mode,
219
223
"family": family,
220
224
"display_name": display_name,
221
225
"description": description,
@@ -1,5 +1,6 @@
1
1
from __future__ import annotations
2
2
3
import inspect
3
4
import logging
4
5
import json
5
6
import asyncio
@@ -791,6 +792,8 @@ class Api:
791
792
models = provider.get_models(api_key=credentials.credentials)
792
793
else:
793
794
models = provider.get_models()
795
if inspect.isawaitable(models):
796
models = await models
794
797
return {
795
798
"object": "list",
796
799
"data": [
@@ -1433,21 +1436,12 @@ class Api:
1433
1436
except ProviderNotFoundError as e:
1434
1437
return ErrorResponse.from_message(str(e), 404)
1435
1438
1436
def safe_get_models(provider: ProviderType) -> list[str]:
1437
try:
1438
return (
1439
provider.get_models() if hasattr(provider, "get_models") else []
1440
)
1441
except Exception:
1442
return []
1443
1444
1439
return {
1445
1440
"id": provider.__name__,
1446
1441
"object": "provider",
1447
1442
"created": 0,
1448
1443
"url": provider.url,
1449
1444
"label": getattr(provider, "label", None),
1450
"models": safe_get_models(provider),
1451
1445
"image_models": getattr(provider, "image_models", []) or [],
1452
1446
"vision_models": [
1453
1447
model
@@ -1,5 +1,6 @@
1
1
from __future__ import annotations
2
2
3
import inspect
3
4
import logging
4
5
import os
5
6
import asyncio
@@ -39,7 +40,7 @@ class Api:
39
40
models_lock = threading.Lock()
40
41
41
42
@staticmethod
42
def get_provider_models(
43
async def get_provider_models(
43
44
provider: str, api_key: "str | None" = None, base_url: "str | None" = None, ignored: "list | None" = None
44
45
):
45
46
def get_model_data(
@@ -80,6 +81,8 @@ class Api:
80
81
models = method(ignored=ignored)
81
82
else:
82
83
models = method()
84
if inspect.isawaitable(models):
85
models = await models
83
86
if has_grouped_models:
84
87
return [
85
88
{
@@ -113,15 +116,6 @@ class Api:
113
116
saved = storage.get(f"{version_utils.current_version}/providers")
114
117
if saved is not None:
115
118
return saved
116
def safe_get_models(provider: ProviderModelMixin):
117
if not isinstance(provider, ProviderModelMixin):
118
return True
119
try:
120
return provider.get_models()
121
except Exception as e:
122
logger.exception(e)
123
return True
124
125
119
result = [
126
120
{
127
121
"name": provider.__name__,
@@ -142,7 +136,7 @@ class Api:
142
136
"login": hasattr(provider, "login"),
143
137
}
144
138
for provider in Provider.__providers__
145
if provider.working and safe_get_models(provider)
139
if provider.working
146
140
]
147
141
storage.set(f"{version_utils.current_version}/providers", result)
148
142
return result
@@ -156,7 +150,10 @@ class Api:
156
150
157
151
def safe_get_provider_models(provider) -> tuple[str, list[str]]:
158
152
try:
159
return provider.__name__, list(provider.get_models(timeout=10))
153
models = provider.get_models(timeout=10)
154
if inspect.isawaitable(models):
155
models = asyncio.run(models)
156
return provider.__name__, list(models)
160
157
except MissingAuthError as e:
161
158
return provider.__name__, []
162
159
except Exception as e:
@@ -204,9 +204,9 @@ class Backend_Api(Api):
204
204
return jsonify(self.get_all_models())
205
205
206
206
@app.route("/backend-api/v2/models/<provider>", methods=["GET"])
207
def jsonify_provider_models(**kwargs):
207
async def jsonify_provider_models(**kwargs):
208
208
try:
209
response = self.get_provider_models(**kwargs)
209
response = await self.get_provider_models(**kwargs)
210
210
if response is None:
211
211
return jsonify({"error": {"message": "Provider not found"}}), 404
212
212
except MissingAuthError as e:
@@ -1,96 +0,0 @@
1
from __future__ import annotations
2
3
import json
4
import os.path
5
from typing import Iterator
6
from uuid import uuid4
7
from functools import partial
8
import webview
9
import platformdirs
10
from plyer import camera
11
from plyer import filechooser
12
13
app_storage_path = platformdirs.user_pictures_dir
14
user_select_image = partial(
15
filechooser.open_file,
16
path=platformdirs.user_pictures_dir(),
17
filters=[["Image", "*.jpg", "*.jpeg", "*.png", "*.webp", "*.svg"]],
18
)
19
20
from .api import Api
21
22
23
class JsApi(Api):
24
def get_conversation(
25
self, options: dict, message_id: str = None, scroll: bool = None
26
) -> Iterator:
27
window = webview.windows[0]
28
if hasattr(self, "image") and self.image is not None:
29
options["image"] = open(self.image, "rb")
30
for message in self._create_response_stream(
31
self._prepare_conversation_kwargs(options),
32
options.get("conversation_id"),
33
options.get("provider"),
34
):
35
if window.evaluate_js(
36
f"""
37
is_stopped() ? true :
38
this.add_message_chunk({
39
json.dumps(message)
40
}, {
41
json.dumps(message_id)
42
}, {
43
json.dumps(options.get('provider'))
44
}, {
45
'true' if scroll else 'false'
46
}); is_stopped();
47
"""
48
):
49
break
50
self.image = None
51
self.set_selected(None)
52
53
def choose_image(self):
54
user_select_image(on_selection=self.on_image_selection)
55
56
def take_picture(self):
57
filename = os.path.join(app_storage_path(), f"chat-{uuid4()}.png")
58
camera.take_picture(filename=filename, on_complete=self.on_camera)
59
60
def on_image_selection(self, filename):
61
filename = filename[0] if isinstance(filename, list) and filename else filename
62
if filename and os.path.exists(filename):
63
self.image = filename
64
else:
65
self.image = None
66
self.set_selected(None if self.image is None else "image")
67
68
def on_camera(self, filename):
69
if filename and os.path.exists(filename):
70
self.image = filename
71
else:
72
self.image = None
73
self.set_selected(None if self.image is None else "camera")
74
75
def set_selected(self, input_id: str = None):
76
window = webview.windows[0]
77
if window is not None:
78
window.evaluate_js(
79
f"document.querySelector(`.image-label.selected`)?.classList.remove(`selected`);"
80
)
81
if input_id is not None and input_id in ("image", "camera"):
82
window.evaluate_js(
83
f'document.querySelector(`label[for="{input_id}"]`)?.classList.add(`selected`);'
84
)
85
86
def get_version(self):
87
return super().get_version()
88
89
def get_models(self):
90
return super().get_models()
91
92
def get_providers(self):
93
return super().get_providers()
94
95
def get_provider_models(self, provider: str, **kwargs):
96
return super().get_provider_models(provider, **kwargs)
@@ -1,55 +0,0 @@
1
from __future__ import annotations
2
3
import sys
4
import os.path
5
import webview
6
7
try:
8
from platformdirs import user_config_dir
9
10
has_platformdirs = True
11
except ImportError:
12
has_platformdirs = False
13
14
from g4f.gui.gui_parser import gui_parser
15
from g4f.gui.server.js_api import JsApi
16
import g4f.version
17
import g4f.debug
18
19
20
def run_webview(
21
debug: bool = False,
22
http_port: int = None,
23
ssl: bool = True,
24
storage_path: str = None,
25
gui: str = None,
26
):
27
if getattr(sys, "frozen", False):
28
dirname = sys._MEIPASS
29
else:
30
dirname = os.path.dirname(__file__)
31
webview.settings["OPEN_EXTERNAL_LINKS_IN_BROWSER"] = True
32
webview.settings["ALLOW_DOWNLOADS"] = True
33
webview.create_window(
34
f"g4f - {g4f.version.utils.current_version}",
35
os.path.join(dirname, "client/index.html"),
36
text_select=True,
37
js_api=JsApi(),
38
)
39
if has_platformdirs and storage_path is None:
40
storage_path = user_config_dir("g4f-webview")
41
webview.start(
42
private_mode=False,
43
storage_path=storage_path,
44
debug=debug,
45
http_port=http_port,
46
ssl=ssl,
47
)
48
49
50
if __name__ == "__main__":
51
parser = gui_parser()
52
args = parser.parse_args()
53
if args.debug:
54
g4f.debug.logging = True
55
run_webview(args.debug, args.port, not args.debug)
@@ -22,3 +22,13 @@ G4F_MAX_HISTORY=12
22
22
23
23
# Max tool-calling rounds before forcing a final answer (default: 4)
24
24
# G4F_MAX_TOOL_LOOPS=4
25
26
# Auto-translation: comma-separated channel IDs where the bot will
27
# auto-translate every non-bot message to English and reply with the
28
# translation. Leave unset to disable.
29
# G4F_TRANSLATE_CHANNELS=123456789012345678,987654321098765432
30
31
# Honeypot channels: comma-separated channel IDs where slash commands
32
# are silently blocked. Useful for announcement or trap channels where
33
# you don't want command spam.
34
# G4F_HONEYPOT_CHANNELS=111111111111111111
@@ -12,6 +12,8 @@ Features:
12
12
feeds the results back, and produces a final answer.
13
13
- Streaming responses edited in-place for a "typing" effect
14
14
- Configurable model and provider via environment variables
15
- Auto-translation: messages in designated channels are translated to English
16
- Honeypot: slash commands are silently blocked in designated channels
15
17
"""
16
18
17
19
from __future__ import annotations
@@ -83,7 +85,27 @@ histories: Dict[int, Deque[dict]] = defaultdict(lambda: deque(maxlen=MAX_HISTORY
83
85
intents = discord.Intents.default()
84
86
intents.message_content = True # required to read user messages
85
87
86
bot = commands.Bot(command_prefix="!", intents=intents)
88
89
# ---------------------------------------------------------------------------
90
# Honeypot: silently ignore slash commands in designated channels
91
# ---------------------------------------------------------------------------
92
class HoneypotTree(app_commands.CommandTree):
93
"""Custom command tree that blocks slash commands in honeypot channels."""
94
95
async def interaction_check(
96
self, interaction: discord.Interaction, /
97
) -> bool:
98
if HONEYPOT_CHANNELS and interaction.channel_id in HONEYPOT_CHANNELS:
99
if not interaction.response.is_done():
100
await interaction.response.send_message(
101
"🚫 Commands are not allowed in this channel.",
102
ephemeral=True,
103
)
104
return False
105
return True
106
107
108
bot = commands.Bot(command_prefix="!", intents=intents, tree_cls=HoneypotTree)
87
109
88
110
89
111
# ---------------------------------------------------------------------------
@@ -101,6 +123,37 @@ IMAGE_CHANNELS = {
101
123
if x.strip().isdigit()
102
124
}
103
125
126
# ---------------------------------------------------------------------------
127
# Auto-translation channels
128
# ---------------------------------------------------------------------------
129
# If enabled, the bot listens to messages in the given channel(s) and
130
# auto-translates them to English, posting the translation as a reply.
131
#
132
# Env vars:
133
# - G4F_TRANSLATE_CHANNELS: comma-separated channel ids (required to enable)
134
_translate_channels_env = os.getenv("G4F_TRANSLATE_CHANNELS", "")
135
TRANSLATE_CHANNELS = {
136
int(x.strip())
137
for x in _translate_channels_env.split(",")
138
if x.strip().isdigit()
139
}
140
141
# ---------------------------------------------------------------------------
142
# Honeypot channels
143
# ---------------------------------------------------------------------------
144
# Slash commands are silently ignored in these channels. Useful for
145
# channels where you don't want bot command spam (e.g. announcement or
146
# honeypot/trap channels).
147
#
148
# Env vars:
149
# - G4F_HONEYPOT_CHANNELS: comma-separated channel ids
150
_honeypot_channels_env = os.getenv("G4F_HONEYPOT_CHANNELS", "")
151
HONEYPOT_CHANNELS = {
152
int(x.strip())
153
for x in _honeypot_channels_env.split(",")
154
if x.strip().isdigit()
155
}
156
104
157
105
158
def _build_messages(user_id: int, user_content: str) -> List[dict]:
106
159
"""Return the full message list including system prompt and history."""
@@ -557,10 +610,26 @@ async def on_message(message: discord.Message):
557
610
# Let slash commands etc. work as usual.
558
611
await bot.process_commands(message)
559
612
560
if not IMAGE_CHANNELS:
613
if message.author.bot:
561
614
return
562
615
563
if message.author.bot:
616
# --- Auto-translation to English ---
617
if TRANSLATE_CHANNELS and message.channel.id in TRANSLATE_CHANNELS:
618
content = message.content.strip()
619
if content:
620
try:
621
translation = await _translate_to_english(content)
622
if translation and translation != content:
623
await message.reply(
624
f"🌐 **English:** {_truncate(translation, 1850)}",
625
mention_author=False,
626
)
627
except Exception as e:
628
log.exception("Auto translation failed")
629
return # don't fall through to image generation in translate channels
630
631
# --- Auto image generation ---
632
if not IMAGE_CHANNELS:
564
633
return
565
634
566
635
if message.channel.id not in IMAGE_CHANNELS:
@@ -620,6 +689,31 @@ async def _finalize_response(
620
689
await interaction.edit_original_response(content=_truncate(reply))
621
690
622
691
692
# ---------------------------------------------------------------------------
693
# Translation
694
# ---------------------------------------------------------------------------
695
TRANSLATE_SYSTEM_PROMPT = (
696
"You are a translation engine. Translate the user's message into English. "
697
"Output ONLY the translation — no explanations, no notes, no quotes. "
698
"If the text is already in English, output it unchanged."
699
)
700
701
702
async def _translate_to_english(text: str) -> str:
703
"""Translate *text* to English via g4f and return the translation."""
704
messages = [
705
{"role": "system", "content": TRANSLATE_SYSTEM_PROMPT},
706
{"role": "user", "content": text},
707
]
708
response = await client.chat.completions.create(
709
model=MODEL,
710
messages=messages,
711
stream=False,
712
proxy=PROXY,
713
)
714
return (response.choices[0].message.content or "").strip()
715
716
623
717
# ---------------------------------------------------------------------------
624
718
# Image generation
625
719
# ---------------------------------------------------------------------------