From 167ceedd3175c468d8788a586db956a515ba9829 Mon Sep 17 00:00:00 2001
From: hlohaus <983577+hlohaus@users.noreply.github.com>
Date: Sun, 2 Feb 2025 23:03:59 +0100
Subject: Update model list in OpenaiChat (o3-mini, o3-mini-high) Add Reasoning
to OpenaiChat provider Check for pipeline_tag in HuggingChat providers Add
image preview in PollinationsAI Add input of custom Model in GUI
---
g4f/Provider/Blackbox.py | 9 +-
g4f/Provider/PollinationsAI.py | 22 +++--
g4f/Provider/hf/HuggingFaceAPI.py | 25 +++++-
g4f/Provider/hf/HuggingFaceInference.py | 57 +++++++------
g4f/Provider/needs_auth/OpenaiChat.py | 29 ++++---
g4f/Provider/openai/models.py | 6 ++
g4f/api/__init__.py | 29 ++++---
g4f/gui/client/index.html | 22 +++--
g4f/gui/client/static/css/style.css | 7 +-
g4f/gui/client/static/img/site.webmanifest | 2 +-
g4f/gui/client/static/js/chat.v1.js | 129 +++++++++++++++++++++++------
g4f/gui/server/backend_api.py | 2 +-
g4f/tools/run_tools.py | 28 +++++--
13 files changed, 257 insertions(+), 110 deletions(-)
create mode 100644 g4f/Provider/openai/models.py
diff --git a/g4f/Provider/Blackbox.py b/g4f/Provider/Blackbox.py
index 09d8c196..af773a66 100644
--- a/g4f/Provider/Blackbox.py
+++ b/g4f/Provider/Blackbox.py
@@ -308,14 +308,7 @@ class Blackbox(AsyncGeneratorProvider, ProviderModelMixin):
image_url = image_url_match.group(1)
yield ImageResponse(image_url, format_image_prompt(messages, prompt))
else:
- if "" in text_to_yield and "" in text_to_yield:
- parts = text_to_yield.split('', 1)
- yield parts[0]
- reasoning_parts = parts[1].split('', 1)
- yield Reasoning(f"{reasoning_parts[0]}")
- yield reasoning_parts[1]
- full_response = text_to_yield
- elif "Generated by BLACKBOX.AI" in text_to_yield:
+ if "Generated by BLACKBOX.AI" in text_to_yield:
conversation.validated_value = await cls.fetch_validated(force_refresh=True)
if conversation.validated_value:
data["validated"] = conversation.validated_value
diff --git a/g4f/Provider/PollinationsAI.py b/g4f/Provider/PollinationsAI.py
index 12268288..13bc2581 100644
--- a/g4f/Provider/PollinationsAI.py
+++ b/g4f/Provider/PollinationsAI.py
@@ -13,7 +13,7 @@ from ..typing import AsyncResult, Messages, ImagesType
from ..image import to_data_uri
from ..requests.raise_for_status import raise_for_status
from ..requests.aiohttp import get_connector
-from ..providers.response import ImageResponse, FinishReason, Usage
+from ..providers.response import ImageResponse, ImagePreview, FinishReason, Usage
DEFAULT_HEADERS = {
'Accept': '*/*',
@@ -125,7 +125,7 @@ class PollinationsAI(AsyncGeneratorProvider, ProviderModelMixin):
# Check if models
# Image generation
if model in cls.image_models:
- yield await cls._generate_image(
+ async for chunk in cls._generate_image(
model=model,
prompt=format_image_prompt(messages, prompt),
proxy=proxy,
@@ -136,7 +136,8 @@ class PollinationsAI(AsyncGeneratorProvider, ProviderModelMixin):
private=private,
enhance=enhance,
safe=safe
- )
+ ):
+ yield chunk
else:
# Text generation
async for result in cls._generate_text(
@@ -167,7 +168,7 @@ class PollinationsAI(AsyncGeneratorProvider, ProviderModelMixin):
private: bool,
enhance: bool,
safe: bool
- ) -> ImageResponse:
+ ) -> AsyncResult:
params = {
"seed": seed,
"width": width,
@@ -178,11 +179,16 @@ class PollinationsAI(AsyncGeneratorProvider, ProviderModelMixin):
"enhance": enhance,
"safe": safe
}
- params = {k: json.dumps(v) if isinstance(v, bool) else v for k, v in params.items() if v is not None}
+ params = {k: json.dumps(v) if isinstance(v, bool) else str(v) for k, v in params.items() if v is not None}
+ params = "&".join( "%s=%s" % (key, quote_plus(params[key]))
+ for key in params.keys())
+ url = f"{cls.image_api_endpoint}prompt/{quote_plus(prompt)}?{params}"
+ yield ImagePreview(url, prompt)
async with ClientSession(headers=DEFAULT_HEADERS, connector=get_connector(proxy=proxy)) as session:
- async with session.head(f"{cls.image_api_endpoint}prompt/{quote_plus(prompt)}", params=params) as response:
- await raise_for_status(response)
- return ImageResponse(str(response.url), prompt)
+ async with session.head(url) as response:
+ if response.status != 500: # Server is busy
+ await raise_for_status(response)
+ yield ImageResponse(str(response.url), prompt)
@classmethod
async def _generate_text(
diff --git a/g4f/Provider/hf/HuggingFaceAPI.py b/g4f/Provider/hf/HuggingFaceAPI.py
index fdbb1f7c..2136caf0 100644
--- a/g4f/Provider/hf/HuggingFaceAPI.py
+++ b/g4f/Provider/hf/HuggingFaceAPI.py
@@ -1,8 +1,11 @@
from __future__ import annotations
+from ...providers.types import Messages
+from ...typing import ImagesType
+from ...requests import StreamSession, raise_for_status
+from ...errors import ModelNotSupportedError
from ..template.OpenaiTemplate import OpenaiTemplate
from .models import model_aliases
-from ...providers.types import Messages
from .HuggingChat import HuggingChat
from ... import debug
@@ -37,6 +40,10 @@ class HuggingFaceAPI(OpenaiTemplate):
api_base: str = None,
max_tokens: int = 2048,
max_inputs_lenght: int = 10000,
+ impersonate: str = None,
+ proxy: str = None,
+ timeout: int = 300,
+ images: ImagesType = None,
**kwargs
):
if api_base is None:
@@ -44,6 +51,20 @@ class HuggingFaceAPI(OpenaiTemplate):
if model in cls.model_aliases:
model_name = cls.model_aliases[model]
api_base = f"https://api-inference.huggingface.co/models/{model_name}/v1"
+ if images is not None:
+ async with StreamSession(
+ proxy=proxy,
+ timeout=timeout,
+ impersonate=impersonate,
+ ) as session:
+ async with session.get(f"https://huggingface.co/api/models/{model}") as response:
+ if response.status == 404:
+ raise ModelNotSupportedError(f"Model is not supported: {model} in: {cls.__name__}")
+ await raise_for_status(response)
+ model_data = await response.json()
+ pipeline_tag = model_data.get("pipeline_tag")
+ if pipeline_tag != "image-text-to-text":
+ raise ModelNotSupportedError(f"Model is not supported: {model} in: {cls.__name__} pipeline_tag={pipeline_tag}")
start = calculate_lenght(messages)
if start > max_inputs_lenght:
if len(messages) > 6:
@@ -54,7 +75,7 @@ class HuggingFaceAPI(OpenaiTemplate):
if len(messages) > 1 and calculate_lenght(messages) > max_inputs_lenght:
messages = [messages[-1]]
debug.log(f"Messages trimmed from: {start} to: {calculate_lenght(messages)}")
- async for chunk in super().create_async_generator(model, messages, api_base=api_base, max_tokens=max_tokens, **kwargs):
+ async for chunk in super().create_async_generator(model, messages, api_base=api_base, max_tokens=max_tokens, images=images, **kwargs):
yield chunk
def calculate_lenght(messages: Messages) -> int:
diff --git a/g4f/Provider/hf/HuggingFaceInference.py b/g4f/Provider/hf/HuggingFaceInference.py
index df866ba3..68e6adff 100644
--- a/g4f/Provider/hf/HuggingFaceInference.py
+++ b/g4f/Provider/hf/HuggingFaceInference.py
@@ -78,18 +78,13 @@ class HuggingFaceInference(AsyncGeneratorProvider, ProviderModelMixin):
if api_key is not None:
headers["Authorization"] = f"Bearer {api_key}"
payload = None
- if cls.get_models() and model in cls.image_models:
- stream = False
- prompt = format_image_prompt(messages, prompt)
- payload = {"inputs": prompt, "parameters": {"seed": random.randint(0, 2**32), **extra_data}}
- else:
- params = {
- "return_full_text": False,
- "max_new_tokens": max_tokens,
- "temperature": temperature,
- **extra_data
- }
- do_continue = action == "continue"
+ params = {
+ "return_full_text": False,
+ "max_new_tokens": max_tokens,
+ "temperature": temperature,
+ **extra_data
+ }
+ do_continue = action == "continue"
async with StreamSession(
headers=headers,
proxy=proxy,
@@ -101,22 +96,30 @@ class HuggingFaceInference(AsyncGeneratorProvider, ProviderModelMixin):
raise ModelNotSupportedError(f"Model is not supported: {model} in: {cls.__name__}")
await raise_for_status(response)
model_data = await response.json()
- model_type = None
- if "config" in model_data and "model_type" in model_data["config"]:
- model_type = model_data["config"]["model_type"]
- debug.log(f"Model type: {model_type}")
- inputs = get_inputs(messages, model_data, model_type, do_continue)
- debug.log(f"Inputs len: {len(inputs)}")
- if len(inputs) > 4096:
- if len(messages) > 6:
- messages = messages[:3] + messages[-3:]
- else:
- messages = [m for m in messages if m["role"] == "system"] + [messages[-1]]
+ pipeline_tag = model_data.get("pipeline_tag")
+ if pipeline_tag == "text-to-image":
+ stream = False
+ inputs = format_image_prompt(messages, prompt)
+ payload = {"inputs": inputs, "parameters": {"seed": random.randint(0, 2**32), **extra_data}}
+ elif pipeline_tag in ("text-generation", "image-text-to-text"):
+ model_type = None
+ if "config" in model_data and "model_type" in model_data["config"]:
+ model_type = model_data["config"]["model_type"]
+ debug.log(f"Model type: {model_type}")
inputs = get_inputs(messages, model_data, model_type, do_continue)
- debug.log(f"New len: {len(inputs)}")
- if model_type == "gpt2" and max_tokens >= 1024:
- params["max_new_tokens"] = 512
- payload = {"inputs": inputs, "parameters": params, "stream": stream}
+ debug.log(f"Inputs len: {len(inputs)}")
+ if len(inputs) > 4096:
+ if len(messages) > 6:
+ messages = messages[:3] + messages[-3:]
+ else:
+ messages = [m for m in messages if m["role"] == "system"] + [messages[-1]]
+ inputs = get_inputs(messages, model_data, model_type, do_continue)
+ debug.log(f"New len: {len(inputs)}")
+ if model_type == "gpt2" and max_tokens >= 1024:
+ params["max_new_tokens"] = 512
+ payload = {"inputs": inputs, "parameters": params, "stream": stream}
+ else:
+ raise ModelNotSupportedError(f"Model is not supported: {model} in: {cls.__name__} pipeline_tag: {pipeline_tag}")
async with session.post(f"{api_base.rstrip('/')}/models/{model}", json=payload) as response:
if response.status == 404:
diff --git a/g4f/Provider/needs_auth/OpenaiChat.py b/g4f/Provider/needs_auth/OpenaiChat.py
index 8fde1090..81961bdf 100644
--- a/g4f/Provider/needs_auth/OpenaiChat.py
+++ b/g4f/Provider/needs_auth/OpenaiChat.py
@@ -25,8 +25,9 @@ from ...requests import get_nodriver
from ...image import ImageResponse, ImageRequest, to_image, to_bytes, is_accepted_format
from ...errors import MissingAuthError, NoValidHarFileError
from ...providers.response import JsonConversation, FinishReason, SynthesizeData, AuthResult
-from ...providers.response import Sources, TitleGeneration, RequestLogin, Parameters
+from ...providers.response import Sources, TitleGeneration, RequestLogin, Parameters, Reasoning
from ..helper import format_cookies
+from ..openai.models import default_model, default_image_model, models, image_models, text_models
from ..openai.har_file import get_request_config
from ..openai.har_file import RequestConfig, arkReq, arkose_url, start_url, conversation_url, backend_url, backend_anon_url
from ..openai.proofofwork import generate_proof_token
@@ -95,12 +96,11 @@ class OpenaiChat(AsyncAuthedProvider, ProviderModelMixin):
supports_gpt_4 = True
supports_message_history = True
supports_system_message = True
- default_model = "auto"
- default_image_model = "dall-e-3"
- image_models = [default_image_model]
- text_models = [default_model, "gpt-4", "gpt-4o", "gpt-4o-mini", "o1", "o1-preview", "o1-mini"]
+ default_model = default_model
+ default_image_model = default_image_model
+ image_models = image_models
vision_models = text_models
- models = text_models + image_models
+ models = models
synthesize_content_type = "audio/mpeg"
_api_key: str = None
@@ -368,9 +368,11 @@ class OpenaiChat(AsyncAuthedProvider, ProviderModelMixin):
)
[debug.log(text) for text in (
#f"Arkose: {'False' if not need_arkose else auth_result.arkose_token[:12]+'...'}",
- f"Proofofwork: {'False' if proofofwork is None else proofofwork[:12]+'...'}",
- f"AccessToken: {'False' if cls._api_key is None else cls._api_key[:12]+'...'}",
+ #f"Proofofwork: {'False' if proofofwork is None else proofofwork[:12]+'...'}",
+ #f"AccessToken: {'False' if cls._api_key is None else cls._api_key[:12]+'...'}",
)]
+ if action == "continue" and conversation.message_id is None:
+ action = "next"
data = {
"action": action,
"parent_message_id": conversation.message_id,
@@ -497,7 +499,7 @@ class OpenaiChat(AsyncAuthedProvider, ProviderModelMixin):
v = line.get("v")
if isinstance(v, str) and fields.is_recipient:
if "p" not in line or line.get("p") == "/message/content/parts/0":
- yield v
+ yield Reasoning(token=v) if fields.is_thinking else v
elif isinstance(v, list):
for m in v:
if m.get("p") == "/message/content/parts/0" and fields.is_recipient:
@@ -508,6 +510,9 @@ class OpenaiChat(AsyncAuthedProvider, ProviderModelMixin):
sources.add_source(link)
elif re.match(r"^/message/metadata/content_references/\d+$", m.get("p")):
sources.add_source(m.get("v"))
+ elif m.get("p") == "/message/metadata/finished_text":
+ fields.is_thinking = False
+ yield Reasoning(status=m.get("v"))
elif m.get("p") == "/message/metadata":
fields.finish_reason = m.get("v", {}).get("finish_details", {}).get("type")
break
@@ -519,6 +524,9 @@ class OpenaiChat(AsyncAuthedProvider, ProviderModelMixin):
fields.is_recipient = m.get("recipient", "all") == "all"
if fields.is_recipient:
c = m.get("content", {})
+ if c.get("content_type") == "text" and m.get("author", {}).get("role") == "tool" and "initial_text" in m.get("metadata", {}):
+ fields.is_thinking = True
+ yield Reasoning(status=c.get("metadata", {}).get("initial_text"))
if c.get("content_type") == "multimodal_text":
generated_images = []
for element in c.get("parts"):
@@ -697,13 +705,14 @@ class Conversation(JsonConversation):
"""
Class to encapsulate response fields.
"""
- def __init__(self, conversation_id: str = None, message_id: str = None, user_id: str = None, finish_reason: str = None, parent_message_id: str = None):
+ def __init__(self, conversation_id: str = None, message_id: str = None, user_id: str = None, finish_reason: str = None, parent_message_id: str = None, is_thinking: bool = False):
self.conversation_id = conversation_id
self.message_id = message_id
self.finish_reason = finish_reason
self.is_recipient = False
self.parent_message_id = message_id if parent_message_id is None else parent_message_id
self.user_id = user_id
+ self.is_thinking = is_thinking
def get_cookies(
urls: Optional[Iterator[str]] = None
diff --git a/g4f/Provider/openai/models.py b/g4f/Provider/openai/models.py
new file mode 100644
index 00000000..5207280a
--- /dev/null
+++ b/g4f/Provider/openai/models.py
@@ -0,0 +1,6 @@
+default_model = "auto"
+default_image_model = "dall-e-3"
+image_models = [default_image_model]
+text_models = [default_model, "gpt-4", "gpt-4o", "gpt-4o-mini", "o1", "o1-preview", "o1-mini", "o3-mini", "o3-mini-high"]
+vision_models = text_models
+models = text_models + image_models
\ No newline at end of file
diff --git a/g4f/api/__init__.py b/g4f/api/__init__.py
index 457575c0..29cc5e0a 100644
--- a/g4f/api/__init__.py
+++ b/g4f/api/__init__.py
@@ -10,6 +10,7 @@ from email.utils import formatdate
import os.path
import hashlib
import asyncio
+from urllib.parse import quote_plus
from fastapi import FastAPI, Response, Request, UploadFile, Depends
from fastapi.middleware.wsgi import WSGIMiddleware
from fastapi.responses import StreamingResponse, RedirectResponse, HTMLResponse, JSONResponse
@@ -176,11 +177,11 @@ class Api:
return ErrorResponse.from_message("G4F API key required", HTTP_401_UNAUTHORIZED)
if not secrets.compare_digest(AppConfig.g4f_api_key, user_g4f_api_key):
return ErrorResponse.from_message("Invalid G4F API key", HTTP_403_FORBIDDEN)
- elif not AppConfig.demo:
- if user_g4f_api_key is not None and path.startswith("/images/"):
+ elif not AppConfig.demo and not path.startswith("/images/"):
+ if user_g4f_api_key is not None:
if not secrets.compare_digest(AppConfig.g4f_api_key, user_g4f_api_key):
return ErrorResponse.from_message("Invalid G4F API key", HTTP_403_FORBIDDEN)
- elif path.startswith("/backend-api/") or path.startswith("/images/") or path.startswith("/chat/") and path != "/chat/":
+ elif path.startswith("/backend-api/") or path.startswith("/chat/") and path != "/chat/":
try:
username = await self.get_username(request)
except HTTPException as e:
@@ -551,8 +552,8 @@ class Api:
HTTP_404_NOT_FOUND: {}
})
async def get_image(filename, request: Request):
- target = os.path.join(images_dir, filename)
- ext = os.path.splitext(filename)[1]
+ target = os.path.join(images_dir, quote_plus(filename))
+ ext = os.path.splitext(filename)[1][1:]
stat_result = SimpleNamespace()
stat_result.st_size = 0
if os.path.isfile(target):
@@ -560,10 +561,12 @@ class Api:
stat_result.st_mtime = int(f"{filename.split('_')[0]}") if filename.startswith("1") else 0
headers = {
"cache-control": "public, max-age=31536000",
- "content-type": f"image/{ext.replace('jpg', 'jpeg')[1:] or 'jpeg'}",
- "content-length": str(stat_result.st_size),
+ "content-type": f"image/{ext.replace('jpg', 'jpeg') or 'jpeg'}",
"last-modified": formatdate(stat_result.st_mtime, usegmt=True),
"etag": f'"{hashlib.md5(filename.encode()).hexdigest()}"',
+ **({
+ "content-length": str(stat_result.st_size),
+ } if stat_result.st_size else {})
}
response = FileResponse(
target,
@@ -583,10 +586,14 @@ class Api:
source_url = source_url[1]
source_url = source_url.replace("%2F", "/").replace("%3A", ":").replace("%3F", "?").replace("%3D", "=")
if source_url.startswith("https://"):
- await copy_images(
- [source_url],
- target=target)
- debug.log(f"Image copied from {source_url}")
+ try:
+ await copy_images(
+ [source_url],
+ target=target)
+ debug.log(f"Image copied from {source_url}")
+ except Exception as e:
+ debug.log(f"{type(e).__name__}: Download failed: {source_url}\n{e}")
+ return RedirectResponse(url=source_url)
if not os.path.isfile(target):
return ErrorResponse.from_message("File not found", HTTP_404_NOT_FOUND)
async def stream():
diff --git a/g4f/gui/client/index.html b/g4f/gui/client/index.html
index 370b4867..c7e445ce 100644
--- a/g4f/gui/client/index.html
+++ b/g4f/gui/client/index.html
@@ -47,12 +47,13 @@
gallery: '#messages',
children: 'a:has(img)',
secondaryZoomLevel: 2,
+ allowPanToNext: true,
pswpModule: () => import('https://cdn.jsdelivr.net/npm/photoswipe'),
});
lightbox.addFilter('itemData', (itemData, index) => {
const img = itemData.element.querySelector('img');
- itemData.width = img.naturalWidth;
- itemData.height = img.naturalHeight;
+ itemData.width = img.naturalWidth || 1024;
+ itemData.height = img.naturalHeight || 1024;
return itemData;
});
lightbox.on('uiRegister', function() {
@@ -66,7 +67,13 @@
lightbox.pswp.on('change', () => {
const currSlideElement = lightbox.pswp.currSlide.data.element;
if (currSlideElement) {
- el.innerText = currSlideElement.querySelector('img').getAttribute('alt');
+ const img = currSlideElement.querySelector('img');
+ el.innerText = img.getAttribute('alt');
+ const download = document.createElement("a");
+ download.setAttribute("href", img.getAttribute('src'));
+ download.setAttribute("download", `${img.getAttribute('alt')}${lightbox.pswp.currSlide.index}.jpg`);
+ download.innerHTML = '';
+ el.appendChild(download);
}
});
}
@@ -157,8 +164,8 @@
-
-
+
+
@@ -269,6 +276,7 @@
@@ -293,7 +301,8 @@
-
+
+
diff --git a/g4f/gui/client/static/css/style.css b/g4f/gui/client/static/css/style.css
index 29ba7501..2e8702bc 100644
--- a/g4f/gui/client/static/css/style.css
+++ b/g4f/gui/client/static/css/style.css
@@ -799,7 +799,7 @@ form input:checked+label:after {
color: var(--colour-3);
}
-select {
+select, input.model {
border-radius: 8px;
backdrop-filter: blur(20px);
cursor: pointer;
@@ -871,6 +871,7 @@ button.regenerate_button, button.continue_button, button.options_button {
}
select:hover,
+input.model:hover
.buttons button:hover,
.stop_generating button:hover,
.toolbar .regenerate button:hover,
@@ -948,7 +949,7 @@ select:hover,
}
@media only screen and (min-width: 40em) {
- select {
+ select, input.model {
width: 200px;
}
.field {
@@ -1446,7 +1447,7 @@ form .field.saved .fa-xmark {
max-height: 200px;
}
-.hidden {
+.hidden, input.hidden {
display: none;
}
diff --git a/g4f/gui/client/static/img/site.webmanifest b/g4f/gui/client/static/img/site.webmanifest
index 2367fad0..7756f2a4 100644
--- a/g4f/gui/client/static/img/site.webmanifest
+++ b/g4f/gui/client/static/img/site.webmanifest
@@ -17,7 +17,7 @@
"background_color": "#ffffff",
"display": "standalone",
"share_target": {
- "action": "/chat/",
+ "action": "/chat/share",
"method": "GET",
"enctype": "application/x-www-form-urlencoded",
"params": {
diff --git a/g4f/gui/client/static/js/chat.v1.js b/g4f/gui/client/static/js/chat.v1.js
index e7733e44..fd5b0112 100644
--- a/g4f/gui/client/static/js/chat.v1.js
+++ b/g4f/gui/client/static/js/chat.v1.js
@@ -15,6 +15,7 @@ const inputCount = document.getElementById("input-count").querySelector("
const providerSelect = document.getElementById("provider");
const modelSelect = document.getElementById("model");
const modelProvider = document.getElementById("model2");
+const custom_model = document.getElementById("model3");
const chatPrompt = document.getElementById("chatPrompt");
const settings = document.querySelector(".settings");
const chat = document.querySelector(".conversation");
@@ -78,18 +79,28 @@ function render_reasoning(reasoning, final = false) {
` : "";
return `
- Reasoning : ${escapeHtml(reasoning.status)}
+ Reasoning 🧠: ${escapeHtml(reasoning.status)}
${inner_text}
`;
}
+function render_reasoning_text(reasoning) {
+ return `Reasoning 🧠: ${reasoning.status}\n\n${reasoning.text}\n\n`;
+}
+
function filter_message(text) {
return text.replaceAll(
/[\s\S]+/gm, ""
).replace(/ \[aborted\]$/g, "").replace(/ \[error\]$/g, "");
}
+function filter_message_content(text) {
+ return text.replaceAll(
+ /\/\]\(\/generate\//gm, "/](/images/"
+ ).replace(/ \[aborted\]$/g, "").replace(/ \[error\]$/g, "")
+}
+
function fallback_clipboard (text) {
var textBox = document.createElement("textarea");
textBox.value = text;
@@ -182,6 +193,53 @@ const get_message_el = (el) => {
return message_el;
}
+function register_message_images() {
+ message_box.querySelectorAll(`.loading-indicator`).forEach((el) => el.remove());
+ message_box.querySelectorAll(`.message img:not([alt="your avatar"])`).forEach(async (el) => {
+ if (!el.complete) {
+ const indicator = document.createElement("span");
+ indicator.classList.add("loading-indicator");
+ indicator.innerHTML = ``;
+ el.parentElement.appendChild(indicator);
+ el.onerror = () => {
+ let indexCommand;
+ if ((indexCommand = el.src.indexOf("/generate/")) >= 0) {
+ indexCommand = indexCommand + "/generate/".length + 1;
+ let newPath = el.src.substring(indexCommand)
+ let filename = newPath.replace(/(?:\?.+?|$)/, "");
+ let seed = Math.floor(Date.now() / 1000);
+ newPath = `https://image.pollinations.ai/prompt/${newPath}?seed=${seed}&nologo=true`;
+ let downloadUrl = newPath;
+ if (document.getElementById("download_images")?.checked) {
+ downloadUrl = `/images/${filename}?url=${escapeHtml(newPath)}`;
+ }
+ const link = document.createElement("a");
+ link.setAttribute("href", newPath);
+ const newImg = document.createElement("img");
+ newImg.src = downloadUrl;
+ newImg.alt = el.alt;
+ newImg.onload = () => {
+ lazy_scroll_to_bottom();
+ indicator.remove();
+ }
+ link.appendChild(newImg);
+ el.parentElement.appendChild(link);
+ } else {
+ const span = document.createElement("span");
+ span.innerHTML = `${escapeHtml(el.alt)}`;
+ el.parentElement.appendChild(span);
+ }
+ el.remove();
+ indicator.remove();
+ }
+ el.onload = () => {
+ indicator.remove();
+ lazy_scroll_to_bottom();
+ }
+ }
+ });
+}
+
const register_message_buttons = async () => {
message_box.querySelectorAll(".message .content .provider").forEach(async (el) => {
if (!("click" in el.dataset)) {
@@ -243,24 +301,22 @@ const register_message_buttons = async () => {
message_box.querySelectorAll(".message .fa-file-export").forEach(async (el) => {
if (!("click" in el.dataset)) {
el.dataset.click = "true";
+ //
el.addEventListener("click", async () => {
const elem = window.document.createElement('a');
let filename = `chat ${new Date().toLocaleString()}.md`.replaceAll(":", "-");
const conversation = await get_conversation(window.conversation_id);
let buffer = "";
conversation.items.forEach(message => {
+ buffer += render_reasoning_text(message.reasoning);
buffer += `${message.role == 'user' ? 'User' : 'Assistant'}: ${message.content.trim()}\n\n\n`;
});
- const file = new File([buffer.trim()], 'message.md', {type: 'text/plain'});
- const objectUrl = URL.createObjectURL(file);
- elem.href = objectUrl;
- elem.download = filename;
- document.body.appendChild(elem);
- elem.click();
- document.body.removeChild(elem);
+ var download = document.getElementById("download");
+ download.setAttribute("href", "data:text/markdown;charset=utf-8," + encodeURIComponent(buffer.trim()));
+ download.setAttribute("download", filename);
+ download.click();
el.classList.add("clicked");
setTimeout(() => el.classList.remove("clicked"), 1000);
- URL.revokeObjectURL(objectUrl);
})
}
});
@@ -376,7 +432,7 @@ const handle_ask = async (do_ask_gpt = true) => {
messageInput.focus();
await scroll_to_bottom();
- let message = messageInput.value;
+ let message = messageInput.value.trim();
if (message.length <= 0) {
return;
}
@@ -755,6 +811,7 @@ async function add_message_chunk(message, message_id, provider, scroll, finish_m
if (!img.complete)
return;
content_map.inner.innerHTML = markdown_render(message.preview);
+ await register_message_images();
} else if (message.type == "content") {
message_storage[message_id] += message.content;
update_message(content_map, message_id, null, scroll);
@@ -779,7 +836,7 @@ async function add_message_chunk(message, message_id, provider, scroll, finish_m
} else if (message.type == "reasoning") {
if (!reasoning_storage[message_id]) {
reasoning_storage[message_id] = message;
- reasoning_storage[message_id].text = message.token || "";
+ reasoning_storage[message_id].text = "";
} else if (message.status) {
reasoning_storage[message_id].status = message.status;
} else if (message.token) {
@@ -952,6 +1009,7 @@ const ask_gpt = async (message_id, message_index = -1, regenerate = false, provi
}
await safe_remove_cancel_button();
await register_message_buttons();
+ await register_message_images();
await load_conversations();
regenerate_button.classList.remove("regenerate-hidden");
}
@@ -1201,8 +1259,8 @@ const load_conversation = async (conversation_id, scroll=true) => {
} else {
buffer = "";
}
- buffer = buffer.replace(/ \[aborted\]$/g, "").replace(/ \[error\]$/g, "");
- new_content = item.content.replace(/ \[aborted\]$/g, "").replace(/ \[error\]$/g, "");
+ buffer = filter_message_content(buffer);
+ new_content = filter_message_content(item.content);
buffer = merge_messages(buffer, new_content);
last_model = item.provider?.model;
providers.push(item.provider?.name);
@@ -1658,12 +1716,9 @@ const register_settings_storage = async () => {
const load_settings_storage = async () => {
const optionElements = document.querySelectorAll(optionElementsSelector);
optionElements.forEach((element) => {
- if (element.name && element.name != element.id && (value = appStorage.getItem(element.name))) {
- appStorage.setItem(element.id, value);
- appStorage.removeItem(element.name);
- }
- if (!(value = appStorage.getItem(element.id))) {
- return;
+ value = appStorage.getItem(element.id);
+ if (value == null && element.dataset.value) {
+ value = element.dataset.value;
}
if (value) {
switch (element.type) {
@@ -1677,10 +1732,10 @@ const load_settings_storage = async () => {
case "number":
case "textarea":
if (element.id.endsWith("-api_key")) {
- element.placeholder = value && value.length >= 22 ? (value.substring(0, 12)+"*".repeat(12)+value.substring(value.length-12)) : "*".repeat(value.length);
+ element.placeholder = value && value.length >= 22 ? (value.substring(0, 12)+"*".repeat(12)+value.substring(value.length-12)) : "*".repeat(value ? value.length : 0);
element.dataset.value = value;
} else {
- element.value = value;
+ element.value = value == null ? element.dataset.value : value;
}
break;
default:
@@ -1834,7 +1889,7 @@ async function on_load() {
let chat_url = new URL(window.location.href)
let chat_params = new URLSearchParams(chat_url.search);
if (chat_params.get("prompt")) {
- messageInput.value = `${chat_params.title}\n${chat_params.prompt}\n${chat_params.url}`.trim();
+ messageInput.value = `${window.location.href}\n`;
messageInput.style.height = messageInput.scrollHeight + "px";
messageInput.focus();
//await handle_ask();
@@ -2255,7 +2310,9 @@ chatPrompt?.addEventListener("input", async () => {
});
function get_selected_model() {
- if (modelProvider.selectedIndex >= 0) {
+ if (custom_model.value) {
+ return custom_model;
+ } else if (modelProvider.selectedIndex >= 0) {
return modelProvider.options[modelProvider.selectedIndex];
} else if (modelSelect.selectedIndex >= 0) {
model = modelSelect.options[modelSelect.selectedIndex];
@@ -2401,17 +2458,31 @@ async function load_provider_models(provider=null) {
if (!provider) {
provider = providerSelect.value;
}
+ if (!custom_model.value) {
+ custom_model.classList.add("hidden");
+ }
+ if (provider == "Custom Model" || custom_model.value) {
+ modelProvider.classList.add("hidden");
+ modelSelect.classList.add("hidden");
+ document.getElementById("model3").classList.remove("hidden");
+ return;
+ }
modelProvider.innerHTML = '';
modelProvider.name = `model[${provider}]`;
if (!provider) {
modelProvider.classList.add("hidden");
modelSelect.classList.remove("hidden");
+ document.getElementById("model3").value = "";
+ document.getElementById("model3").classList.remove("hidden");
return;
}
const models = await api('models', provider);
if (models && models.length > 0) {
modelSelect.classList.add("hidden");
- modelProvider.classList.remove("hidden");
+ if (!custom_model.value) {
+ custom_model.classList.add("hidden");
+ modelProvider.classList.remove("hidden");
+ }
let defaultIndex = 0;
models.forEach((model, i) => {
let option = document.createElement('option');
@@ -2423,11 +2494,13 @@ async function load_provider_models(provider=null) {
defaultIndex = i;
}
});
- modelProvider.selectedIndex = defaultIndex;
let value = appStorage.getItem(modelProvider.name);
if (value) {
modelProvider.value = value;
}
+ modelProvider.selectedIndex = defaultIndex;
+ } else if (custom_model.value) {
+ modelSelect.classList.add("hidden");
} else {
modelProvider.classList.add("hidden");
modelSelect.classList.remove("hidden");
@@ -2439,6 +2512,12 @@ providerSelect.addEventListener("change", () => {
});
modelSelect.addEventListener("change", () => messageInput.focus());
modelProvider.addEventListener("change", () => messageInput.focus());
+custom_model.addEventListener("change", () => {
+ if (!custom_model.value) {
+ load_provider_models();
+ }
+ messageInput.focus();
+});
document.getElementById("pin").addEventListener("click", async () => {
const pin_container = document.getElementById("pin_container");
diff --git a/g4f/gui/server/backend_api.py b/g4f/gui/server/backend_api.py
index 0e83c889..16d27798 100644
--- a/g4f/gui/server/backend_api.py
+++ b/g4f/gui/server/backend_api.py
@@ -276,7 +276,7 @@ class Backend_Api(Api):
response = iter_run_tools(ChatCompletion.create, **parameters)
if do_filter_markdown:
- return Response(filter_markdown(response, do_filter_markdown), mimetype='text/plain')
+ return Response(filter_markdown("".join([str(chunk) for chunk in response]), do_filter_markdown), mimetype='text/plain')
def cast_str():
for chunk in response:
if not isinstance(chunk, Exception):
diff --git a/g4f/tools/run_tools.py b/g4f/tools/run_tools.py
index 436e7f50..54a9b237 100644
--- a/g4f/tools/run_tools.py
+++ b/g4f/tools/run_tools.py
@@ -155,16 +155,28 @@ def iter_run_tools(
yield chunk
continue
if "" in chunk:
- chunk = chunk.split("", 1)
- yield chunk[0]
- yield Reasoning(None, "Is thinking...", is_thinking="")
- yield Reasoning(chunk[1])
+ if chunk != "":
+ chunk = chunk.split("", 1)
+ if len(chunk) > 0 and chunk[0]:
+ yield chunk[0]
+ yield Reasoning(None, "🤔 Is thinking...", is_thinking="")
+ if chunk != "":
+ if len(chunk) > 1 and chunk[1]:
+ yield Reasoning(chunk[1])
is_thinking = time.time()
if "" in chunk:
- chunk = chunk.split("", 1)
- yield Reasoning(chunk[0])
- yield Reasoning(None, f"Finished in {round(time.time()-is_thinking, 2)} seconds", is_thinking="")
- yield chunk[1]
+ if chunk != "":
+ chunk = chunk.split("", 1)
+ if len(chunk) > 0 and chunk[0]:
+ yield Reasoning(chunk[0])
+ is_thinking = time.time() - is_thinking
+ if is_thinking > 1:
+ yield Reasoning(None, f"Thought for {is_thinking:.2f}s", is_thinking="")
+ else:
+ yield Reasoning(None, f"Finished", is_thinking="")
+ if chunk != "":
+ if len(chunk) > 1 and chunk[1]:
+ yield chunk[1]
is_thinking = 0
elif is_thinking:
yield Reasoning(chunk)
--
cgit v1.2.3