Merge pull request #2920 from hlohaus/30Mar

feat: add LM Arena provider, async‑ify Copilot & surface follow‑up su…
This commit is contained in:
H Lohaus 2025-04-17 09:06:44 +02:00 committed by GitHub
commit 0c0c72c203
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
29 changed files with 630 additions and 320 deletions

View file

@ -26,6 +26,10 @@ from typing import Optional, Dict, Any, List, Tuple
from g4f.client import Client from g4f.client import Client
from g4f.models import ModelUtils from g4f.models import ModelUtils
import g4f.Provider
from g4f import debug
debug.logging = True
# Constants # Constants
DEFAULT_MODEL = "claude-3.7-sonnet" DEFAULT_MODEL = "claude-3.7-sonnet"
@ -184,16 +188,21 @@ def generate_commit_message(diff_text: str, model: str = DEFAULT_MODEL) -> Optio
# Make API call # Make API call
response = client.chat.completions.create( response = client.chat.completions.create(
prompt,
model=model, model=model,
messages=[{"role": "user", "content": prompt}] stream=True,
) )
content = []
for chunk in response:
# Stop spinner and clear line # Stop spinner and clear line
if spinner:
spinner.set() spinner.set()
sys.stdout.write("\r" + " " * 50 + "\r") print(" " * 50 + "\n", flush=True)
sys.stdout.flush() spinner = None
if isinstance(chunk.choices[0].delta.content, str):
return response.choices[0].message.content.strip() content.append(chunk.choices[0].delta.content)
print(chunk.choices[0].delta.content, end="", flush=True)
return "".join(content).strip()
except Exception as e: except Exception as e:
# Stop spinner if it's running # Stop spinner if it's running
if 'spinner' in locals() and spinner: if 'spinner' in locals() and spinner:
@ -306,11 +315,6 @@ def main():
print("Failed to generate commit message after multiple attempts.") print("Failed to generate commit message after multiple attempts.")
sys.exit(1) sys.exit(1)
print("\nGenerated commit message:")
print("-" * 50)
print(commit_message)
print("-" * 50)
if args.edit: if args.edit:
print("\nOpening editor to modify commit message...") print("\nOpening editor to modify commit message...")
commit_message = edit_commit_message(commit_message) commit_message = edit_commit_message(commit_message)

View file

