XFEstudio/gpt4free
feat: add EasyChat and GLM providers, update HuggingFace and SSE parsing
- Added new `EasyChat` provider (`g4f/Provider/EasyChat.py`) with captcha handling, nodriver callback, and token caching - Added new `GLM` provider (`g4f/Provider/GLM.py`) with model retrieval, auth token fetch, and SSE streaming support - Updated `g4f/Provider/__init__.py` to import `EasyChat` and `GLM` - Modified `LMArenaBeta` in `g4f/Provider/needs_auth/LMArenaBeta.py` to remove nodriver availability check and always use `get_args_from_nodriver` with callback - Updated `HuggingFaceAPI` in `g4f/Provider/needs_auth/hf/HuggingFaceAPI.py` to use `default_model` from `models` instead of `default_llama_model` and removed commented `max_inputs_lenght` param - Updated `HuggingFace` in `g4f/Provider/needs_auth/hf/__init__.py` to import `default_model` instead of `default_vision_model`, set `default_model` class attribute, and commented out HuggingFaceInference and image model handling logic - Modified `OpenaiTemplate` in `g4f/Provider/template/OpenaiTemplate.py` to prefer `"name"` over `"id"` when populating `vision_models`, `models`, and `models_count` - Enhanced `sse_stream` in `g4f/requests/__init__.py` to strip and skip empty `data:` lines, handle JSON decode errors, and raise `ValueError` on invalid JSON
b9dfe1a4
代码差异
@@ -300,9 +300,10 @@ class Blackbox(AsyncGeneratorProvider, ProviderModelMixin):
async for chunk in response.content.iter_any():
if chunk:
chunk_text = chunk.decode()
full_response.append(chunk_text)
yield chunk_text
if chunk_text != "Login to continue using":
full_response.append(chunk_text)
yield chunk_text
full_response_text = ''.join(full_response)
# Handle conversation history
@@ -0,0 +1,108 @@
from __future__ import annotations
import asyncio
import json
try:
import nodriver
except ImportError:
pass
from ..typing import AsyncResult, Messages
from ..config import DEFAULT_MODEL
from ..requests import get_args_from_nodriver
from ..providers.base_provider import AuthFileMixin
from .template import OpenaiTemplate
from .. import debug
class EasyChat(OpenaiTemplate, AuthFileMixin):
url = "https://chat3.eqing.tech"
api_base = f"{url}/api/openai/v1"
api_endpoint = f"{api_base}/chat/completions"
working = True
active_by_default = True
default_model = "gpt-oss-120b-free"
model_aliases = {
DEFAULT_MODEL: default_model,
}
captchaToken: dict = None
@classmethod
async def create_async_generator(
cls,
model: str,
messages: Messages,
proxy: str = None,
extra_body: dict = None,
**kwargs
) -> AsyncResult:
args = None
auth_file = cls.get_cache_file()
if auth_file.exists():
with auth_file.open("r") as f:
args = json.load(f)
cls.captchaToken = args.pop("captchaToken")
if cls.captchaToken:
debug.log("EasyChat: Using cached captchaToken.")
def on_request(event: nodriver.cdp.network.RequestWillBeSent, page=None):
if event.request.url != cls.api_endpoint:
return
if not event.request.post_data:
return
cls.captchaToken = json.loads(event.request.post_data).get("captchaToken")
async def callback(page):
await page.send(nodriver.cdp.network.enable())
page.add_handler(nodriver.cdp.network.RequestWillBeSent, on_request)
button = await page.find("我已知晓")
if button:
await button.click()
else:
debug.error("No 'Agree' button found.")
for _ in range(3):
for _ in range(300):
modal = await page.find("Verifying...")
if not modal:
break
debug.log("EasyChaat: Waiting for captcha verification...")
if cls.captchaToken:
debug.log("EasyChat: Captcha token found, proceeding.")
break
textarea = await page.select("textarea", 180)
await textarea.send_keys("Hello")
await asyncio.sleep(1)
button = await page.select("button[class*='chat_chat-input-send']")
if button:
await button.click()
for _ in range(300):
await asyncio.sleep(1)
if cls.captchaToken:
break
await asyncio.sleep(3)
if not args:
args = await get_args_from_nodriver(cls.url, proxy=proxy, callback=callback)
if extra_body is None:
extra_body = {}
extra_body.setdefault("captchaToken", cls.captchaToken)
try:
last_chunk = None
async for chunk in super().create_async_generator(
model=model,
messages=messages,
extra_body=extra_body,
**args
):
# Remove provided by
if last_chunk == "\n" and chunk == "\n":
break
last_chunk = chunk
yield chunk
except Exception as e:
if "CLEAR-CAPTCHA-TOKEN" in str(e):
auth_file.unlink(missing_ok=True)
cls.captchaToken = None
debug.log("EasyChat: Captcha token cleared, please try again.")
raise e
with auth_file.open("w") as f:
json.dump({**args, "captchaToken": cls.captchaToken}, f)
@@ -0,0 +1,78 @@
from __future__ import annotations
import uuid
import requests
from ..typing import AsyncResult, Messages
from ..providers.response import Usage, Reasoning
from ..requests import StreamSession, raise_for_status
from .base_provider import AsyncGeneratorProvider, ProviderModelMixin
class GLM(AsyncGeneratorProvider, ProviderModelMixin):
url = "https://chat.z.ai"
api_endpoint = "https://chat.z.ai/api/chat/completions"
working = True
active_by_default = True
default_model = "GLM-4.5"
api_key = None
@classmethod
def get_models(cls, **kwargs) -> str:
if not cls.models:
response = requests.get(f"{cls.url}/api/v1/auths/")
cls.api_key = response.json().get("token")
response = requests.get(f"{cls.url}/api/models", headers={"Authorization": f"Bearer {cls.api_key}"})
data = response.json().get("data", [])
cls.model_aliases = {data.get("name"): data.get("id") for data in data}
cls.models = list(cls.model_aliases.keys())
return cls.models
@classmethod
async def create_async_generator(
cls,
model: str,
messages: Messages,
proxy: str = None,
**kwargs
) -> AsyncResult:
cls.get_models()
model = cls.get_model(model)
data = {
"chat_id": "local",
"id": str(uuid.uuid4()),
"stream": True,
"model": model,
"messages": messages,
"params": {},
"tool_servers": [],
}
async with StreamSession(
impersonate="chrome",
proxy=proxy,
) as session:
async with session.post(
cls.api_endpoint,
json=data,
headers={"Authorization": f"Bearer {cls.api_key}", "x-fe-version": "prod-fe-1.0.57"},
) as response:
await raise_for_status(response)
usage = None
async for chunk in response.sse():
if chunk.get("type") == "chat:completion":
if not usage:
usage = chunk.get("data", {}).get("usage")
if usage:
yield Usage(**usage)
if chunk.get("data", {}).get("phase") == "thinking":
delta_content = chunk.get("data", {}).get("delta_content")
delta_content = delta_content.split("</summary>\n>")[-1] if delta_content else ""
if delta_content:
yield Reasoning(delta_content)
else:
edit_content = chunk.get("data", {}).get("edit_content")
if edit_content:
yield edit_content.split("\n</details>\n")[-1]
else:
delta_content = chunk.get("data", {}).get("delta_content")
if delta_content:
yield delta_content
@@ -39,7 +39,9 @@ from .Cloudflare import Cloudflare
from .Copilot import Copilot
from .DeepInfraChat import DeepInfraChat
from .DuckDuckGo import DuckDuckGo
from .EasyChat import EasyChat
from .Free2GPT import Free2GPT
from .GLM import GLM
from .GptOss import GptOss
from .ImageLabs import ImageLabs
from .Kimi import Kimi
@@ -147,19 +147,13 @@ class LMArenaBeta(AsyncGeneratorProvider, ProviderModelMixin, AuthFileMixin):
if cache_file.exists() and cache_file.stat().st_mtime > time.time() - 60 * 30:
with cache_file.open("r") as f:
args = json.load(f)
elif has_nodriver:
try:
async def callback(page):
while not await page.evaluate('document.cookie.indexOf("arena-auth-prod-v1") >= 0'):
await asyncio.sleep(1)
while not await page.evaluate('document.querySelector(\'textarea\')'):
await asyncio.sleep(1)
args = await get_args_from_nodriver(cls.url, proxy=proxy, callback=callback)
except (RuntimeError, FileNotFoundError) as e:
debug.log(f"Nodriver is not available:", e)
args = {"headers": DEFAULT_HEADERS, "cookies": {}, "impersonate": "chrome"}
else:
args = {"headers": DEFAULT_HEADERS, "cookies": {}, "impersonate": "chrome"}
async def callback(page):
while not await page.evaluate('document.cookie.indexOf("arena-auth-prod-v1") >= 0'):
await asyncio.sleep(1)
while not await page.evaluate('document.querySelector(\'textarea\')'):
await asyncio.sleep(1)
args = await get_args_from_nodriver(cls.url, proxy=proxy, callback=callback)
# Build the JSON payload
is_image_model = model in image_models
@@ -8,7 +8,7 @@ from ....requests import StreamSession, raise_for_status
from ....errors import ModelNotFoundError, PaymentRequiredError
from ....providers.response import ProviderInfo
from ...template.OpenaiTemplate import OpenaiTemplate
from .models import model_aliases, vision_models, default_llama_model, default_vision_model, text_models
from .models import model_aliases, vision_models, default_model, default_vision_model, text_models
class HuggingFaceAPI(OpenaiTemplate):
label = "HuggingFace (Text Generation)"
@@ -18,7 +18,7 @@ class HuggingFaceAPI(OpenaiTemplate):
working = True
needs_auth = True
default_model = default_llama_model
default_model = default_model
default_vision_model = default_vision_model
vision_models = vision_models
model_aliases = model_aliases
@@ -78,7 +78,6 @@ class HuggingFaceAPI(OpenaiTemplate):
api_base: str = None,
api_key: str = None,
max_tokens: int = 2048,
# max_inputs_lenght: int = 10000,
media: MediaListType = None,
**kwargs
):
@@ -10,7 +10,7 @@ from .HuggingChat import HuggingChat
from .HuggingFaceAPI import HuggingFaceAPI
from .HuggingFaceInference import HuggingFaceInference
from .HuggingFaceMedia import HuggingFaceMedia
from .models import model_aliases, image_model_aliases, vision_models, default_vision_model
from .models import model_aliases, image_model_aliases, vision_models, default_model
from .... import debug
class HuggingFace(AsyncGeneratorProvider, ProviderModelMixin):
@@ -28,7 +28,7 @@ class HuggingFace(AsyncGeneratorProvider, ProviderModelMixin):
model_aliases = {**model_aliases, **image_model_aliases}
vision_models = vision_models
default_vision_model = default_vision_model
default_model = default_model
@classmethod
async def create_async_generator(
@@ -39,19 +39,19 @@ class HuggingFace(AsyncGeneratorProvider, ProviderModelMixin):
) -> AsyncResult:
if model in cls.model_aliases:
model = cls.model_aliases[model]
if "tools" not in kwargs and "media" not in kwargs and random.random() >= 0.5:
try:
is_started = False
async for chunk in HuggingFaceInference.create_async_generator(model, messages, **kwargs):
if isinstance(chunk, (str, ImageResponse)):
is_started = True
yield chunk
if is_started:
return
except Exception as e:
if is_started:
raise e
debug.error(f"{cls.__name__} {type(e).__name__}; {e}")
# if "tools" not in kwargs and "media" not in kwargs and random.random() >= 0.5:
# try:
# is_started = False
# async for chunk in HuggingFaceInference.create_async_generator(model, messages, **kwargs):
# if isinstance(chunk, (str, ImageResponse)):
# is_started = True
# yield chunk
# if is_started:
# return
# except Exception as e:
# if is_started:
# raise e
# debug.error(f"{cls.__name__} {type(e).__name__}; {e}")
if not cls.image_models:
cls.get_models()
try:
@@ -60,14 +60,14 @@ class HuggingFace(AsyncGeneratorProvider, ProviderModelMixin):
return
except ModelNotFoundError:
pass
if model in cls.image_models:
if "api_key" not in kwargs:
async for chunk in HuggingChat.create_async_generator(model, messages, **kwargs):
yield chunk
else:
async for chunk in HuggingFaceInference.create_async_generator(model, messages, **kwargs):
yield chunk
return
# if model in cls.image_models:
# if "api_key" not in kwargs:
# async for chunk in HuggingChat.create_async_generator(model, messages, **kwargs):
# yield chunk
# else:
# async for chunk in HuggingFaceInference.create_async_generator(model, messages, **kwargs):
# yield chunk
# return
try:
async for chunk in HuggingFaceAPI.create_async_generator(model, messages, **kwargs):
yield chunk
@@ -47,9 +47,9 @@ class OpenaiTemplate(AsyncGeneratorProvider, ProviderModelMixin, RaiseErrorMixin
data = data.get("data") if isinstance(data, dict) else data
cls.image_models = [model.get("id", model.get("name")) for model in data if model.get("image") or model.get("type") == "image"]
cls.vision_models = cls.vision_models.copy()
cls.vision_models += [model.get("id", model.get("name")) for model in data if model.get("vision")]
cls.models = [model.get("id", model.get("name")) for model in data]
cls.models_count = {model.get("id", model.get("name")): len(model.get("providers", [])) for model in data if len(model.get("providers", [])) > 1}
cls.vision_models += [model.get("name", model.get("id")) for model in data if model.get("vision")]
cls.models = [model.get("name", model.get("id")) for model in data]
cls.models_count = {model.get("name", model.get("id")): len(model.get("providers", [])) for model in data if len(model.get("providers", [])) > 1}
if cls.sort_models:
cls.models.sort()
except Exception as e:
@@ -222,9 +222,15 @@ async def sse_stream(iter_lines: Iterator[bytes]) -> AsyncIterator[dict]:
iter_lines = iter_lines.iter_lines()
async for line in iter_lines:
if line.startswith(b"data: "):
if line[6:].startswith(b"[DONE]"):
rest = line[6:].strip()
if not rest:
continue
if rest.startswith(b"[DONE]"):
break
yield json.loads(line[6:])
try:
yield json.loads(rest)
except json.JSONDecodeError:
raise ValueError(f"Invalid JSON data: {rest}")
async def iter_lines(iter_response: AsyncIterator[bytes], delimiter=None):
"""