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

XFEstudio/gpt4free

feat: update providers, model selection, media handling, and routing

- Added GithubCopilotAPI provider to g4f/Provider/needs_auth and __init__.py - Fixed typo "GGOGLE_SID_COOKIE" to "GOOGLE_SID_COOKIE" in Gemini.py and updated all references - Updated PollinationsAI.py: - Refined model aliases and removed/commented unused/legacy aliases - Updated logic for loading audio and vision models, using swap_models for alias reversals - Adjusted get_model and model loading methods for accuracy - Changed default model lists for text, image, and vision models - Updated conversation title and followup labels for followups tools - Modified save_content in g4f/cli/client.py to handle url downloads for lists, allow cookies/headers, and removed duplicate HTTP download logic - Added asyncio sleep after stdout writes in stream_response for smoother streaming - Changed website.py render default to "home," adjusted chat route to accept any filename, and updated filenames used for rendering - Updated model selection in g4f/models.py by removing PollinationsAI from best_provider and changing model provider order for specific models - Enhanced media merging in g4f/tools/media.py to clarify comment about last user message and handle content appending for lists in render_messages - Updated OpenaiTemplate.py to add an image_url field if media with http(s) URLs is present - Adjusted test_provider_has_model in etc/unittest/models.py to skip providers requiring auth

6c126e46
hlohaus <983577+hlohaus@users.noreply.github.com>
提交于

代码差异