@ -14,12 +14,13 @@ from datetime import datetime, timedelta
from ..typing import AsyncResult, Messages, MediaListType from ..typing import AsyncResult, Messages, MediaListType
from ..requests.raise_for_status import raise_for_status from ..requests.raise_for_status import raise_for_status
from .base_provider import AsyncGeneratorProvider, ProviderModelMixin from .base_provider import AsyncGeneratorProvider, ProviderModelMixin
from .openai.har_file import get_har_files
from ..image import to_data_uri from ..image import to_data_uri
from ..cookies import get_cookies_dir from ..cookies import get_cookies_dir
from .helper import format_image_prompt from .helper import format_image_prompt, render_messages
from ..providers.response import JsonConversation, ImageResponse from ..providers.response import JsonConversation, ImageResponse
from ..tools.media import merge_media from ..tools.media import merge_media
from ..errors import PaymentRequiredError from ..errors import RateLimitError
from .. import debug from .. import debug
class Conversation(JsonConversation): class Conversation(JsonConversation):
@ -428,15 +429,9 @@ class Blackbox(AsyncGeneratorProvider, ProviderModelMixin):
Optional[dict]: Session data if found, None otherwise Optional[dict]: Session data if found, None otherwise
""" """
try: try:
har_dir = get_cookies_dir() for file in get_har_files():
if not os.access(har_dir, os.R_OK):
return None
for root, _, files in os.walk(har_dir):
for file in files:
if file.endswith(".har"):
try: try:
with open(os.path.join(root, file), 'rb') as f: with open(file, 'rb') as f:
har_data = json.load(f) har_data = json.load(f)
for entry in har_data['log']['entries']: for entry in har_data['log']['entries']:
@ -451,7 +446,6 @@ class Blackbox(AsyncGeneratorProvider, ProviderModelMixin):
'"user"' in content['text'] and '"user"' in content['text'] and
'"email"' in content['text'] and '"email"' in content['text'] and
'"expires"' in content['text']): '"expires"' in content['text']):
try: try:
# Remove any HTML or other non-JSON content # Remove any HTML or other non-JSON content
text = content['text'].strip() text = content['text'].strip()
@ -573,7 +567,7 @@ class Blackbox(AsyncGeneratorProvider, ProviderModelMixin):
conversation.message_history = [] conversation.message_history = []
current_messages = [] current_messages = []
for i, msg in enumerate(messages): for i, msg in enumerate(render_messages(messages)):
msg_id = conversation.chat_id if i == 0 and msg["role"] == "user" else cls.generate_id() msg_id = conversation.chat_id if i == 0 and msg["role"] == "user" else cls.generate_id()
current_msg = { current_msg = {
"id": msg_id, "id": msg_id,
@ -690,8 +684,8 @@ class Blackbox(AsyncGeneratorProvider, ProviderModelMixin):
async for chunk in response.content.iter_any(): async for chunk in response.content.iter_any():
if chunk: if chunk:
chunk_text = chunk.decode() chunk_text = chunk.decode()
if chunk_text == "You have reached your request limit for the hour": if "You have reached your request limit for the hour" in chunk_text:
raise PaymentRequiredError(chunk_text) raise RateLimitError(chunk_text)
full_response.append(chunk_text) full_response.append(chunk_text)
# Only yield chunks for non-image models # Only yield chunks for non-image models
if model != cls.default_image_model: if model != cls.default_image_model:

View file

@ -9,7 +9,7 @@ from ..requests import Session, StreamSession, get_args_from_nodriver, raise_for
from ..requests import DEFAULT_HEADERS, has_nodriver, has_curl_cffi from ..requests import DEFAULT_HEADERS, has_nodriver, has_curl_cffi
from ..providers.response import FinishReason, Usage from ..providers.response import FinishReason, Usage
from ..errors import ResponseStatusError, ModelNotFoundError from ..errors import ResponseStatusError, ModelNotFoundError
from .helper import to_string from .helper import render_messages
class Cloudflare(AsyncGeneratorProvider, ProviderModelMixin, AuthFileMixin): class Cloudflare(AsyncGeneratorProvider, ProviderModelMixin, AuthFileMixin):
label = "Cloudflare AI" label = "Cloudflare AI"
@ -82,7 +82,7 @@ class Cloudflare(AsyncGeneratorProvider, ProviderModelMixin, AuthFileMixin):
elif has_nodriver: elif has_nodriver:
cls._args = await get_args_from_nodriver(cls.url, proxy, timeout, cookies) cls._args = await get_args_from_nodriver(cls.url, proxy, timeout, cookies)
else: else:
cls._args = {"headers": DEFAULT_HEADERS, "cookies": {}} cls._args = {"headers": DEFAULT_HEADERS, "cookies": {}, "impersonate": "chrome"}
try: try:
model = cls.get_model(model) model = cls.get_model(model)
except ModelNotFoundError: except ModelNotFoundError:
@ -90,8 +90,7 @@ class Cloudflare(AsyncGeneratorProvider, ProviderModelMixin, AuthFileMixin):
data = { data = {
"messages": [{ "messages": [{
**message, **message,
"content": to_string(message["content"]), "parts": [{"type":"text", "text": message["content"]}]} for message in render_messages(messages)],
"parts": [{"type":"text", "text": to_string(message["content"])}]} for message in messages],
"lora": None, "lora": None,
"model": model, "model": model,
"max_tokens": max_tokens, "max_tokens": max_tokens,

View file

@ -3,11 +3,10 @@ from __future__ import annotations
import os import os
import json import json
import asyncio import asyncio
import base64
from urllib.parse import quote from urllib.parse import quote
try: try:
from curl_cffi.requests import Session from curl_cffi.requests import AsyncSession
from curl_cffi import CurlWsFlag from curl_cffi import CurlWsFlag
has_curl_cffi = True has_curl_cffi = True
except ImportError: except ImportError:
@ -18,14 +17,12 @@ try:
except ImportError: except ImportError:
has_nodriver = False has_nodriver = False
from .base_provider import AbstractProvider, ProviderModelMixin from .base_provider import AsyncGeneratorProvider, ProviderModelMixin
from .helper import format_prompt_max_length from .helper import format_prompt_max_length
from .openai.har_file import get_headers, get_har_files from .openai.har_file import get_headers, get_har_files
from ..typing import CreateResult, Messages, MediaListType from ..typing import AsyncResult, Messages, MediaListType
from ..errors import MissingRequirementsError, NoValidHarFileError, MissingAuthError from ..errors import MissingRequirementsError, NoValidHarFileError, MissingAuthError
from ..requests.raise_for_status import raise_for_status from ..providers.response import BaseConversation, JsonConversation, RequestLogin, ImageResponse, FinishReason, SuggestedFollowups, TitleGeneration, Sources, SourceLink
from ..providers.response import BaseConversation, JsonConversation, RequestLogin, ImageResponse, FinishReason, SuggestedFollowups
from ..providers.asyncio import get_running_loop
from ..tools.media import merge_media from ..tools.media import merge_media
from ..requests import get_nodriver from ..requests import get_nodriver
from ..image import to_bytes, is_accepted_format from ..image import to_bytes, is_accepted_format
@ -38,7 +35,7 @@ class Conversation(JsonConversation):
def __init__(self, conversation_id: str): def __init__(self, conversation_id: str):
self.conversation_id = conversation_id self.conversation_id = conversation_id
class Copilot(AbstractProvider, ProviderModelMixin): class Copilot(AsyncGeneratorProvider, ProviderModelMixin):
label = "Microsoft Copilot" label = "Microsoft Copilot"
url = "https://copilot.microsoft.com" url = "https://copilot.microsoft.com"
@ -62,20 +59,20 @@ class Copilot(AbstractProvider, ProviderModelMixin):
_cookies: dict = None _cookies: dict = None
@classmethod @classmethod
def create_completion( async def create_async_generator(
cls, cls,
model: str, model: str,
messages: Messages, messages: Messages,
stream: bool = False, stream: bool = False,
proxy: str = None, proxy: str = None,
timeout: int = 900, timeout: int = 30,
prompt: str = None, prompt: str = None,
media: MediaListType = None, media: MediaListType = None,
conversation: BaseConversation = None, conversation: BaseConversation = None,
return_conversation: bool = False, return_conversation: bool = False,
api_key: str = None, api_key: str = None,
**kwargs **kwargs
) -> CreateResult: ) -> AsyncResult:
if not has_curl_cffi: if not has_curl_cffi:
raise MissingRequirementsError('Install or update "curl_cffi" package | pip install -U curl_cffi') raise MissingRequirementsError('Install or update "curl_cffi" package | pip install -U curl_cffi')
model = cls.get_model(model) model = cls.get_model(model)
@ -91,14 +88,13 @@ class Copilot(AbstractProvider, ProviderModelMixin):
debug.log(f"Copilot: {h}") debug.log(f"Copilot: {h}")
if has_nodriver: if has_nodriver:
yield RequestLogin(cls.label, os.environ.get("G4F_LOGIN_URL", "")) yield RequestLogin(cls.label, os.environ.get("G4F_LOGIN_URL", ""))
get_running_loop(check_nested=True) cls._access_token, cls._cookies = await get_access_token_and_cookies(cls.url, proxy)
cls._access_token, cls._cookies = asyncio.run(get_access_token_and_cookies(cls.url, proxy))
else: else:
raise h raise h
websocket_url = f"{websocket_url}&accessToken={quote(cls._access_token)}" websocket_url = f"{websocket_url}&accessToken={quote(cls._access_token)}"
headers = {"authorization": f"Bearer {cls._access_token}"} headers = {"authorization": f"Bearer {cls._access_token}"}
with Session( async with AsyncSession(
timeout=timeout, timeout=timeout,
proxy=proxy, proxy=proxy,
impersonate="chrome", impersonate="chrome",
@ -107,35 +103,19 @@ class Copilot(AbstractProvider, ProviderModelMixin):
) as session: ) as session:
if cls._access_token is not None: if cls._access_token is not None:
cls._cookies = session.cookies.jar if hasattr(session.cookies, "jar") else session.cookies cls._cookies = session.cookies.jar if hasattr(session.cookies, "jar") else session.cookies
# if cls._access_token is None: response = await session.get("https://copilot.microsoft.com/c/api/user")
# try:
# url = "https://copilot.microsoft.com/cl/eus-sc/collect"
# headers = {
# "Accept": "application/x-clarity-gzip",
# "referrer": "https://copilot.microsoft.com/onboarding"
# }
# response = session.post(url, headers=headers, data=get_clarity())
# clarity_token = json.loads(response.text.split(" ", maxsplit=1)[-1])[0]["value"]
# debug.log(f"Copilot: Clarity Token: ...{clarity_token[-12:]}")
# except Exception as e:
# debug.log(f"Copilot: {e}")
# else:
# clarity_token = None
response = session.get("https://copilot.microsoft.com/c/api/user")
if response.status_code == 401: if response.status_code == 401:
raise MissingAuthError("Status 401: Invalid access token") raise MissingAuthError("Status 401: Invalid access token")
raise_for_status(response) response.raise_for_status()
user = response.json().get('firstName') user = response.json().get('firstName')
if user is None: if user is None:
cls._access_token = None cls._access_token = None
debug.log(f"Copilot: User: {user or 'null'}") debug.log(f"Copilot: User: {user or 'null'}")
if conversation is None: if conversation is None:
response = session.post(cls.conversation_url) response = await session.post(cls.conversation_url)
raise_for_status(response) response.raise_for_status()
conversation_id = response.json().get("id") conversation_id = response.json().get("id")
conversation = Conversation(conversation_id) conversation = Conversation(conversation_id)
if return_conversation:
yield conversation
if prompt is None: if prompt is None:
prompt = format_prompt_max_length(messages, 10000) prompt = format_prompt_max_length(messages, 10000)
debug.log(f"Copilot: Created conversation: {conversation_id}") debug.log(f"Copilot: Created conversation: {conversation_id}")
@ -144,30 +124,28 @@ class Copilot(AbstractProvider, ProviderModelMixin):
if prompt is None: if prompt is None:
prompt = get_last_user_message(messages) prompt = get_last_user_message(messages)
debug.log(f"Copilot: Use conversation: {conversation_id}") debug.log(f"Copilot: Use conversation: {conversation_id}")
if return_conversation:
yield conversation
uploaded_images = [] uploaded_images = []
media, _ = [(None, None), *merge_media(media, messages)].pop() for media, _ in merge_media(media, messages):
if media:
if not isinstance(media, str): if not isinstance(media, str):
data = to_bytes(media) data = to_bytes(media)
response = session.post( response = await session.post(
"https://copilot.microsoft.com/c/api/attachments", "https://copilot.microsoft.com/c/api/attachments",
headers={"content-type": is_accepted_format(data)}, headers={
"content-type": is_accepted_format(data),
"content-length": str(len(data)),
},
data=data data=data
) )
raise_for_status(response) response.raise_for_status()
media = response.json().get("url") media = response.json().get("url")
uploaded_images.append({"type":"image", "url": media}) uploaded_images.append({"type":"image", "url": media})
wss = session.ws_connect(cls.websocket_url) wss = await session.ws_connect(cls.websocket_url, timeout=3)
# if clarity_token is not None: await wss.send(json.dumps({"event":"setOptions","supportedCards":["weather","local","image","sports","video","ads","finance"],"ads":{"supportedTypes":["multimedia","product","tourActivity","propertyPromotion","text"]}}));
# wss.send(json.dumps({ await wss.send(json.dumps({
# "event": "challengeResponse",
# "token": clarity_token,
# "method":"clarity"
# }).encode(), CurlWsFlag.TEXT)
wss.send(json.dumps({"event":"setOptions","supportedCards":["weather","local","image","sports","video","ads","finance"],"ads":{"supportedTypes":["multimedia","product","tourActivity","propertyPromotion","text"]}}));
wss.send(json.dumps({
"event": "send", "event": "send",
"conversationId": conversation_id, "conversationId": conversation_id,
"content": [*uploaded_images, { "content": [*uploaded_images, {
@ -177,20 +155,20 @@ class Copilot(AbstractProvider, ProviderModelMixin):
"mode": "reasoning" if "Think" in model else "chat", "mode": "reasoning" if "Think" in model else "chat",
}).encode(), CurlWsFlag.TEXT) }).encode(), CurlWsFlag.TEXT)
is_started = False done = False
msg = None msg = None
image_prompt: str = None image_prompt: str = None
last_msg = None last_msg = None
sources = {}
try: try:
while True: while not wss.closed:
try: try:
msg = wss.recv()[0] msg = await asyncio.wait_for(wss.recv(), 3 if done else timeout)
msg = json.loads(msg) msg = json.loads(msg[0])
except: except:
break break
last_msg = msg last_msg = msg
if msg.get("event") == "appendText": if msg.get("event") == "appendText":
is_started = True
yield msg.get("text") yield msg.get("text")
elif msg.get("event") == "generatingImage": elif msg.get("event") == "generatingImage":
image_prompt = msg.get("prompt") image_prompt = msg.get("prompt")
@ -198,20 +176,28 @@ class Copilot(AbstractProvider, ProviderModelMixin):
yield ImageResponse(msg.get("url"), image_prompt, {"preview": msg.get("thumbnailUrl")}) yield ImageResponse(msg.get("url"), image_prompt, {"preview": msg.get("thumbnailUrl")})
elif msg.get("event") == "done": elif msg.get("event") == "done":
yield FinishReason("stop") yield FinishReason("stop")
break done = True
elif msg.get("event") == "suggestedFollowups": elif msg.get("event") == "suggestedFollowups":
yield SuggestedFollowups(msg.get("suggestions")) yield SuggestedFollowups(msg.get("suggestions"))
break break
elif msg.get("event") == "replaceText": elif msg.get("event") == "replaceText":
yield msg.get("text") yield msg.get("text")
elif msg.get("event") == "titleUpdate":
yield TitleGeneration(msg.get("title"))
elif msg.get("event") == "citation":
sources[msg.get("url")] = msg
yield SourceLink(list(sources.keys()).index(msg.get("url")), msg.get("url"))
elif msg.get("event") == "error": elif msg.get("event") == "error":
raise RuntimeError(f"Error: {msg}") raise RuntimeError(f"Error: {msg}")
elif msg.get("event") not in ["received", "startMessage", "citation", "partCompleted"]: elif msg.get("event") not in ["received", "startMessage", "partCompleted"]:
debug.log(f"Copilot Message: {msg}") debug.log(f"Copilot Message: {msg}")
if not is_started: if not done:
raise RuntimeError(f"Invalid response: {last_msg}") raise RuntimeError(f"Invalid response: {last_msg}")
if sources:
yield Sources(sources.values())
finally: finally:
wss.close() if not wss.closed:
await wss.close()
async def get_access_token_and_cookies(url: str, proxy: str = None, target: str = "ChatAI",): async def get_access_token_and_cookies(url: str, proxy: str = None, target: str = "ChatAI",):
browser, stop_browser = await get_nodriver(proxy=proxy, user_data_dir="copilot") browser, stop_browser = await get_nodriver(proxy=proxy, user_data_dir="copilot")
@ -264,8 +250,3 @@ def readHAR(url: str):
raise NoValidHarFileError("No access token found in .har files") raise NoValidHarFileError("No access token found in .har files")
return api_key, cookies return api_key, cookies
def get_clarity() -> bytes:
#{"e":["0.7.58",5,7284,4779,"n59ae4ieqq","aln5en","1upufhz",1,0,0],"a":[[7323,12,65,217,324],[7344,12,65,214,329],[7385,12,65,211,334],[7407,12,65,210,337],[7428,12,65,209,338],[7461,12,65,209,339],[7497,12,65,209,339],[7531,12,65,208,340],[7545,12,65,208,342],[11654,13,65,208,342],[11728,14,65,208,342],[11728,9,65,208,342,17535,19455,0,0,0,"Annehmen",null,"52w7wqv1r.8ovjfyrpu",1],[7284,4,1,393,968,393,968,0,0,231,310,939,0],[12063,0,2,147,3,4,4,18,5,1,10,79,25,15],[12063,36,6,[11938,0]]]}
body = base64.b64decode("H4sIAAAAAAAAA23RwU7DMAwG4HfJ2aqS2E5ibjxH1cMOnQYqYZvUTQPx7vyJRGGAemj01XWcP+9udg+j80MetDhSyrEISc5GrqrtZnmaTydHbrdUnSsWYT2u+8Obo0Ce/IQvaDBmjkwhUlKKIRNHmQgosqEArWPRDQMx90rxeUMPzB1j+UJvwNIxhTvsPcXyX1T+rizE4juK3mEEhpAUg/JvzW1/+U/tB1LATmhqotoiweMea50PLy2vui4LOY3XfD1dwnkor5fn/e18XBFgm6fHjSzZmCyV7d3aRByAEYextaTHEH3i5pgKGVP/s+DScE5PuLKIpW6FnCi1gY3Rbpqmj0/DI/+L7QEAAA==")
return body

View file

@ -3,28 +3,21 @@ from __future__ import annotations
import asyncio import asyncio
try: try:
from duckduckgo_search import DDGS from duckai import DuckAI
from duckduckgo_search.exceptions import DuckDuckGoSearchException, RatelimitException, ConversationLimitException
has_requirements = True has_requirements = True
except ImportError: except ImportError:
has_requirements = False has_requirements = False
try:
import nodriver
has_nodriver = True
except ImportError:
has_nodriver = False
from ..typing import AsyncResult, Messages from ..typing import CreateResult, Messages
from ..requests import get_nodriver from .base_provider import AbstractProvider, ProviderModelMixin
from .base_provider import AsyncGeneratorProvider, ProviderModelMixin
from .helper import get_last_user_message from .helper import get_last_user_message
class DuckDuckGo(AsyncGeneratorProvider, ProviderModelMixin): class DuckDuckGo(AbstractProvider, ProviderModelMixin):
label = "Duck.ai (duckduckgo_search)" label = "Duck.ai (duckduckgo_search)"
url = "https://duckduckgo.com/aichat" url = "https://duckduckgo.com/aichat"
api_base = "https://duckduckgo.com/duckchat/v1/" api_base = "https://duckduckgo.com/duckchat/v1/"
working = False working = has_requirements
supports_stream = True supports_stream = True
supports_system_message = True supports_system_message = True
supports_message_history = True supports_message_history = True
@ -32,7 +25,7 @@ class DuckDuckGo(AsyncGeneratorProvider, ProviderModelMixin):
default_model = "gpt-4o-mini" default_model = "gpt-4o-mini"
models = [default_model, "meta-llama/Llama-3.3-70B-Instruct-Turbo", "claude-3-haiku-20240307", "o3-mini", "mistralai/Mistral-Small-24B-Instruct-2501"] models = [default_model, "meta-llama/Llama-3.3-70B-Instruct-Turbo", "claude-3-haiku-20240307", "o3-mini", "mistralai/Mistral-Small-24B-Instruct-2501"]
ddgs: DDGS = None duck_ai: DuckAI = None
model_aliases = { model_aliases = {
"gpt-4": "gpt-4o-mini", "gpt-4": "gpt-4o-mini",
@ -42,44 +35,17 @@ class DuckDuckGo(AsyncGeneratorProvider, ProviderModelMixin):
} }
@classmethod @classmethod
async def create_async_generator( def create_completion(
cls, cls,
model: str, model: str,
messages: Messages, messages: Messages,
proxy: str = None, proxy: str = None,
timeout: int = 60, timeout: int = 60,
**kwargs **kwargs
) -> AsyncResult: ) -> CreateResult:
if not has_requirements: if not has_requirements:
raise ImportError("duckduckgo_search is not installed. Install it with `pip install duckduckgo-search`.") raise ImportError("duckai is not installed. Install it with `pip install -U duckai`.")
if cls.ddgs is None: if cls.duck_ai is None:
cls.ddgs = DDGS(proxy=proxy, timeout=timeout) cls.duck_ai = DuckAI(proxy=proxy, timeout=timeout)
if has_nodriver:
await cls.nodriver_auth(proxy=proxy)
model = cls.get_model(model) model = cls.get_model(model)
for chunk in cls.ddgs.chat_yield(get_last_user_message(messages), model, timeout): yield cls.duck_ai.chat(get_last_user_message(messages), model, timeout)
yield chunk
@classmethod
async def nodriver_auth(cls, proxy: str = None):
browser, stop_browser = await get_nodriver(proxy=proxy)
try:
page = browser.main_tab
def on_request(event: nodriver.cdp.network.RequestWillBeSent, page=None):
if cls.api_base in event.request.url:
if "X-Vqd-4" in event.request.headers:
cls.ddgs._chat_vqd = event.request.headers["X-Vqd-4"]
if "X-Vqd-Hash-1" in event.request.headers:
cls.ddgs._chat_vqd_hash = event.request.headers["X-Vqd-Hash-1"]
if "F-Fe-Version" in event.request.headers:
cls.ddgs._chat_xfe = event.request.headers["F-Fe-Version" ]
await page.send(nodriver.cdp.network.enable())
page.add_handler(nodriver.cdp.network.RequestWillBeSent, on_request)
page = await browser.get(cls.url)
while True:
if cls.ddgs._chat_vqd:
break
await asyncio.sleep(1)
await page.close()
finally:
stop_browser()

View file

@ -6,4 +6,4 @@ class FreeRouter(OpenaiTemplate):
label = "CablyAI FreeRouter" label = "CablyAI FreeRouter"
url = "https://freerouter.cablyai.com" url = "https://freerouter.cablyai.com"
api_base = "https://freerouter.cablyai.com/v1" api_base = "https://freerouter.cablyai.com/v1"
working = False working = True

View file

@ -52,8 +52,8 @@ class PollinationsAI(AsyncGeneratorProvider, ProviderModelMixin):
audio_models = [default_audio_model] audio_models = [default_audio_model]
extra_image_models = ["flux-pro", "flux-dev", "flux-schnell", "midjourney", "dall-e-3", "turbo"] extra_image_models = ["flux-pro", "flux-dev", "flux-schnell", "midjourney", "dall-e-3", "turbo"]
vision_models = [default_vision_model, "gpt-4o-mini", "openai", "openai-large", "searchgpt"] vision_models = [default_vision_model, "gpt-4o-mini", "openai", "openai-large", "searchgpt"]
extra_text_models = vision_models
_models_loaded = False _models_loaded = False
# https://github.com/pollinations/pollinations/blob/master/text.pollinations.ai/generateTextPortkey.js#L15
model_aliases = { model_aliases = {
### Text Models ### ### Text Models ###
"gpt-4o-mini": "openai", "gpt-4o-mini": "openai",
@ -100,42 +100,31 @@ class PollinationsAI(AsyncGeneratorProvider, ProviderModelMixin):
cls.image_models = all_image_models cls.image_models = all_image_models
# Update of text models
text_response = requests.get("https://text.pollinations.ai/models") text_response = requests.get("https://text.pollinations.ai/models")
text_response.raise_for_status() text_response.raise_for_status()
models = text_response.json() models = text_response.json()
# Purpose of text models
cls.text_models = [
model.get("name")
for model in models
if "input_modalities" in model and "text" in model["input_modalities"]
]
# Purpose of audio models # Purpose of audio models
cls.audio_models = { cls.audio_models = {
model.get("name"): model.get("voices") model.get("name"): model.get("voices")
for model in models for model in models
if model.get("audio") if "output_modalities" in model and "audio" in model["output_modalities"]
} }
# Create a set of unique text models starting with default model # Create a set of unique text models starting with default model
unique_text_models = {cls.default_model} unique_text_models = cls.text_models.copy()
# Add models from vision_models # Add models from vision_models
unique_text_models.update(cls.vision_models) unique_text_models.extend(cls.vision_models)
# Add models from the API response # Add models from the API response
for model in models: for model in models:
model_name = model.get("name") model_name = model.get("name")
if model_name and "input_modalities" in model and "text" in model["input_modalities"]: if model_name and "input_modalities" in model and "text" in model["input_modalities"]:
unique_text_models.add(model_name) unique_text_models.append(model_name)
# Convert to list and update text_models # Convert to list and update text_models
cls.text_models = list(unique_text_models) cls.text_models = list(dict.fromkeys(unique_text_models))
# Update extra_text_models with unique vision models
cls.extra_text_models = [model for model in cls.vision_models if model != cls.default_model]
cls._models_loaded = True cls._models_loaded = True
@ -148,12 +137,10 @@ class PollinationsAI(AsyncGeneratorProvider, ProviderModelMixin):
debug.error(f"Failed to fetch models: {e}") debug.error(f"Failed to fetch models: {e}")
# Return unique models across all categories # Return unique models across all categories
all_models = set(cls.text_models) all_models = cls.text_models.copy()
all_models.update(cls.image_models) all_models.extend(cls.image_models)
all_models.update(cls.audio_models.keys()) all_models.extend(cls.audio_models.keys())
result = list(all_models) return list(dict.fromkeys(all_models))
return result
@classmethod @classmethod
async def create_async_generator( async def create_async_generator(
@ -265,15 +252,15 @@ class PollinationsAI(AsyncGeneratorProvider, ProviderModelMixin):
query = "&".join(f"{k}={quote_plus(str(v))}" for k, v in params.items() if v is not None) query = "&".join(f"{k}={quote_plus(str(v))}" for k, v in params.items() if v is not None)
prompt = quote_plus(prompt)[:2048-256-len(query)] prompt = quote_plus(prompt)[:2048-256-len(query)]
url = f"{cls.image_api_endpoint}prompt/{prompt}?{query}" url = f"{cls.image_api_endpoint}prompt/{prompt}?{query}"
def get_image_url(i: int = 0, seed: Optional[int] = None): def get_image_url(i: int, seed: Optional[int] = None):
if i == 0: if i == 1:
if not cache and seed is None: if not cache and seed is None:
seed = random.randint(0, 2**32) seed = random.randint(0, 2**32)
else: else:
seed = random.randint(0, 2**32) seed = random.randint(0, 2**32)
return f"{url}&seed={seed}" if seed else url return f"{url}&seed={seed}" if seed else url
async with ClientSession(headers=DEFAULT_HEADERS, connector=get_connector(proxy=proxy)) as session: async with ClientSession(headers=DEFAULT_HEADERS, connector=get_connector(proxy=proxy)) as session:
async def get_image(i: int = 0, seed: Optional[int] = None): async def get_image(i: int, seed: Optional[int] = None):
async with session.get(get_image_url(i, seed), allow_redirects=False) as response: async with session.get(get_image_url(i, seed), allow_redirects=False) as response:
try: try:
await raise_for_status(response) await raise_for_status(response)
@ -343,6 +330,8 @@ class PollinationsAI(AsyncGeneratorProvider, ProviderModelMixin):
if line[6:].startswith(b"[DONE]"): if line[6:].startswith(b"[DONE]"):
break break
result = json.loads(line[6:]) result = json.loads(line[6:])
if "error" in result:
raise ResponseError(result["error"].get("message", result["error"]))
if "usage" in result: if "usage" in result:
yield Usage(**result["usage"]) yield Usage(**result["usage"])
choices = result.get("choices", [{}]) choices = result.get("choices", [{}])

View file

@ -0,0 +1,253 @@
from __future__ import annotations
import json
import uuid
import asyncio
from ...typing import AsyncResult, Messages
from ...requests import StreamSession, raise_for_status
from ...providers.response import FinishReason
from ..base_provider import AsyncGeneratorProvider, ProviderModelMixin, AuthFileMixin
from ..helper import format_prompt
from ... import debug
class LMArenaProvider(AsyncGeneratorProvider, ProviderModelMixin, AuthFileMixin):
label = "LM Arena"
url = "https://lmarena.ai"
api_endpoint = "/queue/join?"
working = True
default_model = "chatgpt-4o-latest-20250326"
model_aliases = {"gpt-4o": default_model}
models = [
default_model,
"gpt-4.1-2025-04-14",
"gemini-2.5-pro-exp-03-25",
"llama-4-maverick-03-26-experimental",
"grok-3-preview-02-24",
"claude-3-7-sonnet-20250219",
"claude-3-7-sonnet-20250219-thinking-32k",
"deepseek-v3-0324",
"llama-4-maverick-17b-128e-instruct",
"gpt-4.1-mini-2025-04-14",
"gpt-4.1-nano-2025-04-14",
"gemini-2.0-flash-thinking-exp-01-21",
"gemini-2.0-flash-001",
"gemini-2.0-flash-lite-preview-02-05",
"gemma-3-27b-it",
"gemma-3-12b-it",
"gemma-3-4b-it",
"deepseek-r1",
"claude-3-5-sonnet-20241022",
"o3-mini",
"llama-3.3-70b-instruct",
"gpt-4o-mini-2024-07-18",
"gpt-4o-2024-11-20",
"gpt-4o-2024-08-06",
"gpt-4o-2024-05-13",
"command-a-03-2025",
"qwq-32b",
"p2l-router-7b",
"claude-3-5-haiku-20241022",
"claude-3-5-sonnet-20240620",
"doubao-1.5-pro-32k-250115",
"doubao-1.5-vision-pro-32k-250115",
"mistral-small-24b-instruct-2501",
"phi-4",
"amazon-nova-pro-v1.0",
"amazon-nova-lite-v1.0",
"amazon-nova-micro-v1.0",
"cobalt-exp-beta-v3",
"cobalt-exp-beta-v4",
"qwen-max-2025-01-25",
"qwen-plus-0125-exp",
"qwen2.5-vl-32b-instruct",
"qwen2.5-vl-72b-instruct",
"gemini-1.5-pro-002",
"gemini-1.5-flash-002",
"gemini-1.5-flash-8b-001",
"gemini-1.5-pro-001",
"gemini-1.5-flash-001",
"llama-3.1-405b-instruct-bf16",
"llama-3.3-nemotron-49b-super-v1",
"llama-3.1-nemotron-ultra-253b-v1",
"llama-3.1-nemotron-70b-instruct",
"llama-3.1-70b-instruct",
"llama-3.1-8b-instruct",
"hunyuan-standard-2025-02-10",
"hunyuan-large-2025-02-10",
"hunyuan-standard-vision-2024-12-31",
"hunyuan-turbo-0110",
"hunyuan-turbos-20250226",
"mistral-large-2411",
"pixtral-large-2411",
"mistral-large-2407",
"llama-3.1-nemotron-51b-instruct",
"granite-3.1-8b-instruct",
"granite-3.1-2b-instruct",
"step-2-16k-exp-202412",
"step-2-16k-202502",
"step-1o-vision-32k-highres",
"yi-lightning",
"glm-4-plus",
"glm-4-plus-0111",
"jamba-1.5-large",
"jamba-1.5-mini",
"gemma-2-27b-it",
"gemma-2-9b-it",
"gemma-2-2b-it",
"eureka-chatbot",
"claude-3-haiku-20240307",
"claude-3-sonnet-20240229",
"claude-3-opus-20240229",
"nemotron-4-340b",
"llama-3-70b-instruct",
"llama-3-8b-instruct",
"qwen2.5-plus-1127",
"qwen2.5-coder-32b-instruct",
"qwen2.5-72b-instruct",
"qwen-max-0919",
"qwen-vl-max-1119",
"qwen-vl-max-0809",
"llama-3.1-tulu-3-70b",
"olmo-2-0325-32b-instruct",
"gpt-3.5-turbo-0125",
"reka-core-20240904",
"reka-flash-20240904",
"c4ai-aya-expanse-32b",
"c4ai-aya-expanse-8b",
"c4ai-aya-vision-32b",
"command-r-plus-08-2024",
"command-r-08-2024",
"codestral-2405",
"mixtral-8x22b-instruct-v0.1",
"mixtral-8x7b-instruct-v0.1",
"pixtral-12b-2409",
"ministral-8b-2410"]
_args: dict = None
@staticmethod
def _random_session_hash():
return str(uuid.uuid4())
@classmethod
def _build_payloads(cls, model_id: str, session_hash: str, messages: Messages, max_tokens: int, temperature: float, top_p: float):
first_payload = {
"data": [
None,
model_id,
{"text": format_prompt(messages), "files": []},
{
"text_models": [model_id],
"all_text_models": [model_id],
"vision_models": [],
"all_vision_models": [],
"image_gen_models": [],
"all_image_gen_models": [],
"search_models": [],
"all_search_models": [],
"models": [model_id],
"all_models": [model_id],
"arena_type": "text-arena"
}
],
"event_data": None,
"fn_index": 117,
"trigger_id": 159,
"session_hash": session_hash
}
second_payload = {
"data": [],
"event_data": None,
"fn_index": 118,
"trigger_id": 159,
"session_hash": session_hash
}
third_payload = {
"data": [None, temperature, top_p, max_tokens],
"event_data": None,
"fn_index": 119,
"trigger_id": 159,
"session_hash": session_hash
}
return first_payload, second_payload, third_payload
@classmethod
async def create_async_generator(
cls, model: str, messages: Messages,
max_tokens: int = 2048,
temperature: float = 0.7,
top_p: float = 1,
proxy: str = None,
**kwargs
) -> AsyncResult:
if not model:
model = cls.default_model
if model in cls.model_aliases:
model = cls.model_aliases[model]
session_hash = cls._random_session_hash()
headers = {
"Content-Type": "application/json",
"Accept": "application/json"
}
async with StreamSession(impersonate="chrome", headers=headers) as session:
first_payload, second_payload, third_payload = cls._build_payloads(model, session_hash, messages, max_tokens, temperature, top_p)
# Long stream GET
async def long_stream():
# POST 1
async with session.post(f"{cls.url}{cls.api_endpoint}", json=first_payload, proxy=proxy) as response:
await raise_for_status(response)
# POST 2
async with session.post(f"{cls.url}{cls.api_endpoint}", json=second_payload, proxy=proxy) as response:
await raise_for_status(response)
# POST 3
async with session.post(f"{cls.url}{cls.api_endpoint}", json=third_payload, proxy=proxy) as response:
await raise_for_status(response)
stream_url = f"{cls.url}/queue/data?session_hash={session_hash}"
async with session.get(stream_url, headers={"Accept": "text/event-stream"}, proxy=proxy) as response:
await raise_for_status(response)
text_position = 0
count = 0
async for line in response.iter_lines():
if line.startswith(b"data: "):
try:
msg = json.loads(line[6:])
except Exception as e:
raise RuntimeError(f"Failed to decode JSON from stream: {line}", e)
if msg.get("msg") == "process_generating":
data = msg["output"]["data"][1]
if data:
data = data[0]
if len(data) > 2:
if isinstance(data[2], list):
data[2] = data[2][-1]
content = data[2][text_position:]
if content.endswith(""):
content = content[:-2]
if content:
count += 1
yield count, content
text_position += len(content)
elif msg.get("msg") == "close_stream":
break
elif msg.get("msg") not in ("process_completed", "process_starts", "estimation"):
debug.log(f"Unexpected message: {msg}")
count = 0
async for count, chunk in long_stream():
yield chunk
if count == 0:
await asyncio.sleep(10)
async for count, chunk in long_stream():
yield chunk
if count == 0:
raise RuntimeError("No response from server.")
if count == max_tokens:
yield FinishReason("length")

View file

@ -10,7 +10,7 @@ from .BlackForestLabs_Flux1Dev import BlackForestLabs_Flux1Dev
from .BlackForestLabs_Flux1Schnell import BlackForestLabs_Flux1Schnell from .BlackForestLabs_Flux1Schnell import BlackForestLabs_Flux1Schnell
from .CohereForAI_C4AI_Command import CohereForAI_C4AI_Command from .CohereForAI_C4AI_Command import CohereForAI_C4AI_Command
from .DeepseekAI_JanusPro7b import DeepseekAI_JanusPro7b from .DeepseekAI_JanusPro7b import DeepseekAI_JanusPro7b
from .G4F import G4F from .LMArenaProvider import LMArenaProvider
from .Microsoft_Phi_4 import Microsoft_Phi_4 from .Microsoft_Phi_4 import Microsoft_Phi_4
from .Qwen_QVQ_72B import Qwen_QVQ_72B from .Qwen_QVQ_72B import Qwen_QVQ_72B
from .Qwen_Qwen_2_5 import Qwen_Qwen_2_5 from .Qwen_Qwen_2_5 import Qwen_Qwen_2_5
@ -33,7 +33,7 @@ class HuggingSpace(AsyncGeneratorProvider, ProviderModelMixin):
BlackForestLabs_Flux1Schnell, BlackForestLabs_Flux1Schnell,
CohereForAI_C4AI_Command, CohereForAI_C4AI_Command,
DeepseekAI_JanusPro7b, DeepseekAI_JanusPro7b,
G4F, LMArenaProvider,
Microsoft_Phi_4, Microsoft_Phi_4,
Qwen_QVQ_72B, Qwen_QVQ_72B,
Qwen_Qwen_2_5, Qwen_Qwen_2_5,
@ -88,7 +88,7 @@ class HuggingSpace(AsyncGeneratorProvider, ProviderModelMixin):
for provider in cls.providers: for provider in cls.providers:
if model in provider.get_models(): if model in provider.get_models():
try: try:
async for chunk in provider.create_async_generator(model, messages, images=images, **kwargs): async for chunk in provider.create_async_generator(model, messages, media=media, **kwargs):
is_started = True is_started = True
yield chunk yield chunk
if is_started: if is_started:

View file

@ -10,10 +10,7 @@ from ...typing import AsyncResult, Messages
from ...errors import NoValidHarFileError from ...errors import NoValidHarFileError
from ... import debug from ... import debug
def cookies_to_dict(): class CopilotAccount(Copilot, AsyncAuthedProvider):
return Copilot._cookies if isinstance(Copilot._cookies, dict) else {c.name: c.value for c in Copilot._cookies}
class CopilotAccount(AsyncAuthedProvider, Copilot):
needs_auth = True needs_auth = True
use_nodriver = True use_nodriver = True
parent = "Copilot" parent = "Copilot"
@ -23,17 +20,17 @@ class CopilotAccount(AsyncAuthedProvider, Copilot):
@classmethod @classmethod
async def on_auth_async(cls, proxy: str = None, **kwargs) -> AsyncIterator: async def on_auth_async(cls, proxy: str = None, **kwargs) -> AsyncIterator:
try: try:
Copilot._access_token, Copilot._cookies = readHAR(cls.url) cls._access_token, cls._cookies = readHAR(cls.url)
except NoValidHarFileError as h: except NoValidHarFileError as h:
debug.log(f"Copilot: {h}") debug.log(f"Copilot: {h}")
if has_nodriver: if has_nodriver:
yield RequestLogin(cls.label, os.environ.get("G4F_LOGIN_URL", "")) yield RequestLogin(cls.label, os.environ.get("G4F_LOGIN_URL", ""))
Copilot._access_token, Copilot._cookies = await get_access_token_and_cookies(cls.url, proxy) cls._access_token, cls._cookies = await get_access_token_and_cookies(cls.url, proxy)
else: else:
raise h raise h
yield AuthResult( yield AuthResult(
api_key=Copilot._access_token, api_key=cls._access_token,
cookies=cookies_to_dict() cookies=cls.cookies_to_dict()
) )
@classmethod @classmethod
@ -44,9 +41,12 @@ class CopilotAccount(AsyncAuthedProvider, Copilot):
auth_result: AuthResult, auth_result: AuthResult,
**kwargs **kwargs
) -> AsyncResult: ) -> AsyncResult:
Copilot._access_token = getattr(auth_result, "api_key") cls._access_token = getattr(auth_result, "api_key")
Copilot._cookies = getattr(auth_result, "cookies") cls._cookies = getattr(auth_result, "cookies")
Copilot.needs_auth = cls.needs_auth async for chunk in cls.create_async_generator(model, messages, **kwargs):
for chunk in Copilot.create_completion(model, messages, **kwargs):
yield chunk yield chunk
auth_result.cookies = cookies_to_dict() auth_result.cookies = cls.cookies_to_dict()
@classmethod
def cookies_to_dict(cls):
return cls._cookies if isinstance(cls._cookies, dict) else {c.name: c.value for c in cls._cookies}

View file

@ -146,7 +146,7 @@ async def get_access_token_and_user_agent(url: str, proxy: str = None):
browser, stop_browser = await get_nodriver(proxy=proxy, user_data_dir="designer") browser, stop_browser = await get_nodriver(proxy=proxy, user_data_dir="designer")
try: try:
page = await browser.get(url) page = await browser.get(url)
user_agent = await page.evaluate("navigator.userAgent") user_agent = await page.evaluate("navigator.userAgent", return_by_value=True)
access_token = None access_token = None
while access_token is None: while access_token is None:
access_token = await page.evaluate(""" access_token = await page.evaluate("""

View file

@ -278,7 +278,7 @@ class OpenaiChat(AsyncAuthedProvider, ProviderModelMixin):
messages: Messages, messages: Messages,
auth_result: AuthResult, auth_result: AuthResult,
proxy: str = None, proxy: str = None,
timeout: int = 180, timeout: int = 360,
auto_continue: bool = False, auto_continue: bool = False,
action: str = "next", action: str = "next",
conversation: Conversation = None, conversation: Conversation = None,
@ -447,7 +447,7 @@ class OpenaiChat(AsyncAuthedProvider, ProviderModelMixin):
link = sources.list[int(match.group(1))]["url"] link = sources.list[int(match.group(1))]["url"]
return f"[[{int(match.group(1))+1}]]({link})" return f"[[{int(match.group(1))+1}]]({link})"
return f" [{int(match.group(1))+1}]" return f" [{int(match.group(1))+1}]"
buffer = re.sub(r'(?:cite\nturn0search|cite\nturn0news|turn0news)(\d+)', replacer, buffer) buffer = re.sub(r'(?:cite\nturn[0-9]+|turn[0-9]+)(?:search|news|view)(\d+)', replacer, buffer)
else: else:
continue continue
yield buffer yield buffer
@ -489,25 +489,39 @@ class OpenaiChat(AsyncAuthedProvider, ProviderModelMixin):
if "type" in line: if "type" in line:
if line["type"] == "title_generation": if line["type"] == "title_generation":
yield TitleGeneration(line["title"]) yield TitleGeneration(line["title"])
fields.p = line.get("p", fields.p)
if fields.p.startswith("/message/content/thoughts"):
if fields.p.endswith("/content"):
if fields.thoughts_summary:
yield Reasoning(token="", status=fields.thoughts_summary)
fields.thoughts_summary = ""
yield Reasoning(token=line.get("v"))
elif fields.p.endswith("/summary"):
fields.thoughts_summary += line.get("v")
return
if "v" in line: if "v" in line:
v = line.get("v") v = line.get("v")
if isinstance(v, str) and fields.is_recipient: if isinstance(v, str) and fields.recipient == "all":
if "p" not in line or line.get("p") == "/message/content/parts/0": if "p" not in line or line.get("p") == "/message/content/parts/0":
yield Reasoning(token=v) if fields.is_thinking else v yield Reasoning(token=v) if fields.is_thinking else v
elif isinstance(v, list): elif isinstance(v, list):
for m in v: for m in v:
if m.get("p") == "/message/content/parts/0" and fields.is_recipient: if m.get("p") == "/message/content/parts/0" and fields.recipient == "all":
yield m.get("v") yield m.get("v")
elif m.get("p") == "/message/metadata/search_result_groups": elif m.get("p") == "/message/metadata/search_result_groups":
for entry in [p.get("entries") for p in m.get("v")]: for entry in [p.get("entries") for p in m.get("v")]:
for link in entry: for link in entry:
sources.add_source(link) sources.add_source(link)
elif re.match(r"^/message/metadata/content_references/\d+$", m.get("p")): elif m.get("p") == "/message/metadata/content_references":
for entry in m.get("v"):
for link in entry.get("sources", []):
sources.add_source(link)
elif m.get("p") and re.match(r"^/message/metadata/content_references/\d+$", m.get("p")):
sources.add_source(m.get("v")) sources.add_source(m.get("v"))
elif m.get("p") == "/message/metadata/finished_text": elif m.get("p") == "/message/metadata/finished_text":
fields.is_thinking = False fields.is_thinking = False
yield Reasoning(status=m.get("v")) yield Reasoning(status=m.get("v"))
elif m.get("p") == "/message/metadata": elif m.get("p") == "/message/metadata" and fields.recipient == "all":
fields.finish_reason = m.get("v", {}).get("finish_details", {}).get("type") fields.finish_reason = m.get("v", {}).get("finish_details", {}).get("type")
break break
elif isinstance(v, dict): elif isinstance(v, dict):
@ -515,8 +529,8 @@ class OpenaiChat(AsyncAuthedProvider, ProviderModelMixin):
fields.conversation_id = v.get("conversation_id") fields.conversation_id = v.get("conversation_id")
debug.log(f"OpenaiChat: New conversation: {fields.conversation_id}") debug.log(f"OpenaiChat: New conversation: {fields.conversation_id}")
m = v.get("message", {}) m = v.get("message", {})
fields.is_recipient = m.get("recipient", "all") == "all" fields.recipient = m.get("recipient", fields.recipient)
if fields.is_recipient: if fields.recipient == "all":
c = m.get("content", {}) c = m.get("content", {})
if c.get("content_type") == "text" and m.get("author", {}).get("role") == "tool" and "initial_text" in m.get("metadata", {}): if c.get("content_type") == "text" and m.get("author", {}).get("role") == "tool" and "initial_text" in m.get("metadata", {}):
fields.is_thinking = True fields.is_thinking = True
@ -578,14 +592,17 @@ class OpenaiChat(AsyncAuthedProvider, ProviderModelMixin):
cls._set_api_key(api_key) cls._set_api_key(api_key)
else: else:
try: try:
await get_request_config(cls.request_config, proxy) cls.request_config = await get_request_config(cls.request_config, proxy)
if cls.request_config is None:
cls.request_config = RequestConfig()
cls._create_request_args(cls.request_config.cookies, cls.request_config.headers) cls._create_request_args(cls.request_config.cookies, cls.request_config.headers)
if cls.request_config.access_token is not None or cls.needs_auth: if cls.needs_auth and cls.request_config.access_token is None:
raise NoValidHarFileError(f"Missing access token")
if not cls._set_api_key(cls.request_config.access_token): if not cls._set_api_key(cls.request_config.access_token):
raise NoValidHarFileError(f"Access token is not valid: {cls.request_config.access_token}") raise NoValidHarFileError(f"Access token is not valid: {cls.request_config.access_token}")
except NoValidHarFileError: except NoValidHarFileError:
if has_nodriver: if has_nodriver:
if cls._api_key is None: if cls.request_config.access_token is None:
yield RequestLogin(cls.label, os.environ.get("G4F_LOGIN_URL", "")) yield RequestLogin(cls.label, os.environ.get("G4F_LOGIN_URL", ""))
await cls.nodriver_auth(proxy) await cls.nodriver_auth(proxy)
else: else:
@ -622,15 +639,18 @@ class OpenaiChat(AsyncAuthedProvider, ProviderModelMixin):
await page.send(nodriver.cdp.network.enable()) await page.send(nodriver.cdp.network.enable())
page.add_handler(nodriver.cdp.network.RequestWillBeSent, on_request) page.add_handler(nodriver.cdp.network.RequestWillBeSent, on_request)
page = await browser.get(cls.url) page = await browser.get(cls.url)
user_agent = await page.evaluate("window.navigator.userAgent") user_agent = await page.evaluate("window.navigator.userAgent", return_by_value=True)
await page.select("#prompt-textarea", 240) while not await page.evaluate("document.getElementById('prompt-textarea').id"):
await page.evaluate("document.getElementById('prompt-textarea').innerText = 'Hello'") await asyncio.sleep(1)
await page.select("[data-testid=\"send-button\"]", 30) while not await page.evaluate("document.querySelector('[data-testid=\"send-button\"]').type"):
await asyncio.sleep(1)
await page.evaluate("document.querySelector('[data-testid=\"send-button\"]').click()") await page.evaluate("document.querySelector('[data-testid=\"send-button\"]').click()")
while True: while True:
body = await page.evaluate("JSON.stringify(window.__remixContext)") body = await page.evaluate("JSON.stringify(window.__remixContext)", return_by_value=True)
if hasattr(body, "value"):
body = body.value
if body: if body:
match = re.search(r'"accessToken":"(.*?)"', body) match = re.search(r'"accessToken":"(.+?)"', body)
if match: if match:
cls._api_key = match.group(1) cls._api_key = match.group(1)
break break
@ -674,6 +694,7 @@ class OpenaiChat(AsyncAuthedProvider, ProviderModelMixin):
@classmethod @classmethod
def _set_api_key(cls, api_key: str): def _set_api_key(cls, api_key: str):
cls._api_key = api_key
if api_key: if api_key:
exp = api_key.split(".")[1] exp = api_key.split(".")[1]
exp = (exp + "=" * (4 - len(exp) % 4)).encode() exp = (exp + "=" * (4 - len(exp) % 4)).encode()
@ -681,11 +702,11 @@ class OpenaiChat(AsyncAuthedProvider, ProviderModelMixin):
debug.log(f"OpenaiChat: API key expires at\n {cls._expires} we have:\n {time.time()}") debug.log(f"OpenaiChat: API key expires at\n {cls._expires} we have:\n {time.time()}")
if time.time() > cls._expires: if time.time() > cls._expires:
debug.log(f"OpenaiChat: API key is expired") debug.log(f"OpenaiChat: API key is expired")
return False
else: else:
cls._api_key = api_key
cls._headers["authorization"] = f"Bearer {api_key}" cls._headers["authorization"] = f"Bearer {api_key}"
return True return True
return False return True
@classmethod @classmethod
def _update_cookie_header(cls): def _update_cookie_header(cls):
@ -700,10 +721,12 @@ class Conversation(JsonConversation):
self.conversation_id = conversation_id self.conversation_id = conversation_id
self.message_id = message_id self.message_id = message_id
self.finish_reason = finish_reason self.finish_reason = finish_reason
self.is_recipient = False self.recipient = "all"
self.parent_message_id = message_id if parent_message_id is None else parent_message_id self.parent_message_id = message_id if parent_message_id is None else parent_message_id
self.user_id = user_id self.user_id = user_id
self.is_thinking = is_thinking self.is_thinking = is_thinking
self.p = None
self.thoughts_summary = ""
def get_cookies( def get_cookies(
urls: Optional[Iterator[str]] = None urls: Optional[Iterator[str]] = None

View file

@ -49,6 +49,7 @@ def get_har_files():
for file in files: for file in files:
if file.endswith(".har"): if file.endswith(".har"):
harPath.append(os.path.join(root, file)) harPath.append(os.path.join(root, file))
break
if not harPath: if not harPath:
raise NoValidHarFileError("No .har file found") raise NoValidHarFileError("No .har file found")
harPath.sort(key=lambda x: os.path.getmtime(x)) harPath.sort(key=lambda x: os.path.getmtime(x))
@ -86,8 +87,6 @@ def readHAR(request_config: RequestConfig):
request_config.cookies = {c['name']: c['value'] for c in v['request']['cookies']} request_config.cookies = {c['name']: c['value'] for c in v['request']['cookies']}
except Exception as e: except Exception as e:
debug.log(f"Error on read headers: {e}") debug.log(f"Error on read headers: {e}")
if request_config.proof_token is None:
raise NoValidHarFileError("No proof_token found in .har files")
def get_headers(entry) -> dict: def get_headers(entry) -> dict:
return {h['name'].lower(): h['value'] for h in entry['request']['headers'] if h['name'].lower() not in ['content-length', 'cookie'] and not h['name'].startswith(':')} return {h['name'].lower(): h['value'] for h in entry['request']['headers'] if h['name'].lower() not in ['content-length', 'cookie'] and not h['name'].startswith(':')}
@ -152,8 +151,9 @@ def getN() -> str:
return base64.b64encode(timestamp.encode()).decode() return base64.b64encode(timestamp.encode()).decode()
async def get_request_config(request_config: RequestConfig, proxy: str) -> RequestConfig: async def get_request_config(request_config: RequestConfig, proxy: str) -> RequestConfig:
if request_config.proof_token is None:
readHAR(request_config) readHAR(request_config)
if request_config.arkose_request is not None: if request_config.arkose_request is not None:
request_config.arkose_token = await sendRequest(genArkReq(request_config.arkose_request), proxy) request_config.arkose_token = await sendRequest(genArkReq(request_config.arkose_request), proxy)
if request_config.proof_token is None:
raise NoValidHarFileError("No proof_token found in .har files")
return request_config return request_config

View file

@ -309,9 +309,9 @@ class Api:
if credentials is not None and credentials.credentials != "secret": if credentials is not None and credentials.credentials != "secret":
config.api_key = credentials.credentials config.api_key = credentials.credentials
conversation = None conversation = config.conversation
return_conversation = config.return_conversation return_conversation = config.return_conversation
if conversation is not None: if conversation:
conversation = JsonConversation(**conversation) conversation = JsonConversation(**conversation)
return_conversation = True return_conversation = True
elif config.conversation_id is not None and config.provider is not None: elif config.conversation_id is not None and config.provider is not None:

View file

@ -217,7 +217,7 @@ async def async_iter_response(
if stream: if stream:
chat_completion = ChatCompletionChunk.model_construct( chat_completion = ChatCompletionChunk.model_construct(
None, finish_reason, completion_id, int(time.time()), usage=usage None, finish_reason, completion_id, int(time.time()), usage=usage, conversation=conversation
) )
else: else:
if response_format is not None and "type" in response_format: if response_format is not None and "type" in response_format:
@ -228,7 +228,7 @@ async def async_iter_response(
**filter_none( **filter_none(
tool_calls=[ToolCallModel.model_construct(**tool_call) for tool_call in tool_calls] tool_calls=[ToolCallModel.model_construct(**tool_call) for tool_call in tool_calls]
) if tool_calls is not None else {}, ) if tool_calls is not None else {},
conversation=None if conversation is None else conversation.get_dict() conversation=conversation
) )
if provider is not None: if provider is not None:
chat_completion.provider = provider.name chat_completion.provider = provider.name

View file

@ -10,7 +10,7 @@ from ..client.helper import filter_markdown
from .helper import filter_none from .helper import filter_none
try: try:
from pydantic import BaseModel from pydantic import BaseModel, field_serializer
except ImportError: except ImportError:
class BaseModel(): class BaseModel():
@classmethod @classmethod
@ -19,6 +19,11 @@ except ImportError:
for key, value in data.items(): for key, value in data.items():
setattr(new, key, value) setattr(new, key, value)
return new return new
class field_serializer():
def __init__(self, field_name):
self.field_name = field_name
def __call__(self, *args, **kwargs):
return args[0]
class BaseModel(BaseModel): class BaseModel(BaseModel):
@classmethod @classmethod
@ -72,6 +77,7 @@ class ChatCompletionChunk(BaseModel):
provider: Optional[str] provider: Optional[str]
choices: List[ChatCompletionDeltaChoice] choices: List[ChatCompletionDeltaChoice]
usage: UsageModel usage: UsageModel
conversation: dict
@classmethod @classmethod
def model_construct( def model_construct(
@ -80,7 +86,8 @@ class ChatCompletionChunk(BaseModel):
finish_reason: str, finish_reason: str,
completion_id: str = None, completion_id: str = None,
created: int = None, created: int = None,
usage: UsageModel = None usage: UsageModel = None,
conversation: dict = None
): ):
return super().model_construct( return super().model_construct(
id=f"chatcmpl-{completion_id}" if completion_id else None, id=f"chatcmpl-{completion_id}" if completion_id else None,
@ -92,9 +99,15 @@ class ChatCompletionChunk(BaseModel):
ChatCompletionDelta.model_construct(content), ChatCompletionDelta.model_construct(content),
finish_reason finish_reason
)], )],
**filter_none(usage=usage) **filter_none(usage=usage, conversation=conversation)
) )
@field_serializer('conversation')
def serialize_conversation(self, conversation: dict):
if hasattr(conversation, "get_dict"):
return conversation.get_dict()
return conversation
class ChatCompletionMessage(BaseModel): class ChatCompletionMessage(BaseModel):
role: str role: str
content: str content: str
@ -104,6 +117,10 @@ class ChatCompletionMessage(BaseModel):
def model_construct(cls, content: str, tool_calls: list = None): def model_construct(cls, content: str, tool_calls: list = None):
return super().model_construct(role="assistant", content=content, **filter_none(tool_calls=tool_calls)) return super().model_construct(role="assistant", content=content, **filter_none(tool_calls=tool_calls))
@field_serializer('content')
def serialize_content(self, content: str):
return str(content)
def save(self, filepath: str, allowd_types = None): def save(self, filepath: str, allowd_types = None):
if hasattr(self.content, "data"): if hasattr(self.content, "data"):
os.rename(self.content.data.replace("/media", images_dir), filepath) os.rename(self.content.data.replace("/media", images_dir), filepath)
@ -160,6 +177,12 @@ class ChatCompletion(BaseModel):
**filter_none(usage=usage, conversation=conversation) **filter_none(usage=usage, conversation=conversation)
) )
@field_serializer('conversation')
def serialize_conversation(self, conversation: dict):
if hasattr(conversation, "get_dict"):
return conversation.get_dict()
return conversation
class ChatCompletionDelta(BaseModel): class ChatCompletionDelta(BaseModel):
role: str role: str
content: str content: str
@ -168,6 +191,10 @@ class ChatCompletionDelta(BaseModel):
def model_construct(cls, content: Optional[str]): def model_construct(cls, content: Optional[str]):
return super().model_construct(role="assistant", content=content) return super().model_construct(role="assistant", content=content)
@field_serializer('content')
def serialize_content(self, content: str):
return str(content)
class ChatCompletionDeltaChoice(BaseModel): class ChatCompletionDeltaChoice(BaseModel):
index: int index: int
delta: ChatCompletionDelta delta: ChatCompletionDelta

View file

@ -56,12 +56,11 @@ DOMAINS = [
".google.com", ".google.com",
"www.whiterabbitneo.com", "www.whiterabbitneo.com",
"huggingface.co", "huggingface.co",
".huggingface.co"
"chat.reka.ai", "chat.reka.ai",
"chatgpt.com", "chatgpt.com",
".cerebras.ai", ".cerebras.ai",
"github.com", "github.com",
"huggingface.co",
".huggingface.co"
] ]
if has_browser_cookie3 and os.environ.get('DBUS_SESSION_BUS_ADDRESS') == "/dev/null": if has_browser_cookie3 and os.environ.get('DBUS_SESSION_BUS_ADDRESS') == "/dev/null":
@ -152,6 +151,7 @@ def read_cookie_files(dirPath: str = None):
harFiles.append(os.path.join(root, file)) harFiles.append(os.path.join(root, file))
elif file.endswith(".json"): elif file.endswith(".json"):
cookieFiles.append(os.path.join(root, file)) cookieFiles.append(os.path.join(root, file))
break
CookiesConfig.cookies = {} CookiesConfig.cookies = {}
for path in harFiles: for path in harFiles:

View file

@ -169,15 +169,15 @@
if (errorVideo < 3 || !refreshOnHide) { if (errorVideo < 3 || !refreshOnHide) {
return; return;
} }
if (skipRefresh > 0) {
skipRefresh -= 1;
return;
}
if (errorImage < 3) { if (errorImage < 3) {
imageFeed.src = "/search/image+g4f?skip=" + skipImage; imageFeed.src = "/search/image+g4f?skip=" + skipImage;
skipImage++; skipImage++;
return; return;
} }
if (skipRefresh > 0) {
skipRefresh -= 1;
return;
}
if (images.length > 0) { if (images.length > 0) {
imageFeed.classList.remove("hidden"); imageFeed.classList.remove("hidden");
imageFeed.src = images.shift(); imageFeed.src = images.shift();
@ -194,10 +194,13 @@
imageFeed.onload = () => { imageFeed.onload = () => {
imageFeed.classList.remove("hidden"); imageFeed.classList.remove("hidden");
gradient.classList.add("hidden"); gradient.classList.add("hidden");
errorImage = 0;
}; };
imageFeed.onclick = () => { imageFeed.onclick = () => {
imageFeed.src = "/search/image?random=" + Math.random(); imageFeed.src = "/search/image?random=" + Math.random();
skipRefresh = 2; if (skipRefresh < 4) {
skipRefresh += 1;
}
}; };
})(); })();
</script> </script>

View file

@ -1533,6 +1533,7 @@ form textarea {
.chat-top-panel .convo-title { .chat-top-panel .convo-title {
margin: 0 10px; margin: 0 10px;
font-size: 14px; font-size: 14px;
font-weight: bold;
text-align: center; text-align: center;
flex: 1; flex: 1;
} }
@ -1581,7 +1582,6 @@ form textarea {
} }
.chat-body { .chat-body {
flex: 1; flex: 1;
padding: 10px;
overflow-y: auto; overflow-y: auto;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -1613,11 +1613,25 @@ form textarea {
border: 1px dashed var(--conversations); border: 1px dashed var(--conversations);
box-shadow: 1px 1px 1px 0px rgba(0,0,0,0.75); box-shadow: 1px 1px 1px 0px rgba(0,0,0,0.75);
} }
.white .chat-footer .send-buttons button { .suggestions {
display: flex;
gap: 6px;
flex-wrap: wrap;
}
.suggestions .suggestion {
background: var(--blur-bg);
color: var(--colour-3);
padding: 10px;
margin: 0 2px 0 4px;
border-radius: 5px;
cursor: pointer;
border: 1px dashed var(--conversations);
}
.white .chat-footer .send-buttons button, .white .suggestions .suggestion {
border-style: solid; border-style: solid;
border-color: var(--blur-border); border-color: var(--blur-border);
} }
.chat-footer .send-buttons button:hover { .chat-footer .send-buttons button:hover, .suggestions .suggestion:hover {
border-style: solid; border-style: solid;
box-shadow: none; box-shadow: none;
background-color: var(--button-hover); background-color: var(--button-hover);

View file

@ -48,6 +48,7 @@ let wakeLock = null;
let countTokensEnabled = true; let countTokensEnabled = true;
let reloadConversation = true; let reloadConversation = true;
let privateConversation = null; let privateConversation = null;
let suggestions = null;
userInput.addEventListener("blur", () => { userInput.addEventListener("blur", () => {
document.documentElement.scrollTop = 0; document.documentElement.scrollTop = 0;
@ -119,9 +120,9 @@ function filter_message(text) {
if (Array.isArray(text)) { if (Array.isArray(text)) {
return text; return text;
} }
return text.replaceAll( return filter_message_content(text.replaceAll(
/<!-- generated images start -->[\s\S]+<!-- generated images end -->/gm, "" /<!-- generated images start -->[\s\S]+<!-- generated images end -->/gm, ""
).replace(/ \[aborted\]$/g, "").replace(/ \[error\]$/g, ""); ))
} }
function filter_message_content(text) { function filter_message_content(text) {
@ -468,7 +469,7 @@ const register_message_buttons = async () => {
el.dataset.click = true; el.dataset.click = true;
const message_el = get_message_el(el); const message_el = get_message_el(el);
el.addEventListener("click", async () => { el.addEventListener("click", async () => {
iframe.src = `/qrcode/${window.conversation_id}#${message_el.dataset.index}`; iframe.src = window.conversation_id ? `/qrcode/${window.conversation_id}#${message_el.dataset.index}` : '/qrcode';
iframe_container.classList.remove("hidden"); iframe_container.classList.remove("hidden");
}); });
}); });
@ -933,9 +934,7 @@ async function add_message_chunk(message, message_id, provider, scroll, finish_m
} else if (message.type == "login") { } else if (message.type == "login") {
update_message(content_map, message_id, markdown_render(message.login), scroll); update_message(content_map, message_id, markdown_render(message.login), scroll);
} else if (message.type == "finish") { } else if (message.type == "finish") {
if (!finish_storage[message_id]) {
finish_storage[message_id] = message.finish; finish_storage[message_id] = message.finish;
}
} else if (message.type == "usage") { } else if (message.type == "usage") {
usage_storage[message_id] = message.usage; usage_storage[message_id] = message.usage;
} else if (message.type == "reasoning") { } else if (message.type == "reasoning") {
@ -950,7 +949,7 @@ async function add_message_chunk(message, message_id, provider, scroll, finish_m
} if (message.token) { } if (message.token) {
reasoning_storage[message_id].text += message.token; reasoning_storage[message_id].text += message.token;
} }
update_message(content_map, message_id, render_reasoning(reasoning_storage[message_id]), scroll); update_message(content_map, message_id, null, scroll);
} else if (message.type == "parameters") { } else if (message.type == "parameters") {
if (!parameters_storage[provider]) { if (!parameters_storage[provider]) {
parameters_storage[provider] = {}; parameters_storage[provider] = {};
@ -958,6 +957,8 @@ async function add_message_chunk(message, message_id, provider, scroll, finish_m
Object.entries(message.parameters).forEach(([key, value]) => { Object.entries(message.parameters).forEach(([key, value]) => {
parameters_storage[provider][key] = value; parameters_storage[provider][key] = value;
}); });
} else if (message.type == "suggestions") {
suggestions = message.suggestions;
} }
} }
@ -998,6 +999,9 @@ const ask_gpt = async (message_id, message_index = -1, regenerate = false, provi
if (scroll) { if (scroll) {
await lazy_scroll_to_bottom(); await lazy_scroll_to_bottom();
} }
let suggestions_el = chatBody.querySelector('.suggestions');
suggestions_el ? suggestions_el.remove() : null;
if (countTokensEnabled) { if (countTokensEnabled) {
let count_total = chatBody.querySelector('.count_total'); let count_total = chatBody.querySelector('.count_total');
count_total ? count_total.parentElement.removeChild(count_total) : null; count_total ? count_total.parentElement.removeChild(count_total) : null;
@ -1507,7 +1511,7 @@ const load_conversation = async (conversation, scroll=true) => {
} else if (reason == "stop" && buffer.split("```").length - 1 % 2 === 1) { } else if (reason == "stop" && buffer.split("```").length - 1 % 2 === 1) {
reason = "length"; reason = "length";
} }
if (reason == "length" || reason == "max_tokens" || reason == "error") { if (reason != "stop") {
actions.push("continue") actions.push("continue")
} }
} }
@ -1578,8 +1582,23 @@ const load_conversation = async (conversation, scroll=true) => {
</div> </div>
`); `);
}); });
chatBody.innerHTML = elements.join("");
if (countTokensEnabled && window.GPTTokenizer_cl100k_base) { if (suggestions) {
const suggestions_el = document.createElement("div");
suggestions_el.classList.add("suggestions");
suggestions.forEach((suggestion)=> {
const el = document.createElement("button");
el.classList.add("suggestion");
el.innerHTML = `<span>${escapeHtml(suggestion)}</span> <i class="fa-solid fa-turn-up"></i>`;
el.onclick = async () => {
await handle_ask(true, suggestion);
}
suggestions_el.appendChild(el);
});
chatBody.appendChild(suggestions_el);
suggestions = null;
} else if (countTokensEnabled && window.GPTTokenizer_cl100k_base) {
const has_media = messages.filter((item)=>Array.isArray(item.content)).length > 0; const has_media = messages.filter((item)=>Array.isArray(item.content)).length > 0;
if (!has_media) { if (!has_media) {
const filtered = prepare_messages(messages, null, true, false); const filtered = prepare_messages(messages, null, true, false);
@ -1587,13 +1606,15 @@ const load_conversation = async (conversation, scroll=true) => {
last_model = last_model?.startsWith("gpt-3") ? "gpt-3.5-turbo" : "gpt-4" last_model = last_model?.startsWith("gpt-3") ? "gpt-3.5-turbo" : "gpt-4"
let count_total = GPTTokenizer_cl100k_base?.encodeChat(filtered, last_model).length let count_total = GPTTokenizer_cl100k_base?.encodeChat(filtered, last_model).length
if (count_total > 0) { if (count_total > 0) {
elements.push(`<div class="count_total">(${count_total} total tokens)</div>`); const count_total_el = document.createElement("div");
count_total_el.classList.add("count_total");
count_total_el.innerText = `(${count_total} total tokens)`;
chatBody.appendChild(count_total_el);
} }
} }
} }
} }
chatBody.innerHTML = elements.join("");
await register_message_buttons(); await register_message_buttons();
highlight(chatBody); highlight(chatBody);
regenerate_button.classList.remove("regenerate-hidden"); regenerate_button.classList.remove("regenerate-hidden");
@ -2079,8 +2100,10 @@ function count_words_and_tokens(text, model, completion_tokens, prompt_tokens) {
function update_message(content_map, message_id, content = null, scroll = true) { function update_message(content_map, message_id, content = null, scroll = true) {
content_map.update_timeouts.push(setTimeout(() => { content_map.update_timeouts.push(setTimeout(() => {
if (!content) { if (!content) {
if (reasoning_storage[message_id]) { if (reasoning_storage[message_id] && message_storage[message_id]) {
content = render_reasoning(reasoning_storage[message_id], true) + markdown_render(message_storage[message_id]); content = render_reasoning(reasoning_storage[message_id], true) + markdown_render(message_storage[message_id]);
} else if (reasoning_storage[message_id]) {
content = render_reasoning(reasoning_storage[message_id]);
} else { } else {
content = markdown_render(message_storage[message_id]); content = markdown_render(message_storage[message_id]);
} }
@ -2097,10 +2120,9 @@ function update_message(content_map, message_id, content = null, scroll = true)
} }
} }
if (error_storage[message_id]) { if (error_storage[message_id]) {
content_map.inner.innerHTML = message + markdown_render(`**An error occured:** ${error_storage[message_id]}`); content += markdown_render(`**An error occured:** ${error_storage[message_id]}`);
} else {
content_map.inner.innerHTML = content;
} }
content_map.inner.innerHTML = content;
if (countTokensEnabled) { if (countTokensEnabled) {
content_map.count.innerText = count_words_and_tokens( content_map.count.innerText = count_words_and_tokens(
(reasoning_storage[message_id] ? reasoning_storage[message_id].text : "") (reasoning_storage[message_id] ? reasoning_storage[message_id].text : "")
@ -2484,8 +2506,14 @@ async function on_api() {
const hide_systemPrompt = document.getElementById("hide-systemPrompt") const hide_systemPrompt = document.getElementById("hide-systemPrompt")
const slide_systemPrompt_icon = document.querySelector(".slide-header i"); const slide_systemPrompt_icon = document.querySelector(".slide-header i");
document.querySelector(".slide-header")?.addEventListener("click", () => {
const checked = slide_systemPrompt_icon.classList.contains("fa-angles-up");
chatPrompt.classList[checked ? "add": "remove"]("hidden");
slide_systemPrompt_icon.classList[checked ? "remove": "add"]("fa-angles-up");
slide_systemPrompt_icon.classList[checked ? "add": "remove"]("fa-angles-down");
});
if (hide_systemPrompt.checked) { if (hide_systemPrompt.checked) {
chatPrompt.classList.add("hidden"); slide_systemPrompt_icon.click();
} }
hide_systemPrompt.addEventListener('change', async (event) => { hide_systemPrompt.addEventListener('change', async (event) => {
if (event.target.checked) { if (event.target.checked) {
@ -2494,12 +2522,6 @@ async function on_api() {
chatPrompt.classList.remove("hidden"); chatPrompt.classList.remove("hidden");
} }
}); });
document.querySelector(".slide-header")?.addEventListener("click", () => {
const checked = slide_systemPrompt_icon.classList.contains("fa-angles-up");
chatPrompt.classList[checked ? "add": "remove"]("hidden");
slide_systemPrompt_icon.classList[checked ? "remove": "add"]("fa-angles-up");
slide_systemPrompt_icon.classList[checked ? "add": "remove"]("fa-angles-down");
});
const userInputHeight = document.getElementById("message-input-height"); const userInputHeight = document.getElementById("message-input-height");
if (userInputHeight) { if (userInputHeight) {
if (userInputHeight.value) { if (userInputHeight.value) {

View file

@ -215,6 +215,8 @@ class Api:
yield self._format_json("content", chunk.to_string()) yield self._format_json("content", chunk.to_string())
elif isinstance(chunk, AudioResponse): elif isinstance(chunk, AudioResponse):
yield self._format_json("content", str(chunk)) yield self._format_json("content", str(chunk))
elif isinstance(chunk, SuggestedFollowups):
yield self._format_json("suggestions", chunk.suggestions)
elif isinstance(chunk, DebugResponse): elif isinstance(chunk, DebugResponse):
yield self._format_json("log", chunk.log) yield self._format_json("log", chunk.log)
elif isinstance(chunk, RawResponse): elif isinstance(chunk, RawResponse):

View file

@ -341,28 +341,35 @@ class Backend_Api(Api):
return redirect(source_url) return redirect(source_url)
raise raise
self.match_files = {}
@app.route('/search/<search>', methods=['GET']) @app.route('/search/<search>', methods=['GET'])
def find_media(search: str): def find_media(search: str):
search = [secure_filename(chunk.lower()) for chunk in search.split("+")] safe_search = [secure_filename(chunk.lower()) for chunk in search.split("+")]
if not os.access(images_dir, os.R_OK): if not os.access(images_dir, os.R_OK):
return jsonify({"error": {"message": "Not found"}}), 404 return jsonify({"error": {"message": "Not found"}}), 404
match_files = {} if search not in self.match_files:
self.match_files[search] = {}
for root, _, files in os.walk(images_dir): for root, _, files in os.walk(images_dir):
for file in files: for file in files:
mime_type = is_allowed_extension(file) mime_type = is_allowed_extension(file)
if mime_type is not None: if mime_type is not None:
mime_type = secure_filename(mime_type) mime_type = secure_filename(mime_type)
for tag in search: for tag in safe_search:
if tag in mime_type: if tag in mime_type:
match_files[file] = match_files.get(file, 0) + 1 self.match_files[search][file] = self.match_files[search].get(file, 0) + 1
break break
for tag in search: for tag in safe_search:
if tag in file.lower(): if tag in file.lower():
match_files[file] = match_files.get(file, 0) + 1 self.match_files[search][file] = self.match_files[search].get(file, 0) + 1
match_files = [file for file, count in match_files.items() if count >= request.args.get("min", len(search))] break
match_files = [file for file, count in self.match_files[search].items() if count >= request.args.get("min", len(safe_search))]
if int(request.args.get("skip", 0)) >= len(match_files): if int(request.args.get("skip", 0)) >= len(match_files):
return jsonify({"error": {"message": "Not found"}}), 404 return jsonify({"error": {"message": "Not found"}}), 404
if (request.args.get("random", False)): if (request.args.get("random", False)):
seed = request.args.get("random")
if seed not in ["true", "True", "1"]:
random.seed(seed)
return redirect(f"/media/{random.choice(match_files)}"), 302 return redirect(f"/media/{random.choice(match_files)}"), 302
return redirect(f"/media/{match_files[int(request.args.get('skip', 0))]}", 302) return redirect(f"/media/{match_files[int(request.args.get('skip', 0))]}", 302)

View file

@ -28,7 +28,7 @@ def get_media_extension(media: str) -> str:
extension = os.path.splitext(path)[1] extension = os.path.splitext(path)[1]
if not extension: if not extension:
extension = os.path.splitext(media)[1] extension = os.path.splitext(media)[1]
if not extension: if not extension or len(extension) > 4:
return "" return ""
if extension[1:] not in EXTENSIONS_MAP: if extension[1:] not in EXTENSIONS_MAP:
raise ValueError(f"Unsupported media extension: {extension} in: {media}") raise ValueError(f"Unsupported media extension: {extension} in: {media}")

View file

@ -18,7 +18,6 @@ from .Provider import (
Free2GPT, Free2GPT,
FreeGpt, FreeGpt,
HuggingSpace, HuggingSpace,
G4F,
Grok, Grok,
DeepseekAI_JanusPro7b, DeepseekAI_JanusPro7b,
Glider, Glider,
@ -535,7 +534,7 @@ deepseek_r1 = Model(
janus_pro_7b = VisionModel( janus_pro_7b = VisionModel(
name = DeepseekAI_JanusPro7b.default_model, name = DeepseekAI_JanusPro7b.default_model,
base_provider = 'DeepSeek', base_provider = 'DeepSeek',
best_provider = IterListProvider([DeepseekAI_JanusPro7b, G4F]) best_provider = IterListProvider([DeepseekAI_JanusPro7b])
) )
### x.ai ### ### x.ai ###
@ -985,7 +984,7 @@ demo_models = {
llama_3_2_11b.name: [llama_3_2_11b, [HuggingChat]], llama_3_2_11b.name: [llama_3_2_11b, [HuggingChat]],
qwen_2_vl_7b.name: [qwen_2_vl_7b, [HuggingFaceAPI]], qwen_2_vl_7b.name: [qwen_2_vl_7b, [HuggingFaceAPI]],
deepseek_r1.name: [deepseek_r1, [HuggingFace, PollinationsAI]], deepseek_r1.name: [deepseek_r1, [HuggingFace, PollinationsAI]],
janus_pro_7b.name: [janus_pro_7b, [HuggingSpace, G4F]], janus_pro_7b.name: [janus_pro_7b, [HuggingSpace]],
command_r.name: [command_r, [HuggingSpace]], command_r.name: [command_r, [HuggingSpace]],
command_r_plus.name: [command_r_plus, [HuggingSpace]], command_r_plus.name: [command_r_plus, [HuggingSpace]],
command_r7b.name: [command_r7b, [HuggingSpace]], command_r7b.name: [command_r7b, [HuggingSpace]],

View file

@ -425,9 +425,14 @@ class AsyncAuthedProvider(AsyncGeneratorProvider, AuthFileMixin):
if auth_result is not None: if auth_result is not None:
cache_file.parent.mkdir(parents=True, exist_ok=True) cache_file.parent.mkdir(parents=True, exist_ok=True)
try: try:
cache_file.write_text(json.dumps(auth_result.get_dict())) def toJSON(obj):
except TypeError: if hasattr(obj, "get_dict"):
raise RuntimeError(f"Failed to save: {auth_result.get_dict()}") return obj.get_dict()
return str(obj)
with cache_file.open("w") as cache_file:
json.dump(auth_result, cache_file, default=toJSON)
except TypeError as e:
raise RuntimeError(f"Failed to save: {auth_result.get_dict()}\n{type(e).__name__}: {e}")
elif cache_file.exists(): elif cache_file.exists():
cache_file.unlink() cache_file.unlink()
@ -443,7 +448,9 @@ class AsyncAuthedProvider(AsyncGeneratorProvider, AuthFileMixin):
try: try:
if cache_file.exists(): if cache_file.exists():
with cache_file.open("r") as f: with cache_file.open("r") as f:
auth_result = AuthResult(**json.load(f)) data = f.read()
if data:
auth_result = AuthResult(**json.loads(data))
else: else:
raise MissingAuthError raise MissingAuthError
yield from to_sync_generator(cls.create_authed(model, messages, auth_result, **kwargs)) yield from to_sync_generator(cls.create_authed(model, messages, auth_result, **kwargs))

View file

@ -24,6 +24,16 @@ def to_string(value) -> str:
return "".join([to_string(v) for v in value if v.get("type", "text") == "text"]) return "".join([to_string(v) for v in value if v.get("type", "text") == "text"])
return str(value) return str(value)
def render_messages(messages: Messages) -> Iterator:
for idx, message in enumerate(messages):
if isinstance(message, dict) and isinstance(message.get("content"), list):
yield {
**message,
"content": to_string(message["content"]),
}
else:
yield message
def format_prompt(messages: Messages, add_special_tokens: bool = False, do_continue: bool = False, include_system: bool = True) -> str: def format_prompt(messages: Messages, add_special_tokens: bool = False, do_continue: bool = False, include_system: bool = True) -> str:
""" """
Format a series of messages into a single string, optionally adding special tokens. Format a series of messages into a single string, optionally adding special tokens.

View file

@ -240,6 +240,15 @@ class Sources(ResponseType):
for idx, link in enumerate(self.list) for idx, link in enumerate(self.list)
])) ]))
class SourceLink(ResponseType):
def __init__(self, title: str, url: str) -> None:
self.title = title
self.url = url
def __str__(self) -> str:
title = f"[{self.title}]"
return f" {format_link(self.url, title)}"
class YouTube(HiddenResponse): class YouTube(HiddenResponse):
def __init__(self, ids: List[str]) -> None: def __init__(self, ids: List[str]) -> None:
"""Initialize with a list of YouTube IDs.""" """Initialize with a list of YouTube IDs."""

View file

@ -103,8 +103,9 @@ async def get_args_from_nodriver(
else: else:
await browser.cookies.set_all(get_cookie_params_from_dict(cookies, url=url, domain=domain)) await browser.cookies.set_all(get_cookie_params_from_dict(cookies, url=url, domain=domain))
page = await browser.get(url) page = await browser.get(url)
user_agent = str(await page.evaluate("window.navigator.userAgent")) user_agent = await page.evaluate("window.navigator.userAgent", return_by_value=True)
await page.wait_for("body:not(.no-js)", timeout=timeout) while not await page.evaluate("document.querySelector('body:not(.no-js)')"):
await asyncio.sleep(1)
if wait_for is not None: if wait_for is not None:
await page.wait_for(wait_for, timeout=timeout) await page.wait_for(wait_for, timeout=timeout)
if callback is not None: if callback is not None:

View file

@ -44,7 +44,7 @@ async def raise_for_status_async(response: Union[StreamResponse, ClientResponse]
if response.status == 403 and is_cloudflare(message): if response.status == 403 and is_cloudflare(message):
raise CloudflareError(f"Response {response.status}: Cloudflare detected") raise CloudflareError(f"Response {response.status}: Cloudflare detected")
elif response.status == 403 and is_openai(message): elif response.status == 403 and is_openai(message):
raise ResponseStatusError(f"Response {response.status}: OpenAI Bot detected") raise MissingAuthError(f"Response {response.status}: OpenAI Bot detected")
elif response.status == 502: elif response.status == 502:
raise ResponseStatusError(f"Response {response.status}: Bad Gateway") raise ResponseStatusError(f"Response {response.status}: Bad Gateway")
elif response.status == 504: elif response.status == 504:
@ -71,7 +71,7 @@ def raise_for_status(response: Union[Response, StreamResponse, ClientResponse, R
if response.status_code == 403 and is_cloudflare(response.text): if response.status_code == 403 and is_cloudflare(response.text):
raise CloudflareError(f"Response {response.status_code}: Cloudflare detected") raise CloudflareError(f"Response {response.status_code}: Cloudflare detected")
elif response.status_code == 403 and is_openai(response.text): elif response.status_code == 403 and is_openai(response.text):
raise ResponseStatusError(f"Response {response.status_code}: OpenAI Bot detected") raise MissingAuthError(f"Response {response.status_code}: OpenAI Bot detected")
elif response.status_code == 502: elif response.status_code == 502:
raise ResponseStatusError(f"Response {response.status_code}: Bad Gateway") raise ResponseStatusError(f"Response {response.status_code}: Bad Gateway")
elif response.status_code == 504: elif response.status_code == 504: