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

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

@ -19,7 +19,7 @@ from ..cookies import get_cookies_dir
from .helper import format_image_prompt from .helper import format_image_prompt
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):
@ -690,8 +690,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

@ -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,31 +103,17 @@ 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: if return_conversation:
@ -146,28 +128,24 @@ class Copilot(AbstractProvider, ProviderModelMixin):
debug.log(f"Copilot: Use conversation: {conversation_id}") debug.log(f"Copilot: Use conversation: {conversation_id}")
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

@ -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,251 @@
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:].rstrip("")
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,

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

@ -489,6 +489,16 @@ 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.is_recipient:
@ -502,7 +512,7 @@ class OpenaiChat(AsyncAuthedProvider, ProviderModelMixin):
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") 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
@ -578,9 +588,12 @@ 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:
@ -622,15 +635,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 +690,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 +698,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):
@ -704,6 +721,8 @@ class Conversation(JsonConversation):
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

@ -86,8 +86,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 +150,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

@ -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;
@ -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") {
@ -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");
@ -2484,8 +2505,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 +2521,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

@ -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

@ -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,7 +103,7 @@ 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) await page.wait_for("body:not(.no-js)", timeout=timeout)
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)