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

XFEstudio/gpt4free

feat: add LM Arena provider, async‑ify Copilot & surface follow‑up suggestions

* **Provider/Blackbox.py** * Raise `RateLimitError` when `"You have reached your request limit for the hour"` substring is detected * **Provider/Copilot.py** * Convert class to `AsyncGeneratorProvider`; rename `create_completion` → `create_async_generator` * Swap `curl_cffi.requests.Session` for `AsyncSession`; reduce default timeout to **30 s** * Fully async websocket flow (`await session.ws_connect`, `await wss.send/recv/close`) * Emit new response types: `TitleGeneration`, `SourceLink`, aggregated `Sources` * Track request completion with `done` flag; collect citations in `sources` dict * **Provider/DuckDuckGo.py** * Replace `duckduckgo_search.DDGS` with `duckai.DuckAI` * Change base class to `AbstractProvider`; drop nodriver‑based auth * **Provider/PollinationsAI.py** * Re‑build text/audio model lists ensuring uniqueness; remove unused `extra_text_models` * Fix image seed logic (`i==1` for first retry); propagate streaming `error` field via `ResponseError` * **Provider/hf_space** * **New file** `LMArenaProvider.py` implementing async queue/stream client * Register `LMArenaProvider` in `hf_space/__init__.py`; delete `G4F` import * **Provider/needs_auth/CopilotAccount.py** * Inherit order changed to `Copilot, AsyncAuthedProvider` * Refactor token & cookie propagation; add `cookies_to_dict` helper * **Provider/needs_auth/OpenaiChat.py** * Parse reasoning thoughts/summary; yield `Reasoning` responses * Tighten access‑token validation and nodriver JS evaluations (`return_by_value`) * Extend `Conversation` with `p` and `thoughts_summary` * **providers/response.py** * Add `SourceLink` response class returning single formatted citation link * **providers/base_provider.py** * Serialize `AuthResult` with custom `json.dump` to handle non‑serializable fields * Gracefully skip empty cache files when loading auth data * **image/copy_images.py** * Ignore file extensions longer than 4 chars when inferring type * **requests/__init__.py** * Use `return_by_value=True` for `navigator.userAgent` extraction * **models.py** * Remove `G4F` from model provider lists; update `janus_pro_7b` best providers * **GUI server/api.py** * Stream `SuggestedFollowups` to client (`"suggestions"` event) * **GUI static assets** * **style.css**: bold chat title, add `.suggestions` styles, remove padding from `.chat-body` * **chat.v1.js** * Capture `suggestions` packets, render buttons, and send as quick replies * Re‑order finish‑reason logic; adjust token count placement and system‑prompt toggling * **chat-top-panel / footer** interactions updated accordingly * **gui/client/static/js/chat.v1.js** & **css** further UI refinements (scroll handling, token counting, hide prompt toggle) * Minor updates across multiple files to match new async interfaces and headers (`userAgent`, `raise_for_status`)

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

代码差异

