返回提交历史
Modified
g4f/Provider/PollinationsAI.py
+97
-106
Modified
g4f/Provider/hf_space/Qwen_QVQ_72B.py
+1
-0
Modified
g4f/Provider/hf_space/__init__.py
+6
-0
Modified
g4f/Provider/needs_auth/HuggingChat.py
+49
-16
Modified
g4f/Provider/needs_auth/HuggingFace.py
+1
-1
Modified
g4f/Provider/needs_auth/HuggingFaceAPI.py
+21
-3
Modified
g4f/Provider/needs_auth/OpenaiAPI.py
+9
-13
Modified
g4f/Provider/needs_auth/OpenaiChat.py
+1
-2
Modified
g4f/gui/client/static/css/style.css
+23
-0
Modified
g4f/gui/client/static/js/chat.v1.js
+70
-30
Modified
g4f/gui/server/api.py
+10
-3
Modified
g4f/gui/server/backend_api.py
+2
-2
Modified
g4f/providers/base_provider.py
+2
-1
Modified
g4f/providers/response.py
+9
-2
Modified
g4f/requests/raise_for_status.py
+2
-2
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
代码差异
15 个文件
+303
-181
@@ -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
@@ -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
@@ -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
@@ -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:
@@ -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()]))
@@ -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
@@ -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 {
@@ -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:
@@ -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;
@@ -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);
@@ -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):
@@ -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"