11 个文件 +79 -72
Modified etc/tool/commit.py +1 -1
@@ -347,7 +347,7 @@ def generate_commit_message(diff_text: str, model: str = DEFAULT_MODEL, max_retr
347 347 spinner = None
348 348 content.append(chunk.choices[0].delta.content)
349 349 print(chunk.choices[0].delta.content, end="", flush=True)
350 return "".join(content).strip("`").strip()
350 return "".join(content).strip("`").split("\n---\n")[0].strip()
351 351 except Exception as e:
352 352 # Stop spinner if it's running
353 353 if 'spinner' in locals() and spinner:
Modified etc/unittest/models.py +2 -0
@@ -12,6 +12,8 @@ class TestProviderHasModel(unittest.TestCase):
12 12 def test_provider_has_model(self):
13 13 for model, providers in __models__.values():
14 14 for provider in providers:
15 if provider.needs_auth:
16 continue
15 17 if issubclass(provider, ProviderModelMixin):
16 18 provider.get_models() # Update models
17 19 if model.name in provider.model_aliases:
Modified g4f/Provider/PollinationsAI.py +24 -45
@@ -5,7 +5,7 @@ import json
5 5 import random
6 6 import requests
7 7 import asyncio
8 from urllib.parse import quote, quote_plus
8 from urllib.parse import quote_plus
9 9 from typing import Optional
10 10 from aiohttp import ClientSession, ClientTimeout
11 11
@@ -40,14 +40,14 @@ FOLLOWUPS_TOOLS = [{
40 40 "parameters": {
41 41 "properties": {
42 42 "title": {
43 "title": "Conversation Title",
43 "title": "Conversation title. Prefixed with one or more emojies",
44 44 "type": "string"
45 45 },
46 46 "followups": {
47 47 "items": {
48 48 "type": "string"
49 49 },
50 "title": "Suggested Followups",
50 "title": "Suggested 4 Followups (only user messages)",
51 51 "type": "array"
52 52 }
53 53 },
@@ -59,7 +59,7 @@ FOLLOWUPS_TOOLS = [{
59 59
60 60 FOLLOWUPS_DEVELOPER_MESSAGE = [{
61 61 "role": "developer",
62 "content": "Prefix conversation title with one or more emojies. Suggested 4 Followups (User messages only).",
62 "content": "Provide conversation options.",
63 63 }]
64 64
65 65 class PollinationsAI(AsyncGeneratorProvider, ProviderModelMixin):
@@ -83,61 +83,38 @@ class PollinationsAI(AsyncGeneratorProvider, ProviderModelMixin):
83 83 default_vision_model = default_model
84 84 default_audio_model = "openai-audio"
85 85 text_models = [default_model, "evil"]
86 image_models = [default_image_model, "flux-dev", "turbo", "gptimage"]
86 image_models = [default_image_model, "kontext", "gptimage"]
87 87 audio_models = {default_audio_model: []}
88 vision_models = [default_vision_model, "gpt-4o-mini", "openai", "openai-large", "openai-reasoning", "searchgpt"]
88 vision_models = [default_vision_model]
89 89 _models_loaded = False
90 # https://github.com/pollinations/pollinations/blob/master/text.pollinations.ai/generateTextPortkey.js#L15
91 90 model_aliases = {
92 ### Text Models ###
91 "gpt-4": "openai",
92 "gpt-4o": "openai",
93 "gpt-4.1-mini": "openai",
93 94 "gpt-4o-mini": "openai",
94 95 "gpt-4.1-nano": "openai-fast",
95 "gpt-4": "openai-large",
96 "gpt-4o": "openai-large",
97 96 "gpt-4.1": "openai-large",
98 "gpt-4o-audio": "openai-audio",
99 97 "o4-mini": "openai-reasoning",
100 "gpt-4.1-mini": "openai",
101 "command-r-plus": "command-r",
102 "gemini-2.5-flash": "gemini",
103 "gemini-2.0-flash-thinking": "gemini-thinking",
104 98 "qwen-2.5-coder-32b": "qwen-coder",
105 99 "llama-3.3-70b": "llama",
106 100 "llama-4-scout": "llamascout",
107 "llama-4-scout-17b": "llamascout",
108 101 "mistral-small-3.1-24b": "mistral",
109 "deepseek-r1": "deepseek-reasoning-large",
110 "deepseek-r1-distill-llama-70b": "deepseek-reasoning-large",
111 #"deepseek-r1-distill-llama-70b": "deepseek-r1-llama",
112 #"mistral-small-3.1-24b": "unity", # Personas
113 #"mirexa": "mirexa", # Personas
114 #"midijourney": "midijourney", # Personas
115 #"rtist": "rtist", # Personas
116 #"searchgpt": "searchgpt",
117 #"evil": "evil", # Personas
118 "deepseek-r1-distill-qwen-32b": "deepseek-reasoning",
119 102 "phi-4": "phi",
120 #"pixtral-12b": "pixtral",
121 #"hormoz-8b": "hormoz",
122 "qwq-32b": "qwen-qwq",
123 #"hypnosis-tracy-7b": "hypnosis-tracy", # Personas
124 #"mistral-?": "sur", # Personas
125 "deepseek-v3": "deepseek",
103 "deepseek-r1": "deepseek-reasoning",
126 104 "deepseek-v3-0324": "deepseek",
127 #"bidara": "bidara", # Personas
105 "deepseek-v3": "deepseek",
128 106 "grok-3-mini": "grok",
129
130 ### Audio Models ###
131 "gpt-4o-audio": "openai-audio",
107 "grok-3-mini-high": "grok",
132 108 "gpt-4o-mini-audio": "openai-audio",
133
134 ### Image Models ###
109 "gpt-4o-audio": "openai-audio",
135 110 "sdxl-turbo": "turbo",
136 111 "gpt-image": "gptimage",
137 "dall-e-3": "gptimage",
112 "flux-dev": "flux",
113 "flux-schnell": "flux",
138 114 "flux-pro": "flux",
139 "flux-schnell": "flux"
115 "flux": "flux",
140 116 }
117 swap_models = {value: key for key, value in model_aliases.items()}
141 118
142 119 @classmethod
143 120 def get_model(cls, model: str) -> str:
@@ -185,11 +162,14 @@ class PollinationsAI(AsyncGeneratorProvider, ProviderModelMixin):
185 162 cls.audio_models = {
186 163 model.get("name"): model.get("voices")
187 164 for model in models
188 if "output_modalities" in model and "audio" in model["output_modalities"] and model.get("name") != "gemini"
165 if "output_modalities" in model and "audio" in model["output_modalities"]
189 166 }
167 for alias, model in cls.model_aliases.items():
168 if model in cls.audio_models and alias not in cls.audio_models:
169 cls.audio_models.update({alias: {}})
190 170
191 171 cls.vision_models.extend([
192 model.get("name")
172 cls.swap_models.get(model.get("name"), model.get("name"))
193 173 for model in models
194 174 if model.get("vision") and model not in cls.vision_models
195 175 ])
@@ -207,7 +187,7 @@ class PollinationsAI(AsyncGeneratorProvider, ProviderModelMixin):
207 187 for model in models:
208 188 model_name = model.get("name")
209 189 if model_name and "input_modalities" in model and "text" in model["input_modalities"]:
210 text_models.append(model_name)
190 text_models.append(cls.swap_models.get(model_name, model_name))
211 191
212 192 # Convert to list and update text_models
213 193 cls.text_models = list(dict.fromkeys(text_models))
@@ -237,7 +217,7 @@ class PollinationsAI(AsyncGeneratorProvider, ProviderModelMixin):
237 217 {"group": "Text Generation", "models": cls.text_models},
238 218 {"group": "Image Generation", "models": cls.image_models},
239 219 {"group": "Audio Generation", "models": list(cls.audio_models.keys())},
240 {"group": "Audio Voices", "models": cls.audio_models[cls.default_audio_model]}
220 {"group": "Audio Voices", "models": cls.audio_models.get(cls.default_audio_model, [])},
241 221 ]
242 222
243 223 @classmethod
@@ -270,7 +250,6 @@ class PollinationsAI(AsyncGeneratorProvider, ProviderModelMixin):
270 250 top_p: float = None,
271 251 frequency_penalty: float = None,
272 252 response_format: Optional[dict] = None,
273 download_media: bool = True,
274 253 extra_parameters: list[str] = ["tools", "parallel_tool_calls", "tool_choice", "reasoning_effort", "logit_bias", "voice", "modalities", "audio"],
275 254 **kwargs
276 255 ) -> AsyncResult:
Modified g4f/Provider/needs_auth/Gemini.py +6 -5
@@ -59,7 +59,7 @@ UPLOAD_IMAGE_HEADERS = {
59 59 }
60 60 GOOGLE_COOKIE_DOMAIN = ".google.com"
61 61 ROTATE_COOKIES_URL = "https://accounts.google.com/RotateCookies"
62 GGOGLE_SID_COOKIE = "__Secure-1PSID"
62 GOOGLE_SID_COOKIE = "__Secure-1PSID"
63 63
64 64 models = {
65 65 "gemini-2.5-pro-exp": {"x-goog-ext-525001261-jspb": '[1,null,null,null,"2525e3954d185b3c"]'},
@@ -152,11 +152,12 @@ class Gemini(AsyncGeneratorProvider, ProviderModelMixin):
152 152 """
153 153
154 154 while True:
155 new_1psidts = None
155 156 try:
156 157 new_1psidts = await rotate_1psidts(cls.url, cls._cookies, proxy)
157 158 except Exception as e:
158 159 debug.error(f"Failed to refresh cookies: {e}")
159 task = cls.rotate_tasks.get(cls._cookies[GGOGLE_SID_COOKIE])
160 task = cls.rotate_tasks.get(cls._cookies[GOOGLE_SID_COOKIE])
160 161 if task:
161 162 task.cancel()
162 163 debug.error(
@@ -220,10 +221,10 @@ class Gemini(AsyncGeneratorProvider, ProviderModelMixin):
220 221 await cls.fetch_snlm0e(session, cls._cookies)
221 222 if not cls._snlm0e:
222 223 raise RuntimeError("Invalid cookies. SNlM0e not found")
223 if GGOGLE_SID_COOKIE in cls._cookies:
224 task = cls.rotate_tasks.get(cls._cookies[GGOGLE_SID_COOKIE])
224 if GOOGLE_SID_COOKIE in cls._cookies:
225 task = cls.rotate_tasks.get(cls._cookies[GOOGLE_SID_COOKIE])
225 226 if not task:
226 cls.rotate_tasks[cls._cookies[GGOGLE_SID_COOKIE]] = asyncio.create_task(
227 cls.rotate_tasks[cls._cookies[GOOGLE_SID_COOKIE]] = asyncio.create_task(
227 228 cls.start_auto_refresh()
228 229 )
229 230
Added g4f/Provider/needs_auth/GithubCopilotAPI.py +12 -0
@@ -0,0 +1,12 @@
1 from __future__ import annotations
2
3 from .OpenaiAPI import OpenaiAPI
4
5 class GithubCopilotAPI(OpenaiAPI):
6 label = "GitHub Copilot API"
7 url = "https://github.com/copilot"
8 login_url = "https://aider.chat/docs/llms/github.html"
9 working = True
10 api_base = "https://api.githubcopilot.com"
11 needs_auth = True
12
Modified g4f/Provider/needs_auth/__init__.py +1 -0
@@ -13,6 +13,7 @@ from .Gemini import Gemini
13 13 from .GeminiPro import GeminiPro
14 14 from .GigaChat import GigaChat
15 15 from .GithubCopilot import GithubCopilot
16 from .GithubCopilotAPI import GithubCopilotAPI
16 17 from .GlhfChat import GlhfChat
17 18 from .Grok import Grok
18 19 from .Groq import Groq
Modified g4f/Provider/template/OpenaiTemplate.py +3 -0
@@ -92,6 +92,9 @@ class OpenaiTemplate(AsyncGeneratorProvider, ProviderModelMixin, RaiseErrorMixin
92 92 "model": model,
93 93 **use_aspect_ratio({"width": kwargs.get("width"), "height": kwargs.get("height")}, kwargs.get("aspect_ratio", None))
94 94 }
95 # Handle media if provided
96 if media is not None:
97 data["image_url"] = next([data for data, _ in media if data and isinstance(data, str) and data.startswith("http://") or data.startswith("https://")], None)
95 98 async with session.post(f"{api_base.rstrip('/')}/images/generations", json=data, ssl=cls.ssl) as response:
96 99 data = await response.json()
97 100 cls.raise_error(data, response.status)
Modified g4f/cli/client.py +16 -8
@@ -134,6 +134,7 @@ async def stream_response(
134 134 for byte in str(token).encode('utf-8'):
135 135 sys.stdout.buffer.write(bytes([byte]))
136 136 sys.stdout.buffer.flush()
137 await asyncio.sleep(0.01)
137 138 except (IOError, BrokenPipeError) as e:
138 139 print(f"\nError writing to stdout: {e}", file=sys.stderr)
139 140 break
@@ -153,7 +154,21 @@ async def stream_response(
153 154
154 155 def save_content(content, filepath: str, allowed_types = None):
155 156 if hasattr(content, "urls"):
156 content = next(iter(content.urls), None) if isinstance(content.urls, list) else content.urls
157 import requests
158 for url in content.urls:
159 if url.startswith("http://") or url.startswith("https://"):
160 try:
161 response = requests.get(url, cookies=content.get("cookies"), headers=content.get("headers"))
162 if response.status_code == 200:
163 with open(filepath, "wb") as f:
164 f.write(response.content)
165 return True
166 except requests.RequestException as e:
167 print(f"Error downloading {url}: {e}", file=sys.stderr)
168 return False
169 else:
170 content = url
171 break
157 172 elif hasattr(content, "data"):
158 173 content = content.data
159 174 if not content:
@@ -166,13 +181,6 @@ def save_content(content, filepath: str, allowed_types = None):
166 181 with open(filepath, "wb") as f:
167 182 f.write(extract_data_uri(content))
168 183 return True
169 elif content.startswith("http://") or content.startswith("https://"):
170 import requests
171 response = requests.get(content)
172 if response.status_code == 200:
173 with open(filepath, "wb") as f:
174 f.write(response.content)
175 return True
176 184 content = filter_markdown(content, allowed_types)
177 185 if content:
178 186 with open(filepath, "w") as f:
Modified g4f/gui/server/website.py +4 -4
@@ -14,7 +14,7 @@ from ... import version
14 14 def redirect_home():
15 15 return redirect('/chat/')
16 16
17 def render(filename = "chat"):
17 def render(filename = "home"):
18 18 if os.path.exists(DIST_DIR) and not request.args.get("debug"):
19 19 path = os.path.abspath(os.path.join(os.path.dirname(DIST_DIR), (filename + ("" if "." in filename else ".html"))))
20 20 return send_from_directory(os.path.dirname(path), os.path.basename(path))
@@ -72,7 +72,7 @@ class Website:
72 72 'function': self._background,
73 73 'methods': ['GET', 'POST']
74 74 },
75 '/chat/<conversation_id>': {
75 '/chat/<filename>': {
76 76 'function': self._chat,
77 77 'methods': ['GET', 'POST']
78 78 },
@@ -95,8 +95,8 @@ class Website:
95 95 def _background(self, filename = "background"):
96 96 return render(filename)
97 97
98 def _chat(self, filename = "chat"):
99 filename = "chat/index" if filename == 'chat' else secure_filename(filename)
98 def _chat(self, filename = ""):
99 filename = f"chat/{filename}" if filename else "chat/index"
100 100 return render(filename)
101 101
102 102 def _dist(self, name: str):
Modified g4f/models.py +6 -8
@@ -23,7 +23,6 @@ from .Provider import (
23 23 OIVSCodeSer0501,
24 24 OpenAIFM,
25 25 PerplexityLabs,
26 Pi,
27 26 PollinationsAI,
28 27 PollinationsImage,
29 28 TeachAnything,
@@ -37,7 +36,6 @@ from .Provider import (
37 36 CopilotAccount,
38 37 Gemini,
39 38 GeminiPro,
40 HailuoAI,
41 39 HuggingChat,
42 40 HuggingFace,
43 41 HuggingFaceMedia,
@@ -502,7 +500,7 @@ gemini_2_0_flash = Model(
502 500 gemini_2_0_flash_thinking = Model(
503 501 name = 'gemini-2.0-flash-thinking',
504 502 base_provider = 'Google',
505 best_provider = IterListProvider([PollinationsAI, Gemini])
503 best_provider = IterListProvider([Gemini, GeminiPro])
506 504 )
507 505
508 506 gemini_2_0_flash_thinking_with_apps = Model(
@@ -515,7 +513,7 @@ gemini_2_0_flash_thinking_with_apps = Model(
515 513 gemini_2_5_flash = Model(
516 514 name = 'gemini-2.5-flash',
517 515 base_provider = 'Google',
518 best_provider = IterListProvider([PollinationsAI, Gemini])
516 best_provider = IterListProvider([Gemini, GeminiPro])
519 517 )
520 518
521 519 gemini_2_5_pro = Model(
@@ -561,7 +559,7 @@ command_r = Model(
561 559 command_r_plus = Model(
562 560 name = 'command-r-plus',
563 561 base_provider = 'CohereForAI',
564 best_provider = IterListProvider([PollinationsAI, HuggingSpace, HuggingChat])
562 best_provider = IterListProvider([HuggingSpace, HuggingChat])
565 563 )
566 564
567 565 command_r7b = Model(
@@ -693,7 +691,7 @@ qwen_3_0_6b = Model(
693 691 qwq_32b = Model(
694 692 name = 'qwq-32b',
695 693 base_provider = 'Qwen',
696 best_provider = IterListProvider([DeepInfraChat, PollinationsAI, Together, HuggingChat])
694 best_provider = IterListProvider([DeepInfraChat, Together, HuggingChat])
697 695 )
698 696
699 697 ### DeepSeek ###
@@ -720,7 +718,7 @@ deepseek_r1_turbo = Model(
720 718 deepseek_r1_distill_llama_70b = Model(
721 719 name = 'deepseek-r1-distill-llama-70b',
722 720 base_provider = 'DeepSeek',
723 best_provider = IterListProvider([DeepInfraChat, Together, PollinationsAI])
721 best_provider = IterListProvider([DeepInfraChat, Together])
724 722 )
725 723
726 724 deepseek_r1_distill_qwen_1_5b = Model(
@@ -738,7 +736,7 @@ deepseek_r1_distill_qwen_14b = Model(
738 736 deepseek_r1_distill_qwen_32b = Model(
739 737 name = 'deepseek-r1-distill-qwen-32b',
740 738 base_provider = 'DeepSeek',
741 best_provider = IterListProvider([DeepInfraChat, PollinationsAI])
739 best_provider = IterListProvider([DeepInfraChat])
742 740 )
743 741
744 742 # deepseek-v2
Modified g4f/tools/media.py +4 -1
@@ -56,6 +56,7 @@ def render_part(part: dict) -> dict:
56 56
57 57 def merge_media(media: list, messages: list) -> Iterator:
58 58 buffer = []
59 # Read media from the last user message
59 60 for message in messages:
60 61 if message.get("role") == "user":
61 62 content = message.get("content")
@@ -90,13 +91,15 @@ def render_messages(messages: Messages, media: list = None) -> Iterator:
90 91 last_is_assistant = True
91 92 else:
92 93 last_is_assistant = False
93 if isinstance(message["content"], list):
94 # Render content parts
95 if isinstance(message.get("content"), list):
94 96 parts = [render_part(part) for part in message["content"] if part]
95 97 yield {
96 98 **message,
97 99 "content": [part for part in parts if part]
98 100 }
99 101 else:
102 # Append media to the last message
100 103 if media is not None and idx == len(messages) - 1:
101 104 yield {
102 105 **message,