XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet
公开
关注 0 Fork 0 Star 0
返回提交历史

XFEstudio/gpt4free

Update Gemini Models

81bad36d
hlohaus <hlohaus@users.noreply.github.com>
提交于

代码差异

10 个文件 +192 -184
Modified .gitignore +1 -0
@@ -17,3 +17,4 @@ har_and_cookies
17 17 playground
18 18 .vscode
19 19
20 build
Modified g4f/Provider/needs_auth/Gemini.py +65 -6
@@ -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.
Modified g4f/Provider/needs_auth/gemini_utils.py +5 -1
@@ -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,
Modified g4f/api/__init__.py +3 -9
@@ -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
Modified g4f/gui/server/api.py +9 -12
@@ -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:
Modified g4f/gui/server/backend_api.py +2 -2
@@ -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:
Deleted g4f/gui/server/js_api.py +0 -96
@@ -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)
Deleted g4f/gui/webview.py +0 -55
@@ -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)
Modified projects/discord-bot/.env.example +10 -0
@@ -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
Modified projects/discord-bot/bot.py +97 -3
@@ -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 # ---------------------------------------------------------------------------