19 个文件 +473 -212
Modified etc/tool/commit.py +17 -13
@@ -26,6 +26,10 @@ from typing import Optional, Dict, Any, List, Tuple
26 26
27 27 from g4f.client import Client
28 28 from g4f.models import ModelUtils
29 import g4f.Provider
30
31 from g4f import debug
32 debug.logging = True
29 33
30 34 # Constants
31 35 DEFAULT_MODEL = "claude-3.7-sonnet"
@@ -184,16 +188,21 @@ def generate_commit_message(diff_text: str, model: str = DEFAULT_MODEL) -> Optio
184 188
185 189 # Make API call
186 190 response = client.chat.completions.create(
191 prompt,
187 192 model=model,
188 messages=[{"role": "user", "content": prompt}]
193 stream=True,
189 194 )
190
191 # Stop spinner and clear line
192 spinner.set()
193 sys.stdout.write("\r" + " " * 50 + "\r")
194 sys.stdout.flush()
195
196 return response.choices[0].message.content.strip()
195 content = []
196 for chunk in response:
197 # Stop spinner and clear line
198 if spinner:
199 spinner.set()
200 print(" " * 50 + "\n", flush=True)
201 spinner = None
202 if isinstance(chunk.choices[0].delta.content, str):
203 content.append(chunk.choices[0].delta.content)
204 print(chunk.choices[0].delta.content, end="", flush=True)
205 return "".join(content).strip()
197 206 except Exception as e:
198 207 # Stop spinner if it's running
199 208 if 'spinner' in locals() and spinner:
@@ -306,11 +315,6 @@ def main():
306 315 print("Failed to generate commit message after multiple attempts.")
307 316 sys.exit(1)
308 317
309 print("\nGenerated commit message:")
310 print("-" * 50)
311 print(commit_message)
312 print("-" * 50)
313
314 318 if args.edit:
315 319 print("\nOpening editor to modify commit message...")
316 320 commit_message = edit_commit_message(commit_message)
Modified g4f/Provider/Blackbox.py +3 -3
@@ -19,7 +19,7 @@ from ..cookies import get_cookies_dir
19 19 from .helper import format_image_prompt
20 20 from ..providers.response import JsonConversation, ImageResponse
21 21 from ..tools.media import merge_media
22 from ..errors import PaymentRequiredError
22 from ..errors import RateLimitError
23 23 from .. import debug
24 24
25 25 class Conversation(JsonConversation):
@@ -690,8 +690,8 @@ class Blackbox(AsyncGeneratorProvider, ProviderModelMixin):
690 690 async for chunk in response.content.iter_any():
691 691 if chunk:
692 692 chunk_text = chunk.decode()
693 if chunk_text == "You have reached your request limit for the hour":
694 raise PaymentRequiredError(chunk_text)
693 if "You have reached your request limit for the hour" in chunk_text:
694 raise RateLimitError(chunk_text)
695 695 full_response.append(chunk_text)
696 696 # Only yield chunks for non-image models
697 697 if model != cls.default_image_model:
Modified g4f/Provider/Copilot.py +42 -61
@@ -3,11 +3,10 @@ from __future__ import annotations
3 3 import os
4 4 import json
5 5 import asyncio
6 import base64
7 6 from urllib.parse import quote
8 7
9 8 try:
10 from curl_cffi.requests import Session
9 from curl_cffi.requests import AsyncSession
11 10 from curl_cffi import CurlWsFlag
12 11 has_curl_cffi = True
13 12 except ImportError:
@@ -18,14 +17,12 @@ try:
18 17 except ImportError:
19 18 has_nodriver = False
20 19
21 from .base_provider import AbstractProvider, ProviderModelMixin
20 from .base_provider import AsyncGeneratorProvider, ProviderModelMixin
22 21 from .helper import format_prompt_max_length
23 22 from .openai.har_file import get_headers, get_har_files
24 from ..typing import CreateResult, Messages, MediaListType
23 from ..typing import AsyncResult, Messages, MediaListType
25 24 from ..errors import MissingRequirementsError, NoValidHarFileError, MissingAuthError
26 from ..requests.raise_for_status import raise_for_status
27 from ..providers.response import BaseConversation, JsonConversation, RequestLogin, ImageResponse, FinishReason, SuggestedFollowups
28 from ..providers.asyncio import get_running_loop
25 from ..providers.response import BaseConversation, JsonConversation, RequestLogin, ImageResponse, FinishReason, SuggestedFollowups, TitleGeneration, Sources, SourceLink
29 26 from ..tools.media import merge_media
30 27 from ..requests import get_nodriver
31 28 from ..image import to_bytes, is_accepted_format
@@ -38,7 +35,7 @@ class Conversation(JsonConversation):
38 35 def __init__(self, conversation_id: str):
39 36 self.conversation_id = conversation_id
40 37
41 class Copilot(AbstractProvider, ProviderModelMixin):
38 class Copilot(AsyncGeneratorProvider, ProviderModelMixin):
42 39 label = "Microsoft Copilot"
43 40 url = "https://copilot.microsoft.com"
44 41
@@ -62,20 +59,20 @@ class Copilot(AbstractProvider, ProviderModelMixin):
62 59 _cookies: dict = None
63 60
64 61 @classmethod
65 def create_completion(
62 async def create_async_generator(
66 63 cls,
67 64 model: str,
68 65 messages: Messages,
69 66 stream: bool = False,
70 67 proxy: str = None,
71 timeout: int = 900,
68 timeout: int = 30,
72 69 prompt: str = None,
73 70 media: MediaListType = None,
74 71 conversation: BaseConversation = None,
75 72 return_conversation: bool = False,
76 73 api_key: str = None,
77 74 **kwargs
78 ) -> CreateResult:
75 ) -> AsyncResult:
79 76 if not has_curl_cffi:
80 77 raise MissingRequirementsError('Install or update "curl_cffi" package | pip install -U curl_cffi')
81 78 model = cls.get_model(model)
@@ -91,14 +88,13 @@ class Copilot(AbstractProvider, ProviderModelMixin):
91 88 debug.log(f"Copilot: {h}")
92 89 if has_nodriver:
93 90 yield RequestLogin(cls.label, os.environ.get("G4F_LOGIN_URL", ""))
94 get_running_loop(check_nested=True)
95 cls._access_token, cls._cookies = asyncio.run(get_access_token_and_cookies(cls.url, proxy))
91 cls._access_token, cls._cookies = await get_access_token_and_cookies(cls.url, proxy)
96 92 else:
97 93 raise h
98 94 websocket_url = f"{websocket_url}&accessToken={quote(cls._access_token)}"
99 95 headers = {"authorization": f"Bearer {cls._access_token}"}
100 96
101 with Session(
97 async with AsyncSession(
102 98 timeout=timeout,
103 99 proxy=proxy,
104 100 impersonate="chrome",
@@ -107,31 +103,17 @@ class Copilot(AbstractProvider, ProviderModelMixin):
107 103 ) as session:
108 104 if cls._access_token is not None:
109 105 cls._cookies = session.cookies.jar if hasattr(session.cookies, "jar") else session.cookies
110 # if cls._access_token is None:
111 # try:
112 # url = "https://copilot.microsoft.com/cl/eus-sc/collect"
113 # headers = {
114 # "Accept": "application/x-clarity-gzip",
115 # "referrer": "https://copilot.microsoft.com/onboarding"
116 # }
117 # response = session.post(url, headers=headers, data=get_clarity())
118 # clarity_token = json.loads(response.text.split(" ", maxsplit=1)[-1])[0]["value"]
119 # debug.log(f"Copilot: Clarity Token: ...{clarity_token[-12:]}")
120 # except Exception as e:
121 # debug.log(f"Copilot: {e}")
122 # else:
123 # clarity_token = None
124 response = session.get("https://copilot.microsoft.com/c/api/user")
106 response = await session.get("https://copilot.microsoft.com/c/api/user")
125 107 if response.status_code == 401:
126 108 raise MissingAuthError("Status 401: Invalid access token")
127 raise_for_status(response)
109 response.raise_for_status()
128 110 user = response.json().get('firstName')
129 111 if user is None:
130 112 cls._access_token = None
131 113 debug.log(f"Copilot: User: {user or 'null'}")
132 114 if conversation is None:
133 response = session.post(cls.conversation_url)
134 raise_for_status(response)
115 response = await session.post(cls.conversation_url)
116 response.raise_for_status()
135 117 conversation_id = response.json().get("id")
136 118 conversation = Conversation(conversation_id)
137 119 if return_conversation:
@@ -146,28 +128,24 @@ class Copilot(AbstractProvider, ProviderModelMixin):
146 128 debug.log(f"Copilot: Use conversation: {conversation_id}")
147 129
148 130 uploaded_images = []
149 media, _ = [(None, None), *merge_media(media, messages)].pop()
150 if media:
131 for media, _ in merge_media(media, messages):
151 132 if not isinstance(media, str):
152 133 data = to_bytes(media)
153 response = session.post(
134 response = await session.post(
154 135 "https://copilot.microsoft.com/c/api/attachments",
155 headers={"content-type": is_accepted_format(data)},
136 headers={
137 "content-type": is_accepted_format(data),
138 "content-length": str(len(data)),
139 },
156 140 data=data
157 141 )
158 raise_for_status(response)
142 response.raise_for_status()
159 143 media = response.json().get("url")
160 144 uploaded_images.append({"type":"image", "url": media})
161 145
162 wss = session.ws_connect(cls.websocket_url)
163 # if clarity_token is not None:
164 # wss.send(json.dumps({
165 # "event": "challengeResponse",
166 # "token": clarity_token,
167 # "method":"clarity"
168 # }).encode(), CurlWsFlag.TEXT)
169 wss.send(json.dumps({"event":"setOptions","supportedCards":["weather","local","image","sports","video","ads","finance"],"ads":{"supportedTypes":["multimedia","product","tourActivity","propertyPromotion","text"]}}));
170 wss.send(json.dumps({
146 wss = await session.ws_connect(cls.websocket_url, timeout=3)
147 await wss.send(json.dumps({"event":"setOptions","supportedCards":["weather","local","image","sports","video","ads","finance"],"ads":{"supportedTypes":["multimedia","product","tourActivity","propertyPromotion","text"]}}));
148 await wss.send(json.dumps({
171 149 "event": "send",
172 150 "conversationId": conversation_id,
173 151 "content": [*uploaded_images, {
@@ -177,20 +155,20 @@ class Copilot(AbstractProvider, ProviderModelMixin):
177 155 "mode": "reasoning" if "Think" in model else "chat",
178 156 }).encode(), CurlWsFlag.TEXT)
179 157
180 is_started = False
158 done = False
181 159 msg = None
182 160 image_prompt: str = None
183 161 last_msg = None
162 sources = {}
184 163 try:
185 while True:
164 while not wss.closed:
186 165 try:
187 msg = wss.recv()[0]
188 msg = json.loads(msg)
166 msg = await asyncio.wait_for(wss.recv(), 3 if done else timeout)
167 msg = json.loads(msg[0])
189 168 except:
190 169 break
191 170 last_msg = msg
192 171 if msg.get("event") == "appendText":
193 is_started = True
194 172 yield msg.get("text")
195 173 elif msg.get("event") == "generatingImage":
196 174 image_prompt = msg.get("prompt")
@@ -198,20 +176,28 @@ class Copilot(AbstractProvider, ProviderModelMixin):
198 176 yield ImageResponse(msg.get("url"), image_prompt, {"preview": msg.get("thumbnailUrl")})
199 177 elif msg.get("event") == "done":
200 178 yield FinishReason("stop")
201 break
179 done = True
202 180 elif msg.get("event") == "suggestedFollowups":
203 181 yield SuggestedFollowups(msg.get("suggestions"))
204 182 break
205 183 elif msg.get("event") == "replaceText":
206 184 yield msg.get("text")
185 elif msg.get("event") == "titleUpdate":
186 yield TitleGeneration(msg.get("title"))
187 elif msg.get("event") == "citation":
188 sources[msg.get("url")] = msg
189 yield SourceLink(list(sources.keys()).index(msg.get("url")), msg.get("url"))
207 190 elif msg.get("event") == "error":
208 191 raise RuntimeError(f"Error: {msg}")
209 elif msg.get("event") not in ["received", "startMessage", "citation", "partCompleted"]:
192 elif msg.get("event") not in ["received", "startMessage", "partCompleted"]:
210 193 debug.log(f"Copilot Message: {msg}")
211 if not is_started:
194 if not done:
212 195 raise RuntimeError(f"Invalid response: {last_msg}")
196 if sources:
197 yield Sources(sources.values())
213 198 finally:
214 wss.close()
199 if not wss.closed:
200 await wss.close()
215 201
216 202 async def get_access_token_and_cookies(url: str, proxy: str = None, target: str = "ChatAI",):
217 203 browser, stop_browser = await get_nodriver(proxy=proxy, user_data_dir="copilot")
@@ -263,9 +249,4 @@ def readHAR(url: str):
263 249 if api_key is None:
264 250 raise NoValidHarFileError("No access token found in .har files")
265 251
266 return api_key, cookies
267
268 def get_clarity() -> bytes:
269 #{"e":["0.7.58",5,7284,4779,"n59ae4ieqq","aln5en","1upufhz",1,0,0],"a":[[7323,12,65,217,324],[7344,12,65,214,329],[7385,12,65,211,334],[7407,12,65,210,337],[7428,12,65,209,338],[7461,12,65,209,339],[7497,12,65,209,339],[7531,12,65,208,340],[7545,12,65,208,342],[11654,13,65,208,342],[11728,14,65,208,342],[11728,9,65,208,342,17535,19455,0,0,0,"Annehmen",null,"52w7wqv1r.8ovjfyrpu",1],[7284,4,1,393,968,393,968,0,0,231,310,939,0],[12063,0,2,147,3,4,4,18,5,1,10,79,25,15],[12063,36,6,[11938,0]]]}
270 body = base64.b64decode("H4sIAAAAAAAAA23RwU7DMAwG4HfJ2aqS2E5ibjxH1cMOnQYqYZvUTQPx7vyJRGGAemj01XWcP+9udg+j80MetDhSyrEISc5GrqrtZnmaTydHbrdUnSsWYT2u+8Obo0Ce/IQvaDBmjkwhUlKKIRNHmQgosqEArWPRDQMx90rxeUMPzB1j+UJvwNIxhTvsPcXyX1T+rizE4juK3mEEhpAUg/JvzW1/+U/tB1LATmhqotoiweMea50PLy2vui4LOY3XfD1dwnkor5fn/e18XBFgm6fHjSzZmCyV7d3aRByAEYextaTHEH3i5pgKGVP/s+DScE5PuLKIpW6FnCi1gY3Rbpqmj0/DI/+L7QEAAA==")
271 return body
252 return api_key, cookies
Modified g4f/Provider/DuckDuckGo.py +12 -46
@@ -3,28 +3,21 @@ from __future__ import annotations
3 3 import asyncio
4 4
5 5 try:
6 from duckduckgo_search import DDGS
7 from duckduckgo_search.exceptions import DuckDuckGoSearchException, RatelimitException, ConversationLimitException
6 from duckai import DuckAI
8 7 has_requirements = True
9 8 except ImportError:
10 9 has_requirements = False
11 try:
12 import nodriver
13 has_nodriver = True
14 except ImportError:
15 has_nodriver = False
16 10
17 from ..typing import AsyncResult, Messages
18 from ..requests import get_nodriver
19 from .base_provider import AsyncGeneratorProvider, ProviderModelMixin
11 from ..typing import CreateResult, Messages
12 from .base_provider import AbstractProvider, ProviderModelMixin
20 13 from .helper import get_last_user_message
21 14
22 class DuckDuckGo(AsyncGeneratorProvider, ProviderModelMixin):
15 class DuckDuckGo(AbstractProvider, ProviderModelMixin):
23 16 label = "Duck.ai (duckduckgo_search)"
24 17 url = "https://duckduckgo.com/aichat"
25 18 api_base = "https://duckduckgo.com/duckchat/v1/"
26 19
27 working = False
20 working = has_requirements
28 21 supports_stream = True
29 22 supports_system_message = True
30 23 supports_message_history = True
@@ -32,7 +25,7 @@ class DuckDuckGo(AsyncGeneratorProvider, ProviderModelMixin):
32 25 default_model = "gpt-4o-mini"
33 26 models = [default_model, "meta-llama/Llama-3.3-70B-Instruct-Turbo", "claude-3-haiku-20240307", "o3-mini", "mistralai/Mistral-Small-24B-Instruct-2501"]
34 27
35 ddgs: DDGS = None
28 duck_ai: DuckAI = None
36 29
37 30 model_aliases = {
38 31 "gpt-4": "gpt-4o-mini",
@@ -42,44 +35,17 @@ class DuckDuckGo(AsyncGeneratorProvider, ProviderModelMixin):
42 35 }
43 36
44 37 @classmethod
45 async def create_async_generator(
38 def create_completion(
46 39 cls,
47 40 model: str,
48 41 messages: Messages,
49 42 proxy: str = None,
50 43 timeout: int = 60,
51 44 **kwargs
52 ) -> AsyncResult:
45 ) -> CreateResult:
53 46 if not has_requirements:
54 raise ImportError("duckduckgo_search is not installed. Install it with `pip install duckduckgo-search`.")
55 if cls.ddgs is None:
56 cls.ddgs = DDGS(proxy=proxy, timeout=timeout)
57 if has_nodriver:
58 await cls.nodriver_auth(proxy=proxy)
47 raise ImportError("duckai is not installed. Install it with `pip install -U duckai`.")
48 if cls.duck_ai is None:
49 cls.duck_ai = DuckAI(proxy=proxy, timeout=timeout)
59 50 model = cls.get_model(model)
60 for chunk in cls.ddgs.chat_yield(get_last_user_message(messages), model, timeout):
61 yield chunk
62
63 @classmethod
64 async def nodriver_auth(cls, proxy: str = None):
65 browser, stop_browser = await get_nodriver(proxy=proxy)
66 try:
67 page = browser.main_tab
68 def on_request(event: nodriver.cdp.network.RequestWillBeSent, page=None):
69 if cls.api_base in event.request.url:
70 if "X-Vqd-4" in event.request.headers:
71 cls.ddgs._chat_vqd = event.request.headers["X-Vqd-4"]
72 if "X-Vqd-Hash-1" in event.request.headers:
73 cls.ddgs._chat_vqd_hash = event.request.headers["X-Vqd-Hash-1"]
74 if "F-Fe-Version" in event.request.headers:
75 cls.ddgs._chat_xfe = event.request.headers["F-Fe-Version" ]
76 await page.send(nodriver.cdp.network.enable())
77 page.add_handler(nodriver.cdp.network.RequestWillBeSent, on_request)
78 page = await browser.get(cls.url)
79 while True:
80 if cls.ddgs._chat_vqd:
81 break
82 await asyncio.sleep(1)
83 await page.close()
84 finally:
85 stop_browser()
51 yield cls.duck_ai.chat(get_last_user_message(messages), model, timeout)
Modified g4f/Provider/PollinationsAI.py +19 -30
@@ -52,8 +52,8 @@ class PollinationsAI(AsyncGeneratorProvider, ProviderModelMixin):
52 52 audio_models = [default_audio_model]
53 53 extra_image_models = ["flux-pro", "flux-dev", "flux-schnell", "midjourney", "dall-e-3", "turbo"]
54 54 vision_models = [default_vision_model, "gpt-4o-mini", "openai", "openai-large", "searchgpt"]
55 extra_text_models = vision_models
56 55 _models_loaded = False
56 # https://github.com/pollinations/pollinations/blob/master/text.pollinations.ai/generateTextPortkey.js#L15
57 57 model_aliases = {
58 58 ### Text Models ###
59 59 "gpt-4o-mini": "openai",
@@ -100,43 +100,32 @@ class PollinationsAI(AsyncGeneratorProvider, ProviderModelMixin):
100 100
101 101 cls.image_models = all_image_models
102 102
103 # Update of text models
104 103 text_response = requests.get("https://text.pollinations.ai/models")
105 104 text_response.raise_for_status()
106 105 models = text_response.json()
107
108 # Purpose of text models
109 cls.text_models = [
110 model.get("name")
111 for model in models
112 if "input_modalities" in model and "text" in model["input_modalities"]
113 ]
114 106
115 107 # Purpose of audio models
116 108 cls.audio_models = {
117 109 model.get("name"): model.get("voices")
118 110 for model in models
119 if model.get("audio")
111 if "output_modalities" in model and "audio" in model["output_modalities"]
120 112 }
121 113
122 114 # Create a set of unique text models starting with default model
123 unique_text_models = {cls.default_model}
124
115 unique_text_models = cls.text_models.copy()
116
125 117 # Add models from vision_models
126 unique_text_models.update(cls.vision_models)
127
118 unique_text_models.extend(cls.vision_models)
119
128 120 # Add models from the API response
129 121 for model in models:
130 122 model_name = model.get("name")
131 123 if model_name and "input_modalities" in model and "text" in model["input_modalities"]:
132 unique_text_models.add(model_name)
133
124 unique_text_models.append(model_name)
125
134 126 # Convert to list and update text_models
135 cls.text_models = list(unique_text_models)
136
137 # Update extra_text_models with unique vision models
138 cls.extra_text_models = [model for model in cls.vision_models if model != cls.default_model]
139
127 cls.text_models = list(dict.fromkeys(unique_text_models))
128
140 129 cls._models_loaded = True
141 130
142 131 except Exception as e:
@@ -148,12 +137,10 @@ class PollinationsAI(AsyncGeneratorProvider, ProviderModelMixin):
148 137 debug.error(f"Failed to fetch models: {e}")
149 138
150 139 # Return unique models across all categories
151 all_models = set(cls.text_models)
152 all_models.update(cls.image_models)
153 all_models.update(cls.audio_models.keys())
154 result = list(all_models)
155 return result
156
140 all_models = cls.text_models.copy()
141 all_models.extend(cls.image_models)
142 all_models.extend(cls.audio_models.keys())
143 return list(dict.fromkeys(all_models))
157 144
158 145 @classmethod
159 146 async def create_async_generator(
@@ -265,15 +252,15 @@ class PollinationsAI(AsyncGeneratorProvider, ProviderModelMixin):
265 252 query = "&".join(f"{k}={quote_plus(str(v))}" for k, v in params.items() if v is not None)
266 253 prompt = quote_plus(prompt)[:2048-256-len(query)]
267 254 url = f"{cls.image_api_endpoint}prompt/{prompt}?{query}"
268 def get_image_url(i: int = 0, seed: Optional[int] = None):
269 if i == 0:
255 def get_image_url(i: int, seed: Optional[int] = None):
256 if i == 1:
270 257 if not cache and seed is None:
271 258 seed = random.randint(0, 2**32)
272 259 else:
273 260 seed = random.randint(0, 2**32)
274 261 return f"{url}&seed={seed}" if seed else url
275 262 async with ClientSession(headers=DEFAULT_HEADERS, connector=get_connector(proxy=proxy)) as session:
276 async def get_image(i: int = 0, seed: Optional[int] = None):
263 async def get_image(i: int, seed: Optional[int] = None):
277 264 async with session.get(get_image_url(i, seed), allow_redirects=False) as response:
278 265 try:
279 266 await raise_for_status(response)
@@ -343,6 +330,8 @@ class PollinationsAI(AsyncGeneratorProvider, ProviderModelMixin):
343 330 if line[6:].startswith(b"[DONE]"):
344 331 break
345 332 result = json.loads(line[6:])
333 if "error" in result:
334 raise ResponseError(result["error"].get("message", result["error"]))
346 335 if "usage" in result:
347 336 yield Usage(**result["usage"])
348 337 choices = result.get("choices", [{}])
Added g4f/Provider/hf_space/LMArenaProvider.py +251 -0
@@ -0,0 +1,251 @@
1 from __future__ import annotations
2
3 import json
4 import uuid
5 import asyncio
6
7 from ...typing import AsyncResult, Messages
8 from ...requests import StreamSession, raise_for_status
9 from ...providers.response import FinishReason
10 from ..base_provider import AsyncGeneratorProvider, ProviderModelMixin, AuthFileMixin
11 from ..helper import format_prompt
12 from ... import debug
13
14 class LMArenaProvider(AsyncGeneratorProvider, ProviderModelMixin, AuthFileMixin):
15 label = "LM Arena"
16 url = "https://lmarena.ai"
17 api_endpoint = "/queue/join?"
18
19 working = True
20
21 default_model = "chatgpt-4o-latest-20250326"
22 model_aliases = {"gpt-4o": default_model}
23 models = [
24 default_model,
25 "gpt-4.1-2025-04-14",
26 "gemini-2.5-pro-exp-03-25",
27 "llama-4-maverick-03-26-experimental",
28 "grok-3-preview-02-24",
29 "claude-3-7-sonnet-20250219",
30 "claude-3-7-sonnet-20250219-thinking-32k",
31 "deepseek-v3-0324",
32 "llama-4-maverick-17b-128e-instruct",
33 "gpt-4.1-mini-2025-04-14",
34 "gpt-4.1-nano-2025-04-14",
35 "gemini-2.0-flash-thinking-exp-01-21",
36 "gemini-2.0-flash-001",
37 "gemini-2.0-flash-lite-preview-02-05",
38 "gemma-3-27b-it",
39 "gemma-3-12b-it",
40 "gemma-3-4b-it",
41 "deepseek-r1",
42 "claude-3-5-sonnet-20241022",
43 "o3-mini",
44 "llama-3.3-70b-instruct",
45 "gpt-4o-mini-2024-07-18",
46 "gpt-4o-2024-11-20",
47 "gpt-4o-2024-08-06",
48 "gpt-4o-2024-05-13",
49 "command-a-03-2025",
50 "qwq-32b",
51 "p2l-router-7b",
52 "claude-3-5-haiku-20241022",
53 "claude-3-5-sonnet-20240620",
54 "doubao-1.5-pro-32k-250115",
55 "doubao-1.5-vision-pro-32k-250115",
56 "mistral-small-24b-instruct-2501",
57 "phi-4",
58 "amazon-nova-pro-v1.0",
59 "amazon-nova-lite-v1.0",
60 "amazon-nova-micro-v1.0",
61 "cobalt-exp-beta-v3",
62 "cobalt-exp-beta-v4",
63 "qwen-max-2025-01-25",
64 "qwen-plus-0125-exp",
65 "qwen2.5-vl-32b-instruct",
66 "qwen2.5-vl-72b-instruct",
67 "gemini-1.5-pro-002",
68 "gemini-1.5-flash-002",
69 "gemini-1.5-flash-8b-001",
70 "gemini-1.5-pro-001",
71 "gemini-1.5-flash-001",
72 "llama-3.1-405b-instruct-bf16",
73 "llama-3.3-nemotron-49b-super-v1",
74 "llama-3.1-nemotron-ultra-253b-v1",
75 "llama-3.1-nemotron-70b-instruct",
76 "llama-3.1-70b-instruct",
77 "llama-3.1-8b-instruct",
78 "hunyuan-standard-2025-02-10",
79 "hunyuan-large-2025-02-10",
80 "hunyuan-standard-vision-2024-12-31",
81 "hunyuan-turbo-0110",
82 "hunyuan-turbos-20250226",
83 "mistral-large-2411",
84 "pixtral-large-2411",
85 "mistral-large-2407",
86 "llama-3.1-nemotron-51b-instruct",
87 "granite-3.1-8b-instruct",
88 "granite-3.1-2b-instruct",
89 "step-2-16k-exp-202412",
90 "step-2-16k-202502",
91 "step-1o-vision-32k-highres",
92 "yi-lightning",
93 "glm-4-plus",
94 "glm-4-plus-0111",
95 "jamba-1.5-large",
96 "jamba-1.5-mini",
97 "gemma-2-27b-it",
98 "gemma-2-9b-it",
99 "gemma-2-2b-it",
100 "eureka-chatbot",
101 "claude-3-haiku-20240307",
102 "claude-3-sonnet-20240229",
103 "claude-3-opus-20240229",
104 "nemotron-4-340b",
105 "llama-3-70b-instruct",
106 "llama-3-8b-instruct",
107 "qwen2.5-plus-1127",
108 "qwen2.5-coder-32b-instruct",
109 "qwen2.5-72b-instruct",
110 "qwen-max-0919",
111 "qwen-vl-max-1119",
112 "qwen-vl-max-0809",
113 "llama-3.1-tulu-3-70b",
114 "olmo-2-0325-32b-instruct",
115 "gpt-3.5-turbo-0125",
116 "reka-core-20240904",
117 "reka-flash-20240904",
118 "c4ai-aya-expanse-32b",
119 "c4ai-aya-expanse-8b",
120 "c4ai-aya-vision-32b",
121 "command-r-plus-08-2024",
122 "command-r-08-2024",
123 "codestral-2405",
124 "mixtral-8x22b-instruct-v0.1",
125 "mixtral-8x7b-instruct-v0.1",
126 "pixtral-12b-2409",
127 "ministral-8b-2410"]
128
129 _args: dict = None
130
131 @staticmethod
132 def _random_session_hash():
133 return str(uuid.uuid4())
134
135 @classmethod
136 def _build_payloads(cls, model_id: str, session_hash: str, messages: Messages, max_tokens: int, temperature: float, top_p: float):
137 first_payload = {
138 "data": [
139 None,
140 model_id,
141 {"text": format_prompt(messages), "files": []},
142 {
143 "text_models": [model_id],
144 "all_text_models": [model_id],
145 "vision_models": [],
146 "all_vision_models": [],
147 "image_gen_models": [],
148 "all_image_gen_models": [],
149 "search_models": [],
150 "all_search_models": [],
151 "models": [model_id],
152 "all_models": [model_id],
153 "arena_type": "text-arena"
154 }
155 ],
156 "event_data": None,
157 "fn_index": 117,
158 "trigger_id": 159,
159 "session_hash": session_hash
160 }
161
162 second_payload = {
163 "data": [],
164 "event_data": None,
165 "fn_index": 118,
166 "trigger_id": 159,
167 "session_hash": session_hash
168 }
169
170 third_payload = {
171 "data": [None, temperature, top_p, max_tokens],
172 "event_data": None,
173 "fn_index": 119,
174 "trigger_id": 159,
175 "session_hash": session_hash
176 }
177
178 return first_payload, second_payload, third_payload
179
180 @classmethod
181 async def create_async_generator(
182 cls, model: str, messages: Messages,
183 max_tokens: int = 2048,
184 temperature: float = 0.7,
185 top_p: float = 1,
186 proxy: str = None,
187 **kwargs
188 ) -> AsyncResult:
189 if not model:
190 model = cls.default_model
191 if model in cls.model_aliases:
192 model = cls.model_aliases[model]
193 session_hash = cls._random_session_hash()
194 headers = {
195 "Content-Type": "application/json",
196 "Accept": "application/json"
197 }
198 async with StreamSession(impersonate="chrome", headers=headers) as session:
199 first_payload, second_payload, third_payload = cls._build_payloads(model, session_hash, messages, max_tokens, temperature, top_p)
200 # Long stream GET
201 async def long_stream():
202 # POST 1
203 async with session.post(f"{cls.url}{cls.api_endpoint}", json=first_payload, proxy=proxy) as response:
204 await raise_for_status(response)
205
206 # POST 2
207 async with session.post(f"{cls.url}{cls.api_endpoint}", json=second_payload, proxy=proxy) as response:
208 await raise_for_status(response)
209
210 # POST 3
211 async with session.post(f"{cls.url}{cls.api_endpoint}", json=third_payload, proxy=proxy) as response:
212 await raise_for_status(response)
213
214 stream_url = f"{cls.url}/queue/data?session_hash={session_hash}"
215 async with session.get(stream_url, headers={"Accept": "text/event-stream"}, proxy=proxy) as response:
216 await raise_for_status(response)
217 text_position = 0
218 count = 0
219 async for line in response.iter_lines():
220 if line.startswith(b"data: "):
221 try:
222 msg = json.loads(line[6:])
223 except Exception as e:
224 raise RuntimeError(f"Failed to decode JSON from stream: {line}", e)
225 if msg.get("msg") == "process_generating":
226 data = msg["output"]["data"][1]
227 if data:
228 data = data[0]
229 if len(data) > 2:
230 if isinstance(data[2], list):
231 data[2] = data[2][-1]
232 content = data[2][text_position:].rstrip("▌")
233 if content:
234 count += 1
235 yield count, content
236 text_position += len(content)
237 elif msg.get("msg") == "close_stream":
238 break
239 elif msg.get("msg") not in ("process_completed", "process_starts", "estimation"):
240 debug.log(f"Unexpected message: {msg}")
241 count = 0
242 async for count, chunk in long_stream():
243 yield chunk
244 if count == 0:
245 await asyncio.sleep(10)
246 async for count, chunk in long_stream():
247 yield chunk
248 if count == 0:
249 raise RuntimeError("No response from server.")
250 if count == max_tokens:
251 yield FinishReason("length")
Modified g4f/Provider/hf_space/__init__.py +2 -2
@@ -10,7 +10,7 @@ from .BlackForestLabs_Flux1Dev import BlackForestLabs_Flux1Dev
10 10 from .BlackForestLabs_Flux1Schnell import BlackForestLabs_Flux1Schnell
11 11 from .CohereForAI_C4AI_Command import CohereForAI_C4AI_Command
12 12 from .DeepseekAI_JanusPro7b import DeepseekAI_JanusPro7b
13 from .G4F import G4F
13 from .LMArenaProvider import LMArenaProvider
14 14 from .Microsoft_Phi_4 import Microsoft_Phi_4
15 15 from .Qwen_QVQ_72B import Qwen_QVQ_72B
16 16 from .Qwen_Qwen_2_5 import Qwen_Qwen_2_5
@@ -33,7 +33,7 @@ class HuggingSpace(AsyncGeneratorProvider, ProviderModelMixin):
33 33 BlackForestLabs_Flux1Schnell,
34 34 CohereForAI_C4AI_Command,
35 35 DeepseekAI_JanusPro7b,
36 G4F,
36 LMArenaProvider,
37 37 Microsoft_Phi_4,
38 38 Qwen_QVQ_72B,
39 39 Qwen_Qwen_2_5,
Modified g4f/Provider/needs_auth/CopilotAccount.py +13 -13
@@ -10,10 +10,7 @@ from ...typing import AsyncResult, Messages
10 10 from ...errors import NoValidHarFileError
11 11 from ... import debug
12 12
13 def cookies_to_dict():
14 return Copilot._cookies if isinstance(Copilot._cookies, dict) else {c.name: c.value for c in Copilot._cookies}
15
16 class CopilotAccount(AsyncAuthedProvider, Copilot):
13 class CopilotAccount(Copilot, AsyncAuthedProvider):
17 14 needs_auth = True
18 15 use_nodriver = True
19 16 parent = "Copilot"
@@ -23,17 +20,17 @@ class CopilotAccount(AsyncAuthedProvider, Copilot):
23 20 @classmethod
24 21 async def on_auth_async(cls, proxy: str = None, **kwargs) -> AsyncIterator:
25 22 try:
26 Copilot._access_token, Copilot._cookies = readHAR(cls.url)
23 cls._access_token, cls._cookies = readHAR(cls.url)
27 24 except NoValidHarFileError as h:
28 25 debug.log(f"Copilot: {h}")
29 26 if has_nodriver:
30 27 yield RequestLogin(cls.label, os.environ.get("G4F_LOGIN_URL", ""))
31 Copilot._access_token, Copilot._cookies = await get_access_token_and_cookies(cls.url, proxy)
28 cls._access_token, cls._cookies = await get_access_token_and_cookies(cls.url, proxy)
32 29 else:
33 30 raise h
34 31 yield AuthResult(
35 api_key=Copilot._access_token,
36 cookies=cookies_to_dict()
32 api_key=cls._access_token,
33 cookies=cls.cookies_to_dict()
37 34 )
38 35
39 36 @classmethod
@@ -44,9 +41,12 @@ class CopilotAccount(AsyncAuthedProvider, Copilot):
44 41 auth_result: AuthResult,
45 42 **kwargs
46 43 ) -> AsyncResult:
47 Copilot._access_token = getattr(auth_result, "api_key")
48 Copilot._cookies = getattr(auth_result, "cookies")
49 Copilot.needs_auth = cls.needs_auth
50 for chunk in Copilot.create_completion(model, messages, **kwargs):
44 cls._access_token = getattr(auth_result, "api_key")
45 cls._cookies = getattr(auth_result, "cookies")
46 async for chunk in cls.create_async_generator(model, messages, **kwargs):
51 47 yield chunk
52 auth_result.cookies = cookies_to_dict()
48 auth_result.cookies = cls.cookies_to_dict()
49
50 @classmethod
51 def cookies_to_dict(cls):
52 return cls._cookies if isinstance(cls._cookies, dict) else {c.name: c.value for c in cls._cookies}
Modified g4f/Provider/needs_auth/MicrosoftDesigner.py +1 -1
@@ -146,7 +146,7 @@ async def get_access_token_and_user_agent(url: str, proxy: str = None):
146 146 browser, stop_browser = await get_nodriver(proxy=proxy, user_data_dir="designer")
147 147 try:
148 148 page = await browser.get(url)
149 user_agent = await page.evaluate("navigator.userAgent")
149 user_agent = await page.evaluate("navigator.userAgent", return_by_value=True)
150 150 access_token = None
151 151 while access_token is None:
152 152 access_token = await page.evaluate("""
Modified g4f/Provider/needs_auth/OpenaiChat.py +32 -13
@@ -489,6 +489,16 @@ class OpenaiChat(AsyncAuthedProvider, ProviderModelMixin):
489 489 if "type" in line:
490 490 if line["type"] == "title_generation":
491 491 yield TitleGeneration(line["title"])
492 fields.p = line.get("p", fields.p)
493 if fields.p.startswith("/message/content/thoughts"):
494 if fields.p.endswith("/content"):
495 if fields.thoughts_summary:
496 yield Reasoning(token="", status=fields.thoughts_summary)
497 fields.thoughts_summary = ""
498 yield Reasoning(token=line.get("v"))
499 elif fields.p.endswith("/summary"):
500 fields.thoughts_summary += line.get("v")
501 return
492 502 if "v" in line:
493 503 v = line.get("v")
494 504 if isinstance(v, str) and fields.is_recipient:
@@ -502,7 +512,7 @@ class OpenaiChat(AsyncAuthedProvider, ProviderModelMixin):
502 512 for entry in [p.get("entries") for p in m.get("v")]:
503 513 for link in entry:
504 514 sources.add_source(link)
505 elif re.match(r"^/message/metadata/content_references/\d+$", m.get("p")):
515 elif m.get("p") and re.match(r"^/message/metadata/content_references/\d+$", m.get("p")):
506 516 sources.add_source(m.get("v"))
507 517 elif m.get("p") == "/message/metadata/finished_text":
508 518 fields.is_thinking = False
@@ -578,11 +588,14 @@ class OpenaiChat(AsyncAuthedProvider, ProviderModelMixin):
578 588 cls._set_api_key(api_key)
579 589 else:
580 590 try:
581 await get_request_config(cls.request_config, proxy)
591 cls.request_config = await get_request_config(cls.request_config, proxy)
592 if cls.request_config is None:
593 cls.request_config = RequestConfig()
582 594 cls._create_request_args(cls.request_config.cookies, cls.request_config.headers)
583 if cls.request_config.access_token is not None or cls.needs_auth:
584 if not cls._set_api_key(cls.request_config.access_token):
585 raise NoValidHarFileError(f"Access token is not valid: {cls.request_config.access_token}")
595 if cls.needs_auth and cls.request_config.access_token is None:
596 raise NoValidHarFileError(f"Missing access token")
597 if not cls._set_api_key(cls.request_config.access_token):
598 raise NoValidHarFileError(f"Access token is not valid: {cls.request_config.access_token}")
586 599 except NoValidHarFileError:
587 600 if has_nodriver:
588 601 if cls._api_key is None:
@@ -622,15 +635,18 @@ class OpenaiChat(AsyncAuthedProvider, ProviderModelMixin):
622 635 await page.send(nodriver.cdp.network.enable())
623 636 page.add_handler(nodriver.cdp.network.RequestWillBeSent, on_request)
624 637 page = await browser.get(cls.url)
625 user_agent = await page.evaluate("window.navigator.userAgent")
626 await page.select("#prompt-textarea", 240)
627 await page.evaluate("document.getElementById('prompt-textarea').innerText = 'Hello'")
628 await page.select("[data-testid=\"send-button\"]", 30)
638 user_agent = await page.evaluate("window.navigator.userAgent", return_by_value=True)
639 while not await page.evaluate("document.getElementById('prompt-textarea').id"):
640 await asyncio.sleep(1)
641 while not await page.evaluate("document.querySelector('[data-testid=\"send-button\"]').type"):
642 await asyncio.sleep(1)
629 643 await page.evaluate("document.querySelector('[data-testid=\"send-button\"]').click()")
630 644 while True:
631 body = await page.evaluate("JSON.stringify(window.__remixContext)")
645 body = await page.evaluate("JSON.stringify(window.__remixContext)", return_by_value=True)
646 if hasattr(body, "value"):
647 body = body.value
632 648 if body:
633 match = re.search(r'"accessToken":"(.*?)"', body)
649 match = re.search(r'"accessToken":"(.+?)"', body)
634 650 if match:
635 651 cls._api_key = match.group(1)
636 652 break
@@ -674,6 +690,7 @@ class OpenaiChat(AsyncAuthedProvider, ProviderModelMixin):
674 690
675 691 @classmethod
676 692 def _set_api_key(cls, api_key: str):
693 cls._api_key = api_key
677 694 if api_key:
678 695 exp = api_key.split(".")[1]
679 696 exp = (exp + "=" * (4 - len(exp) % 4)).encode()
@@ -681,11 +698,11 @@ class OpenaiChat(AsyncAuthedProvider, ProviderModelMixin):
681 698 debug.log(f"OpenaiChat: API key expires at\n {cls._expires} we have:\n {time.time()}")
682 699 if time.time() > cls._expires:
683 700 debug.log(f"OpenaiChat: API key is expired")
701 return False
684 702 else:
685 cls._api_key = api_key
686 703 cls._headers["authorization"] = f"Bearer {api_key}"
687 704 return True
688 return False
705 return True
689 706
690 707 @classmethod
691 708 def _update_cookie_header(cls):
@@ -704,6 +721,8 @@ class Conversation(JsonConversation):
704 721 self.parent_message_id = message_id if parent_message_id is None else parent_message_id
705 722 self.user_id = user_id
706 723 self.is_thinking = is_thinking
724 self.p = None
725 self.thoughts_summary = ""
707 726
708 727 def get_cookies(
709 728 urls: Optional[Iterator[str]] = None
Modified g4f/Provider/openai/har_file.py +3 -4
@@ -86,8 +86,6 @@ def readHAR(request_config: RequestConfig):
86 86 request_config.cookies = {c['name']: c['value'] for c in v['request']['cookies']}
87 87 except Exception as e:
88 88 debug.log(f"Error on read headers: {e}")
89 if request_config.proof_token is None:
90 raise NoValidHarFileError("No proof_token found in .har files")
91 89
92 90 def get_headers(entry) -> dict:
93 91 return {h['name'].lower(): h['value'] for h in entry['request']['headers'] if h['name'].lower() not in ['content-length', 'cookie'] and not h['name'].startswith(':')}
@@ -152,8 +150,9 @@ def getN() -> str:
152 150 return base64.b64encode(timestamp.encode()).decode()
153 151
154 152 async def get_request_config(request_config: RequestConfig, proxy: str) -> RequestConfig:
155 if request_config.proof_token is None:
156 readHAR(request_config)
153 readHAR(request_config)
157 154 if request_config.arkose_request is not None:
158 155 request_config.arkose_token = await sendRequest(genArkReq(request_config.arkose_request), proxy)
156 if request_config.proof_token is None:
157 raise NoValidHarFileError("No proof_token found in .har files")
159 158 return request_config
Modified g4f/gui/client/static/css/style.css +17 -3
@@ -1533,6 +1533,7 @@ form textarea {
1533 1533 .chat-top-panel .convo-title {
1534 1534 margin: 0 10px;
1535 1535 font-size: 14px;
1536 font-weight: bold;
1536 1537 text-align: center;
1537 1538 flex: 1;
1538 1539 }
@@ -1581,7 +1582,6 @@ form textarea {
1581 1582 }
1582 1583 .chat-body {
1583 1584 flex: 1;
1584 padding: 10px;
1585 1585 overflow-y: auto;
1586 1586 display: flex;
1587 1587 flex-direction: column;
@@ -1613,11 +1613,25 @@ form textarea {
1613 1613 border: 1px dashed var(--conversations);
1614 1614 box-shadow: 1px 1px 1px 0px rgba(0,0,0,0.75);
1615 1615 }
1616 .white .chat-footer .send-buttons button {
1616 .suggestions {
1617 display: flex;
1618 gap: 6px;
1619 flex-wrap: wrap;
1620 }
1621 .suggestions .suggestion {
1622 background: var(--blur-bg);
1623 color: var(--colour-3);
1624 padding: 10px;
1625 margin: 0 2px 0 4px;
1626 border-radius: 5px;
1627 cursor: pointer;
1628 border: 1px dashed var(--conversations);
1629 }
1630 .white .chat-footer .send-buttons button, .white .suggestions .suggestion {
1617 1631 border-style: solid;
1618 1632 border-color: var(--blur-border);
1619 1633 }
1620 .chat-footer .send-buttons button:hover {
1634 .chat-footer .send-buttons button:hover, .suggestions .suggestion:hover {
1621 1635 border-style: solid;
1622 1636 box-shadow: none;
1623 1637 background-color: var(--button-hover);
Modified g4f/gui/client/static/js/chat.v1.js +35 -14
Modified g4f/gui/server/api.py +2 -0
Modified g4f/image/copy_images.py +1 -1
Modified g4f/models.py +2 -3
Modified g4f/providers/base_provider.py +11 -4
Modified g4f/providers/response.py +9 -0
Modified g4f/requests/__init__.py +1 -1