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

XFEstudio/gpt4free

Support TitleGeneration, Reasoning in HuggingChat Improve model list in HuggingSpace, PollinationsAI Fix Image Generation in PollinationsAI Add Image Upload in PollinationsAI Support Usage, FinishReason, jsonMode in PollinationsAI Add Reasoning to Web UI Fix using provider api_keys in Web UI

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

代码差异

15 个文件 +303 -181
Modified g4f/Provider/PollinationsAI.py +97 -106
@@ -3,42 +3,45 @@ from __future__ import annotations
3 3 import json
4 4 import random
5 5 import requests
6 from urllib.parse import quote
6 from urllib.parse import quote_plus
7 7 from typing import Optional
8 8 from aiohttp import ClientSession
9 9
10 from .helper import filter_none
10 11 from .base_provider import AsyncGeneratorProvider, ProviderModelMixin
12 from ..typing import AsyncResult, Messages, ImagesType
13 from ..image import to_data_uri
11 14 from ..requests.raise_for_status import raise_for_status
12 from ..typing import AsyncResult, Messages
13 from ..image import ImageResponse
15 from ..requests.aiohttp import get_connector
16 from ..providers.response import ImageResponse, FinishReason, Usage
17
18 DEFAULT_HEADERS = {
19 'Accept': '*/*',
20 'Accept-Language': 'en-US,en;q=0.9',
21 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36',
22 }
14 23
15 24 class PollinationsAI(AsyncGeneratorProvider, ProviderModelMixin):
16 25 label = "Pollinations AI"
17 26 url = "https://pollinations.ai"
18
27
19 28 working = True
20 29 supports_stream = False
21 30 supports_system_message = True
22 31 supports_message_history = True
23 32
24 # API endpoints base
25 api_base = "https://text.pollinations.ai/openai"
26
27 33 # API endpoints
28 text_api_endpoint = "https://text.pollinations.ai/"
34 text_api_endpoint = "https://text.pollinations.ai/openai"
29 35 image_api_endpoint = "https://image.pollinations.ai/"
30 36
31 37 # Models configuration
32 38 default_model = "openai"
33 39 default_image_model = "flux"
34
35 image_models = []
36 models = []
37
38 additional_models_image = ["midjourney", "dall-e-3"]
39 additional_models_text = ["claude", "karma", "command-r", "llamalight", "mistral-large", "sur", "sur-mistral"]
40 default_vision_model = "gpt-4o"
41 extra_image_models = ["midjourney", "dall-e-3"]
42 vision_models = [default_vision_model, "gpt-4o-mini"]
43 extra_text_models = [*vision_models, "claude", "karma", "command-r", "llamalight", "mistral-large", "sur", "sur-mistral"]
40 44 model_aliases = {
41 "gpt-4o": default_model,
42 45 "qwen-2-72b": "qwen",
43 46 "qwen-2.5-coder-32b": "qwen-coder",
44 47 "llama-3.3-70b": "llama",
@@ -50,22 +53,17 @@ class PollinationsAI(AsyncGeneratorProvider, ProviderModelMixin):
50 53 "deepseek-chat": "deepseek",
51 54 "llama-3.2-3b": "llamalight",
52 55 }
56 text_models = []
53 57
54 58 @classmethod
55 59 def get_models(cls, **kwargs):
56 # Initialize model lists if not exists
57 if not hasattr(cls, 'image_models'):
58 cls.image_models = []
59 if not hasattr(cls, 'text_models'):
60 cls.text_models = []
61
62 60 # Fetch image models if not cached
63 61 if not cls.image_models:
64 62 url = "https://image.pollinations.ai/models"
65 63 response = requests.get(url)
66 64 raise_for_status(response)
67 65 cls.image_models = response.json()
68 cls.image_models.extend(cls.additional_models_image)
66 cls.image_models.extend(cls.extra_image_models)
69 67
70 68 # Fetch text models if not cached
71 69 if not cls.text_models:
@@ -73,7 +71,7 @@ class PollinationsAI(AsyncGeneratorProvider, ProviderModelMixin):
73 71 response = requests.get(url)
74 72 raise_for_status(response)
75 73 cls.text_models = [model.get("name") for model in response.json()]
76 cls.text_models.extend(cls.additional_models_text)
74 cls.text_models.extend(cls.extra_text_models)
77 75
78 76 # Return combined models
79 77 return cls.text_models + cls.image_models
@@ -94,22 +92,27 @@ class PollinationsAI(AsyncGeneratorProvider, ProviderModelMixin):
94 92 enhance: bool = False,
95 93 safe: bool = False,
96 94 # Text specific parameters
97 temperature: float = 0.5,
98 presence_penalty: float = 0,
95 images: ImagesType = None,
96 temperature: float = None,
97 presence_penalty: float = None,
99 98 top_p: float = 1,
100 frequency_penalty: float = 0,
101 stream: bool = False,
99 frequency_penalty: float = None,
100 response_format: Optional[dict] = None,
101 cache: bool = False,
102 102 **kwargs
103 103 ) -> AsyncResult:
104 if images is not None and not model:
105 model = cls.default_vision_model
104 106 model = cls.get_model(model)
107 if not cache and seed is None:
108 seed = random.randint(0, 100000)
105 109
106 110 # Check if models
107 111 # Image generation
108 112 if model in cls.image_models:
109 async for result in cls._generate_image(
113 yield await cls._generate_image(
110 114 model=model,
111 messages=messages,
112 prompt=prompt,
115 prompt=messages[-1]["content"] if prompt is None else prompt,
113 116 proxy=proxy,
114 117 width=width,
115 118 height=height,
@@ -118,19 +121,21 @@ class PollinationsAI(AsyncGeneratorProvider, ProviderModelMixin):
118 121 private=private,
119 122 enhance=enhance,
120 123 safe=safe
121 ):
122 yield result
124 )
123 125 else:
124 126 # Text generation
125 127 async for result in cls._generate_text(
126 128 model=model,
127 129 messages=messages,
130 images=images,
128 131 proxy=proxy,
129 132 temperature=temperature,
130 133 presence_penalty=presence_penalty,
131 134 top_p=top_p,
132 135 frequency_penalty=frequency_penalty,
133 stream=stream
136 response_format=response_format,
137 seed=seed,
138 cache=cache,
134 139 ):
135 140 yield result
136 141
@@ -138,7 +143,6 @@ class PollinationsAI(AsyncGeneratorProvider, ProviderModelMixin):
138 143 async def _generate_image(
139 144 cls,
140 145 model: str,
141 messages: Messages,
142 146 prompt: str,
143 147 proxy: str,
144 148 width: int,
@@ -148,16 +152,7 @@ class PollinationsAI(AsyncGeneratorProvider, ProviderModelMixin):
148 152 private: bool,
149 153 enhance: bool,
150 154 safe: bool
151 ) -> AsyncResult:
152 if seed is None:
153 seed = random.randint(0, 10000)
154
155 headers = {
156 'Accept': '*/*',
157 'Accept-Language': 'en-US,en;q=0.9',
158 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36',
159 }
160
155 ) -> ImageResponse:
161 156 params = {
162 157 "seed": seed,
163 158 "width": width,
@@ -168,42 +163,47 @@ class PollinationsAI(AsyncGeneratorProvider, ProviderModelMixin):
168 163 "enhance": enhance,
169 164 "safe": safe
170 165 }
171 params = {k: v for k, v in params.items() if v is not None}
172
173 async with ClientSession(headers=headers) as session:
174 prompt = messages[-1]["content"] if prompt is None else prompt
175 param_string = "&".join(f"{k}={v}" for k, v in params.items())
176 url = f"{cls.image_api_endpoint}/prompt/{quote(prompt)}?{param_string}"
177
178 async with session.head(url, proxy=proxy) as response:
179 if response.status == 200:
180 image_response = ImageResponse(images=url, alt=prompt)
181 yield image_response
166 params = {k: json.dumps(v) if isinstance(v, bool) else v for k, v in params.items() if v is not None}
167 async with ClientSession(headers=DEFAULT_HEADERS, connector=get_connector(proxy=proxy)) as session:
168 async with session.head(f"{cls.image_api_endpoint}prompt/{quote_plus(prompt)}", params=params) as response:
169 await raise_for_status(response)
170 return ImageResponse(str(response.url), prompt)
182 171
183 172 @classmethod
184 173 async def _generate_text(
185 174 cls,
186 175 model: str,
187 176 messages: Messages,
177 images: Optional[ImagesType],
188 178 proxy: str,
189 179 temperature: float,
190 180 presence_penalty: float,
191 181 top_p: float,
192 182 frequency_penalty: float,
193 stream: bool,
194 seed: Optional[int] = None
195 ) -> AsyncResult:
196 headers = {
197 "accept": "*/*",
198 "accept-language": "en-US,en;q=0.9",
199 "content-type": "application/json",
200 "user-agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"
201 }
202
203 if seed is None:
204 seed = random.randint(0, 10000)
205
206 async with ClientSession(headers=headers) as session:
183 response_format: Optional[dict],
184 seed: Optional[int],
185 cache: bool
186 ) -> AsyncResult:
187 jsonMode = False
188 if response_format is not None and "type" in response_format:
189 if response_format["type"] == "json_object":
190 jsonMode = True
191
192 if images is not None and messages:
193 last_message = messages[-1].copy()
194 last_message["content"] = [
195 *[{
196 "type": "image_url",
197 "image_url": {"url": to_data_uri(image)}
198 } for image, _ in images],
199 {
200 "type": "text",
201 "text": messages[-1]["content"]
202 }
203 ]
204 messages[-1] = last_message
205
206 async with ClientSession(headers=DEFAULT_HEADERS, connector=get_connector(proxy=proxy)) as session:
207 207 data = {
208 208 "messages": messages,
209 209 "model": model,
@@ -211,42 +211,33 @@ class PollinationsAI(AsyncGeneratorProvider, ProviderModelMixin):
211 211 "presence_penalty": presence_penalty,
212 212 "top_p": top_p,
213 213 "frequency_penalty": frequency_penalty,
214 "jsonMode": False,
215 "stream": stream,
214 "jsonMode": jsonMode,
215 "stream": False, # To get more informations like Usage and FinishReason
216 216 "seed": seed,
217 "cache": False
217 "cache": cache
218 218 }
219
220 async with session.post(cls.text_api_endpoint, json=data, proxy=proxy) as response:
221 response.raise_for_status()
222 async for chunk in response.content:
223 if chunk:
224 decoded_chunk = chunk.decode()
225
226 # Skip [DONE].
227 if "data: [DONE]" in decoded_chunk:
228 continue
229
230 # Processing plain text
231 if not decoded_chunk.startswith("data:"):
232 clean_text = decoded_chunk.strip()
233 if clean_text:
234 yield clean_text
235 continue
236
237 # Processing JSON format
238 try:
239 # Remove the prefix “data: “ and parse JSON
240 json_str = decoded_chunk.replace("data:", "").strip()
241 json_response = json.loads(json_str)
242
243 if "choices" in json_response and json_response["choices"]:
244 if "delta" in json_response["choices"][0]:
245 content = json_response["choices"][0]["delta"].get("content")
246 if content:
247 # Remove escaped slashes before parentheses
248 clean_content = content.replace("\\(", "(").replace("\\)", ")")
249 yield clean_content
250 except json.JSONDecodeError:
251 # If JSON could not be parsed, skip
252 continue
219 async with session.post(cls.text_api_endpoint, json=filter_none(**data)) as response:
220 await raise_for_status(response)
221 async for line in response.content:
222 decoded_chunk = line.decode(errors="replace")
223 # If [DONE].
224 if "data: [DONE]" in decoded_chunk:
225 break
226 # Processing JSON format
227 try:
228 # Remove the prefix “data: “ and parse JSON
229 json_str = decoded_chunk.replace("data:", "").strip()
230 data = json.loads(json_str)
231 choice = data["choices"][0]
232 if "usage" in data:
233 yield Usage(**data["usage"])
234 if "message" in choice and "content" in choice["message"] and choice["message"]["content"]:
235 yield choice["message"]["content"].replace("\\(", "(").replace("\\)", ")")
236 elif "delta" in choice and "content" in choice["delta"] and choice["delta"]["content"]:
237 yield choice["delta"]["content"].replace("\\(", "(").replace("\\)", ")")
238 if "finish_reason" in choice and choice["finish_reason"] is not None:
239 yield FinishReason(choice["finish_reason"])
240 break
241 except json.JSONDecodeError:
242 yield decoded_chunk.strip()
243 continue
Modified g4f/Provider/hf_space/Qwen_QVQ_72B.py +1 -0
@@ -18,6 +18,7 @@ class Qwen_QVQ_72B(AsyncGeneratorProvider, ProviderModelMixin):
18 18
19 19 default_model = "qwen-qvq-72b-preview"
20 20 models = [default_model]
21 vision_models = models
21 22 model_aliases = {"qwq-32b": default_model}
22 23
23 24 @classmethod
Modified g4f/Provider/hf_space/__init__.py +6 -0
@@ -33,12 +33,18 @@ class HuggingSpace(AsyncGeneratorProvider, ProviderModelMixin):
33 33 def get_models(cls, **kwargs) -> list[str]:
34 34 if not cls.models:
35 35 models = []
36 image_models = []
37 vision_models = []
36 38 for provider in cls.providers:
37 39 models.extend(provider.get_models(**kwargs))
38 40 models.extend(provider.model_aliases.keys())
41 image_models.extend(provider.image_models)
42 vision_models.extend(provider.vision_models)
39 43 models = list(set(models))
40 44 models.sort()
41 45 cls.models = models
46 cls.image_models = list(set(image_models))
47 cls.vision_models = list(set(vision_models))
42 48 return cls.models
43 49
44 50 @classmethod
Modified g4f/Provider/needs_auth/HuggingChat.py +49 -16
@@ -1,6 +1,8 @@
1 1 from __future__ import annotations
2 2
3 3 import json
4 import re
5 import requests
4 6
5 7 try:
6 8 from curl_cffi.requests import Session, CurlMime
@@ -13,14 +15,13 @@ from ..helper import format_prompt
13 15 from ...typing import CreateResult, Messages, Cookies
14 16 from ...errors import MissingRequirementsError
15 17 from ...requests.raise_for_status import raise_for_status
16 from ...providers.response import JsonConversation, ImageResponse, Sources
18 from ...providers.response import JsonConversation, ImageResponse, Sources, TitleGeneration, Reasoning
17 19 from ...cookies import get_cookies
18 20 from ... import debug
19 21
20 22 class Conversation(JsonConversation):
21 def __init__(self, conversation_id: str, message_id: str):
22 self.conversation_id: str = conversation_id
23 self.message_id: str = message_id
23 def __init__(self, models: dict):
24 self.models: dict = models
24 25
25 26 class HuggingChat(AbstractProvider, ProviderModelMixin):
26 27 url = "https://huggingface.co/chat"
@@ -32,11 +33,11 @@ class HuggingChat(AbstractProvider, ProviderModelMixin):
32 33 default_model = "Qwen/Qwen2.5-72B-Instruct"
33 34 default_image_model = "black-forest-labs/FLUX.1-dev"
34 35 image_models = [
35 "black-forest-labs/FLUX.1-dev",
36 default_image_model,
36 37 "black-forest-labs/FLUX.1-schnell",
37 38 ]
38 models = [
39 'Qwen/Qwen2.5-Coder-32B-Instruct',
39 fallback_models = [
40 default_model,
40 41 'meta-llama/Llama-3.3-70B-Instruct',
41 42 'CohereForAI/c4ai-command-r-plus-08-2024',
42 43 'Qwen/QwQ-32B-Preview',
@@ -63,12 +64,33 @@ class HuggingChat(AbstractProvider, ProviderModelMixin):
63 64 "flux-schnell": "black-forest-labs/FLUX.1-schnell",
64 65 }
65 66
67 @classmethod
68 def get_models(cls):
69 if not cls.models:
70 try:
71 text = requests.get(cls.url).text
72 text = re.sub(r',parameters:{[^}]+?}', '', text)
73 text = re.search(r'models:(\[.+?\]),oldModels:', text).group(1)
74 text = text.replace('void 0', 'null')
75 def add_quotation_mark(match):
76 return f'{match.group(1)}"{match.group(2)}":'
77 text = re.sub(r'([{,])([A-Za-z0-9_]+?):', add_quotation_mark, text)
78 models = json.loads(text)
79 cls.text_models = [model["id"] for model in models]
80 cls.models = cls.text_models + cls.image_models
81 cls.vision_models = [model["id"] for model in models if model["multimodal"]]
82 except Exception as e:
83 debug.log(f"HuggingChat: Error reading models: {type(e).__name__}: {e}")
84 cls.models = [*cls.fallback_models]
85 return cls.models
86
66 87 @classmethod
67 88 def create_completion(
68 89 cls,
69 90 model: str,
70 91 messages: Messages,
71 92 stream: bool,
93 prompt: str = None,
72 94 return_conversation: bool = False,
73 95 conversation: Conversation = None,
74 96 web_search: bool = False,
@@ -99,22 +121,26 @@ class HuggingChat(AbstractProvider, ProviderModelMixin):
99 121 'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36',
100 122 }
101 123
102 if conversation is None:
124 if conversation is None or not hasattr(conversation, "models"):
125 conversation = Conversation({})
126
127 if model not in conversation.models:
103 128 conversationId = cls.create_conversation(session, model)
104 129 messageId = cls.fetch_message_id(session, conversationId)
105 conversation = Conversation(conversationId, messageId)
130 conversation.models[model] = {"conversationId": conversationId, "messageId": messageId}
106 131 if return_conversation:
107 132 yield conversation
108 133 inputs = format_prompt(messages)
109 134 else:
110 conversation.message_id = cls.fetch_message_id(session, conversation.conversation_id)
135 conversationId = conversation.models[model]["conversationId"]
136 conversation.models[model]["message_id"] = cls.fetch_message_id(session, conversationId)
111 137 inputs = messages[-1]["content"]
112 138
113 debug.log(f"Use conversation: {conversation.conversation_id} Use message: {conversation.message_id}")
139 debug.log(f"Use model {model}: {json.dumps(conversation.models[model])}")
114 140
115 141 settings = {
116 142 "inputs": inputs,
117 "id": conversation.message_id,
143 "id": conversation.models[model]["message_id"],
118 144 "is_retry": False,
119 145 "is_continue": False,
120 146 "web_search": web_search,
@@ -128,7 +154,7 @@ class HuggingChat(AbstractProvider, ProviderModelMixin):
128 154 'origin': 'https://huggingface.co',
129 155 'pragma': 'no-cache',
130 156 'priority': 'u=1, i',
131 'referer': f'https://huggingface.co/chat/conversation/{conversation.conversation_id}',
157 'referer': f'https://huggingface.co/chat/conversation/{conversationId}',
132 158 'sec-ch-ua': '"Not)A;Brand";v="99", "Google Chrome";v="127", "Chromium";v="127"',
133 159 'sec-ch-ua-mobile': '?0',
134 160 'sec-ch-ua-platform': '"macOS"',
@@ -142,7 +168,7 @@ class HuggingChat(AbstractProvider, ProviderModelMixin):
142 168 data.addpart('data', data=json.dumps(settings, separators=(',', ':')))
143 169
144 170 response = session.post(
145 f'https://huggingface.co/chat/conversation/{conversation.conversation_id}',
171 f'https://huggingface.co/chat/conversation/{conversationId}',
146 172 cookies=session.cookies,
147 173 headers=headers,
148 174 multipart=data,
@@ -170,10 +196,17 @@ class HuggingChat(AbstractProvider, ProviderModelMixin):
170 196 elif line["type"] == "finalAnswer":
171 197 break
172 198 elif line["type"] == "file":
173 url = f"https://huggingface.co/chat/conversation/{conversation.conversation_id}/output/{line['sha']}"
174 yield ImageResponse(url, alt=messages[-1]["content"], options={"cookies": cookies})
199 url = f"https://huggingface.co/chat/conversation/{conversationId}/output/{line['sha']}"
200 prompt = messages[-1]["content"] if prompt is None else prompt
201 yield ImageResponse(url, alt=prompt, options={"cookies": cookies})
175 202 elif line["type"] == "webSearch" and "sources" in line:
176 203 sources = Sources(line["sources"])
204 elif line["type"] == "title":
205 yield TitleGeneration(line["title"])
206 elif line["type"] == "reasoning":
207 yield Reasoning(line.get("token"), line.get("status"))
208 else:
209 pass #print(line)
177 210
178 211 full_response = full_response.replace('<|im_end|', '').strip()
179 212 if not stream:
Modified g4f/Provider/needs_auth/HuggingFace.py +1 -1
@@ -143,7 +143,7 @@ class HuggingFace(AsyncGeneratorProvider, ProviderModelMixin):
143 143 else:
144 144 is_special = True
145 145 debug.log(f"Special token: {is_special}")
146 yield FinishReason("stop" if is_special else "length", actions=["variant"] if is_special else ["continue", "variant"])
146 yield FinishReason("stop" if is_special else "length")
147 147 else:
148 148 if response.headers["content-type"].startswith("image/"):
149 149 base64_data = base64.b64encode(b"".join([chunk async for chunk in response.iter_content()]))
Modified g4f/Provider/needs_auth/HuggingFaceAPI.py +21 -3
@@ -2,6 +2,7 @@ from __future__ import annotations
2 2
3 3 from .OpenaiAPI import OpenaiAPI
4 4 from .HuggingChat import HuggingChat
5 from ...providers.types import Messages
5 6
6 7 class HuggingFaceAPI(OpenaiAPI):
7 8 label = "HuggingFace (Inference API)"
@@ -11,6 +12,23 @@ class HuggingFaceAPI(OpenaiAPI):
11 12 working = True
12 13 default_model = "meta-llama/Llama-3.2-11B-Vision-Instruct"
13 14 default_vision_model = default_model
14 models = [
15 *HuggingChat.models
16 ]
15
16 @classmethod
17 def get_models(cls, **kwargs):
18 HuggingChat.get_models()
19 cls.models = HuggingChat.text_models
20 cls.vision_models = HuggingChat.vision_models
21 return cls.models
22
23 @classmethod
24 async def create_async_generator(
25 cls,
26 model: str,
27 messages: Messages,
28 api_base: str = None,
29 **kwargs
30 ):
31 if api_base is None:
32 api_base = f"https://api-inference.huggingface.co/models/{model}/v1"
33 async for chunk in super().create_async_generator(model, messages, api_base=api_base, **kwargs):
34 yield chunk
Modified g4f/Provider/needs_auth/OpenaiAPI.py +9 -13
@@ -73,10 +73,11 @@ class OpenaiAPI(AsyncGeneratorProvider, ProviderModelMixin, RaiseErrorMixin):
73 73 raise MissingAuthError('Add a "api_key"')
74 74 if api_base is None:
75 75 api_base = cls.api_base
76 if images is not None:
76 if images is not None and messages:
77 77 if not model and hasattr(cls, "default_vision_model"):
78 78 model = cls.default_vision_model
79 messages[-1]["content"] = [
79 last_message = messages[-1].copy()
80 last_message["content"] = [
80 81 *[{
81 82 "type": "image_url",
82 83 "image_url": {"url": to_data_uri(image)}
@@ -86,6 +87,7 @@ class OpenaiAPI(AsyncGeneratorProvider, ProviderModelMixin, RaiseErrorMixin):
86 87 "text": messages[-1]["content"]
87 88 }
88 89 ]
90 messages[-1] = last_message
89 91 async with StreamSession(
90 92 proxy=proxy,
91 93 headers=cls.get_headers(stream, api_key, headers),
@@ -117,9 +119,9 @@ class OpenaiAPI(AsyncGeneratorProvider, ProviderModelMixin, RaiseErrorMixin):
117 119 yield ToolCalls(choice["message"]["tool_calls"])
118 120 if "usage" in data:
119 121 yield Usage(**data["usage"])
120 finish = cls.read_finish_reason(choice)
121 if finish is not None:
122 yield finish
122 if "finish_reason" in choice and choice["finish_reason"] is not None:
123 yield FinishReason(choice["finish_reason"])
124 return
123 125 else:
124 126 first = True
125 127 async for line in response.iter_lines():
@@ -137,16 +139,10 @@ class OpenaiAPI(AsyncGeneratorProvider, ProviderModelMixin, RaiseErrorMixin):
137 139 if delta:
138 140 first = False
139 141 yield delta
140 finish = cls.read_finish_reason(choice)
141 if finish is not None:
142 yield finish
142 if "finish_reason" in choice and choice["finish_reason"] is not None:
143 yield FinishReason(choice["finish_reason"])
143 144 break
144 145
145 @staticmethod
146 def read_finish_reason(choice: dict) -> Optional[FinishReason]:
147 if "finish_reason" in choice and choice["finish_reason"] is not None:
148 return FinishReason(choice["finish_reason"])
149
150 146 @classmethod
151 147 def get_headers(cls, stream: bool, api_key: str = None, headers: dict = None) -> dict:
152 148 return {
Modified g4f/Provider/needs_auth/OpenaiChat.py +1 -2
@@ -495,8 +495,7 @@ class OpenaiChat(AsyncAuthedProvider, ProviderModelMixin):
495 495 "headers": cls._headers,
496 496 "web_search": web_search,
497 497 })
498 actions = ["variant", "continue"] if conversation.finish_reason == "max_tokens" else ["variant"]
499 yield FinishReason(conversation.finish_reason, actions=actions)
498 yield FinishReason(conversation.finish_reason)
500 499
501 500 @classmethod
502 501 async def iter_messages_line(cls, session: StreamSession, line: bytes, fields: Conversation, sources: Sources) -> AsyncIterator:
Modified g4f/gui/client/static/css/style.css +23 -0
@@ -376,6 +376,29 @@ body:not(.white) a:visited{
376 376 display: flex;
377 377 }
378 378
379 .message .reasoning_text.final:not(.hidden), .message .reasoning_title {
380 margin-bottom: var(--inner-gap);
381 padding-bottom: var(--inner-gap);
382 border-bottom: 1px solid var(--colour-3);
383 overflow: hidden;
384 }
385
386 .message .reasoning_text.final {
387 max-height: 1000px;
388 transition: max-height 0.25s ease-in;
389 }
390
391 .message .reasoning_text.final.hidden {
392 transition: max-height 0.15s ease-out;
393 max-height: 0;
394 display: block;
395 overflow: hidden;
396 }
397
398 .message .reasoning_title {
399 cursor: pointer;
400 }
401
379 402 .message .user i {
380 403 position: absolute;
381 404 bottom: -6px;
Modified g4f/gui/client/static/js/chat.v1.js +70 -30
@@ -35,6 +35,7 @@ let title_storage = {};
35 35 let parameters_storage = {};
36 36 let finish_storage = {};
37 37 let usage_storage = {};
38 let reasoning_storage = {}
38 39
39 40 messageInput.addEventListener("blur", () => {
40 41 window.scrollTo(0, 0);
@@ -70,6 +71,17 @@ if (window.markdownit) {
70 71 }
71 72 }
72 73
74 function render_reasoning(reasoning, final = false) {
75 return `<div class="reasoning_body">
76 <div class="reasoning_title">
77 <strong>Reasoning <i class="fa-solid fa-brain"></i>:</strong> ${escapeHtml(reasoning.status)}
78 </div>
79 <div class="reasoning_text${final ? " final hidden" : ""}">
80 ${markdown_render(reasoning.text)}
81 </div>
82 </div>`;
83 }
84
73 85 function filter_message(text) {
74 86 return text.replaceAll(
75 87 /<!-- generated images start -->[\s\S]+<!-- generated images end -->/gm, ""
@@ -169,7 +181,7 @@ const get_message_el = (el) => {
169 181 }
170 182
171 183 const register_message_buttons = async () => {
172 document.querySelectorAll(".message .content .provider").forEach(async (el) => {
184 message_box.querySelectorAll(".message .content .provider").forEach(async (el) => {
173 185 if (!("click" in el.dataset)) {
174 186 el.dataset.click = "true";
175 187 const provider_forms = document.querySelector(".provider_forms");
@@ -192,7 +204,7 @@ const register_message_buttons = async () => {
192 204 }
193 205 });
194 206
195 document.querySelectorAll(".message .fa-xmark").forEach(async (el) => {
207 message_box.querySelectorAll(".message .fa-xmark").forEach(async (el) => {
196 208 if (!("click" in el.dataset)) {
197 209 el.dataset.click = "true";
198 210 el.addEventListener("click", async () => {
@@ -203,7 +215,7 @@ const register_message_buttons = async () => {
203 215 }
204 216 });
205 217
206 document.querySelectorAll(".message .fa-clipboard").forEach(async (el) => {
218 message_box.querySelectorAll(".message .fa-clipboard").forEach(async (el) => {
207 219 if (!("click" in el.dataset)) {
208 220 el.dataset.click = "true";
209 221 el.addEventListener("click", async () => {
@@ -226,7 +238,7 @@ const register_message_buttons = async () => {
226 238 }
227 239 });
228 240
229 document.querySelectorAll(".message .fa-file-export").forEach(async (el) => {
241 message_box.querySelectorAll(".message .fa-file-export").forEach(async (el) => {
230 242 if (!("click" in el.dataset)) {
231 243 el.dataset.click = "true";
232 244 el.addEventListener("click", async () => {
@@ -244,7 +256,7 @@ const register_message_buttons = async () => {
244 256 }
245 257 });
246 258
247 document.querySelectorAll(".message .fa-volume-high").forEach(async (el) => {
259 message_box.querySelectorAll(".message .fa-volume-high").forEach(async (el) => {
248 260 if (!("click" in el.dataset)) {
249 261 el.dataset.click = "true";
250 262 el.addEventListener("click", async () => {
@@ -270,7 +282,7 @@ const register_message_buttons = async () => {
270 282 }
271 283 });
272 284
273 document.querySelectorAll(".message .regenerate_button").forEach(async (el) => {
285 message_box.querySelectorAll(".message .regenerate_button").forEach(async (el) => {
274 286 if (!("click" in el.dataset)) {
275 287 el.dataset.click = "true";
276 288 el.addEventListener("click", async () => {
@@ -282,7 +294,7 @@ const register_message_buttons = async () => {
282 294 }
283 295 });
284 296
285 document.querySelectorAll(".message .continue_button").forEach(async (el) => {
297 message_box.querySelectorAll(".message .continue_button").forEach(async (el) => {
286 298 if (!("click" in el.dataset)) {
287 299 el.dataset.click = "true";
288 300 el.addEventListener("click", async () => {
@@ -297,7 +309,7 @@ const register_message_buttons = async () => {
297 309 }
298 310 });
299 311
300 document.querySelectorAll(".message .fa-whatsapp").forEach(async (el) => {
312 message_box.querySelectorAll(".message .fa-whatsapp").forEach(async (el) => {
301 313 if (!("click" in el.dataset)) {
302 314 el.dataset.click = "true";
303 315 el.addEventListener("click", async () => {
@@ -307,7 +319,7 @@ const register_message_buttons = async () => {
307 319 }
308 320 });
309 321
310 document.querySelectorAll(".message .fa-print").forEach(async (el) => {
322 message_box.querySelectorAll(".message .fa-print").forEach(async (el) => {
311 323 if (!("click" in el.dataset)) {
312 324 el.dataset.click = "true";
313 325 el.addEventListener("click", async () => {
@@ -323,6 +335,16 @@ const register_message_buttons = async () => {
323 335 })
324 336 }
325 337 });
338
339 message_box.querySelectorAll(".message .reasoning_title").forEach(async (el) => {
340 if (!("click" in el.dataset)) {
341 el.dataset.click = "true";
342 el.addEventListener("click", async () => {
343 let text_el = el.parentElement.querySelector(".reasoning_text");
344 text_el.classList[text_el.classList.contains("hidden") ? "remove" : "add"]("hidden");
345 })
346 }
347 });
326 348 }
327 349
328 350 const delete_conversations = async () => {
@@ -469,7 +491,7 @@ const prepare_messages = (messages, message_index = -1, do_continue = false, do_
469 491 messages.forEach((message) => {
470 492 message_copy = { ...message };
471 493 if (last_message) {
472 if (last_message["role"] == message["role"]) {
494 if (last_message["role"] == message["role"] && message["role"] == "assistant") {
473 495 message_copy["content"] = last_message["content"] + message_copy["content"];
474 496 new_messages.pop();
475 497 }
@@ -515,6 +537,7 @@ const prepare_messages = (messages, message_index = -1, do_continue = false, do_
515 537 delete new_message.synthesize;
516 538 delete new_message.finish;
517 539 delete new_message.usage;
540 delete new_message.reasoning;
518 541 delete new_message.conversation;
519 542 delete new_message.continue;
520 543 // Append message to new messages
@@ -711,11 +734,21 @@ async function add_message_chunk(message, message_id, provider, scroll) {
711 734 } else if (message.type == "title") {
712 735 title_storage[message_id] = message.title;
713 736 } else if (message.type == "login") {
714 update_message(content_map, message_id, message.login, scroll);
737 update_message(content_map, message_id, markdown_render(message.login), scroll);
715 738 } else if (message.type == "finish") {
716 739 finish_storage[message_id] = message.finish;
717 740 } else if (message.type == "usage") {
718 741 usage_storage[message_id] = message.usage;
742 } else if (message.type == "reasoning") {
743 if (!reasoning_storage[message_id]) {
744 reasoning_storage[message_id] = message;
745 reasoning_storage[message_id].text = "";
746 } else if (message.status) {
747 reasoning_storage[message_id].status = message.status;
748 } else if (message.token) {
749 reasoning_storage[message_id].text += message.token;
750 }
751 update_message(content_map, message_id, render_reasoning(reasoning_storage[message_id]), scroll);
719 752 } else if (message.type == "parameters") {
720 753 if (!parameters_storage[provider]) {
721 754 parameters_storage[provider] = {};
@@ -846,6 +879,7 @@ const ask_gpt = async (message_id, message_index = -1, regenerate = false, provi
846 879 title_storage[message_id],
847 880 finish_storage[message_id],
848 881 usage_storage[message_id],
882 reasoning_storage[message_id],
849 883 action=="continue"
850 884 );
851 885 delete controller_storage[message_id];
@@ -1042,6 +1076,7 @@ function merge_messages(message1, message2) {
1042 1076 const load_conversation = async (conversation_id, scroll=true) => {
1043 1077 let conversation = await get_conversation(conversation_id);
1044 1078 let messages = conversation?.items || [];
1079 console.debug("Conversation:", conversation)
1045 1080
1046 1081 if (!conversation) {
1047 1082 return;
@@ -1098,11 +1133,8 @@ const load_conversation = async (conversation_id, scroll=true) => {
1098 1133 let add_buttons = [];
1099 1134 // Find buttons to add
1100 1135 actions = ["variant"]
1101 if (item.finish && item.finish.actions) {
1102 actions = item.finish.actions
1103 }
1104 1136 // Add continue button if possible
1105 if (item.role == "assistant" && !actions.includes("continue")) {
1137 if (item.role == "assistant") {
1106 1138 let reason = "stop";
1107 1139 // Read finish reason from conversation
1108 1140 if (item.finish && item.finish.reason) {
@@ -1167,7 +1199,10 @@ const load_conversation = async (conversation_id, scroll=true) => {
1167 1199 </div>
1168 1200 <div class="content">
1169 1201 ${provider}
1170 <div class="content_inner">${markdown_render(buffer)}</div>
1202 <div class="content_inner">
1203 ${item.reasoning ? render_reasoning(item.reasoning, true): ""}
1204 ${markdown_render(buffer)}
1205 </div>
1171 1206 <div class="count">
1172 1207 ${count_words_and_tokens(buffer, next_provider?.model, completion_tokens, prompt_tokens)}
1173 1208 ${add_buttons.join("")}
@@ -1298,6 +1333,7 @@ const add_message = async (
1298 1333 title = null,
1299 1334 finish = null,
1300 1335 usage = null,
1336 reasoning = null,
1301 1337 do_continue = false
1302 1338 ) => {
1303 1339 const conversation = await get_conversation(conversation_id);
@@ -1329,6 +1365,9 @@ const add_message = async (
1329 1365 if (usage) {
1330 1366 new_message.usage = usage;
1331 1367 }
1368 if (reasoning) {
1369 new_message.reasoning = reasoning;
1370 }
1332 1371 if (do_continue) {
1333 1372 new_message.continue = true;
1334 1373 }
@@ -1604,23 +1643,24 @@ function count_words_and_tokens(text, model, completion_tokens, prompt_tokens) {
1604 1643
1605 1644 function update_message(content_map, message_id, content = null, scroll = true) {
1606 1645 content_map.update_timeouts.push(setTimeout(() => {
1607 if (!content) content = message_storage[message_id];
1608 html = markdown_render(content);
1609 let lastElement, lastIndex = null;
1610 for (element of ['</p>', '</code></pre>', '</p>\n</li>\n</ol>', '</li>\n</ol>', '</li>\n</ul>']) {
1611 const index = html.lastIndexOf(element)
1612 if (index - element.length > lastIndex) {
1613 lastElement = element;
1614 lastIndex = index;
1646 if (!content) {
1647 content = markdown_render(message_storage[message_id]);
1648 let lastElement, lastIndex = null;
1649 for (element of ['</p>', '</code></pre>', '</p>\n</li>\n</ol>', '</li>\n</ol>', '</li>\n</ul>']) {
1650 const index = content.lastIndexOf(element)
1651 if (index - element.length > lastIndex) {
1652 lastElement = element;
1653 lastIndex = index;
1654 }
1655 }
1656 if (lastIndex) {
1657 content = content.substring(0, lastIndex) + '<span class="cursor"></span>' + lastElement;
1615 1658 }
1616 1659 }
1617 if (lastIndex) {
1618 html = html.substring(0, lastIndex) + '<span class="cursor"></span>' + lastElement;
1619 }
1660 content_map.inner.innerHTML = content;
1620 1661 if (error_storage[message_id]) {
1621 1662 content_map.inner.innerHTML += markdown_render(`**An error occured:** ${error_storage[message_id]}`);
1622 1663 }
1623 content_map.inner.innerHTML = html;
1624 1664 content_map.count.innerText = count_words_and_tokens(message_storage[message_id], provider_storage[message_id]?.model);
1625 1665 highlight(content_map.inner);
1626 1666 if (scroll) {
@@ -2132,9 +2172,9 @@ async function read_response(response, message_id, provider, scroll) {
2132 2172 function get_api_key_by_provider(provider) {
2133 2173 let api_key = null;
2134 2174 if (provider) {
2135 api_key = document.getElementById(`${provider}-api_key`)?.id || null;
2175 api_key = document.querySelector(`.${provider}-api_key`)?.id || null;
2136 2176 if (api_key == null) {
2137 api_key = document.querySelector(`.${provider}-api_key`)?.id || null;
2177 api_key = document.getElementById(`${provider}-api_key`)?.id || null;
2138 2178 }
2139 2179 if (api_key) {
2140 2180 api_key = appStorage.getItem(api_key);
Modified g4f/gui/server/api.py +10 -3
@@ -13,7 +13,7 @@ from ...tools.run_tools import iter_run_tools
13 13 from ...Provider import ProviderUtils, __providers__
14 14 from ...providers.base_provider import ProviderModelMixin
15 15 from ...providers.retry_provider import IterListProvider
16 from ...providers.response import BaseConversation, JsonConversation, FinishReason, Usage
16 from ...providers.response import BaseConversation, JsonConversation, FinishReason, Usage, Reasoning
17 17 from ...providers.response import SynthesizeData, TitleGeneration, RequestLogin, Parameters
18 18 from ... import version, models
19 19 from ... import ChatCompletion, get_model_and_provider
@@ -207,6 +207,8 @@ class Api:
207 207 yield self._format_json("finish", chunk.get_dict())
208 208 elif isinstance(chunk, Usage):
209 209 yield self._format_json("usage", chunk.get_dict())
210 elif isinstance(chunk, Reasoning):
211 yield self._format_json("reasoning", token=chunk.token, status=chunk.status)
210 212 else:
211 213 yield self._format_json("content", str(chunk))
212 214 if debug.logs:
@@ -219,10 +221,15 @@ class Api:
219 221 if first:
220 222 yield self.handle_provider(provider_handler, model)
221 223
222 def _format_json(self, response_type: str, content):
224 def _format_json(self, response_type: str, content = None, **kwargs):
225 if content is not None:
226 return {
227 'type': response_type,
228 response_type: content,
229 }
223 230 return {
224 231 'type': response_type,
225 response_type: content
232 **kwargs
226 233 }
227 234
228 235 def handle_provider(self, provider_handler, model):
Modified g4f/gui/server/backend_api.py +2 -2
@@ -309,7 +309,7 @@ class Backend_Api(Api):
309 309 return "Provider not found", 404
310 310 return models
311 311
312 def _format_json(self, response_type: str, content) -> str:
312 def _format_json(self, response_type: str, content = None, **kwargs) -> str:
313 313 """
314 314 Formats and returns a JSON response.
315 315
@@ -320,4 +320,4 @@ class Backend_Api(Api):
320 320 Returns:
321 321 str: A JSON formatted string.
322 322 """
323 return json.dumps(super()._format_json(response_type, content)) + "\n"
323 return json.dumps(super()._format_json(response_type, content, **kwargs)) + "\n"
Modified g4f/providers/base_provider.py +2 -1
Modified g4f/providers/response.py +9 -2
Modified g4f/requests/raise_for_status.py +2 -2