XFEstudio/gpt4free
feat: enhance HAR provider, image handling, markdown upload & cache
- **g4f/Provider/har/__init__.py** - `get_models`/`create_async`: iterate over `(domain, harFile)` and filter with `domain in request_url` - `read_har_files` now yields `(domain, har_data)`; fixes file variable shadowing and uses `json.load` - remove stray `print`, add type hint for `find_str`, replace manual loops with `yield from` - small whitespace clean-up - **g4f/Provider/needs_auth/Grok.py** - `ImagePreview` now passes `auth_result.cookies` and `auth_result.headers` - **g4f/Provider/needs_auth/OpenaiChat.py** - add `Union` import; rename/refactor `get_generated_images` → `get_generated_image` - support `file-service://` and `sediment://` pointers; choose correct download URL - return `ImagePreview` or `ImageResponse` accordingly and stream each image part - propagate 422 errors, update prompt assignment and image handling paths - **g4f/client/__init__.py** - drop unused `ignore_working` parameter in sync/async `Completions` - normalise `media` argument: accept single tuple, infer filename when missing, fix index loop - `Images.create_variation` updated to use the new media logic - **g4f/gui/server/api.py** - expose `latest_version_cached` via `?cache=` query parameter - **g4f/gui/server/backend_api.py** - optional Markdown extraction via `MarkItDown`; save rendered text as `<file>.md` - upload flow rewrites: copy to temp file, move to bucket/media dir, clean temp, store filenames - introduce `has_markitdown` guard and improved logging/exception handling - **g4f/tools/files.py** - remove trailing spaces in HAR code-block header string - **g4f/version.py** - add `latest_version_cached` `@cached_property` for memoised version lookup
8f63f656
代码差异
@@ -17,10 +17,10 @@ class HarProvider(AsyncGeneratorProvider, ProviderModelMixin):
@classmethod
def get_models(cls):
for harFile in read_har_files():
for domain, harFile in read_har_files():
for v in harFile['log']['entries']:
request_url = v['request']['url']
if not request_url.startswith(cls.url) or "." in urlparse(request_url).path or "heartbeat" in request_url:
if domain not in request_url or "." in urlparse(request_url).path or "heartbeat" in request_url:
continue
if "\n\ndata: " not in v['response']['content']['text']:
continue
@@ -41,11 +41,11 @@ class HarProvider(AsyncGeneratorProvider, ProviderModelMixin):
session_hash = str(uuid.uuid4()).replace("-", "")
prompt = get_last_user_message(messages)
for harFile in read_har_files():
for domain, harFile in read_har_files():
async with StreamSession(impersonate="chrome") as session:
for v in harFile['log']['entries']:
request_url = v['request']['url']
if not request_url.startswith(cls.url) or "." in urlparse(request_url).path or "heartbeat" in request_url:
if domain not in request_url or "." in urlparse(request_url).path or "heartbeat" in request_url:
continue
postData = None
if "postData" in v['request']:
@@ -59,8 +59,6 @@ class HarProvider(AsyncGeneratorProvider, ProviderModelMixin):
async with getattr(session, method)(request_url, data=postData, headers=get_headers(v), proxy=proxy) as response:
await raise_for_status(response)
if "heartbeat" in request_url:
continue
returned_data = ""
async for line in response.iter_lines():
if not line.startswith(b"data: "):
@@ -83,9 +81,9 @@ def read_har_files():
for file in files:
if not file.endswith(".har"):
continue
with open(os.path.join(root, file), 'rb') as file:
with open(os.path.join(root, file), 'rb') as f:
try:
yield json.loads(file.read())
yield os.path.splitext(file)[0], json.load(f)
except json.JSONDecodeError:
raise RuntimeError(f"Failed to read HAR file: {file}")
@@ -98,7 +96,7 @@ def read_str_recusive(data):
elif isinstance(item, str):
yield item
def find_str(data, skip=0):
def find_str(data, skip: int = 0):
for item in read_str_recusive(data):
if skip > 0:
skip -= 1
@@ -110,7 +108,6 @@ def read_list_recusive(data, key):
if isinstance(data, dict):
for k, v in data.items():
if k == key:
print(k, v)
yield v
else:
yield from read_list_recusive(v, key)
@@ -123,8 +120,7 @@ def find_list(data, key):
if isinstance(item, str):
yield item
elif isinstance(item, list):
for sub_item in item:
yield sub_item
yield from item
def get_str_list(data):
for item in data:
@@ -111,7 +111,7 @@ class Grok(AsyncAuthedProvider, ProviderModelMixin):
response_data = result.get("response", {})
image = response_data.get("streamingImageGenerationResponse", None)
if image is not None:
yield ImagePreview(f'{cls.assets_url}/{image["imageUrl"]}', "", {"cookies": cookies, "headers": headers})
yield ImagePreview(f'{cls.assets_url}/{image["imageUrl"]}', "", {"cookies": auth_result.cookies, "headers": auth_result.headers})
token = response_data.get("token", result.get("token"))
is_thinking = response_data.get("isThinking", result.get("isThinking"))
if token:
@@ -8,7 +8,7 @@ import json
import base64
import time
import random
from typing import AsyncIterator, Iterator, Optional, Generator, Dict
from typing import AsyncIterator, Iterator, Optional, Generator, Dict, Union
from copy import copy
try:
@@ -254,22 +254,43 @@ class OpenaiChat(AsyncAuthedProvider, ProviderModelMixin):
return messages
@classmethod
async def get_generated_images(cls, session: StreamSession, auth_result: AuthResult, parts: list, prompt: str, conversation_id: str) -> AsyncIterator:
async def get_generated_image(cls, session: StreamSession, auth_result: AuthResult, element: Union[dict, str], prompt: str = None, conversation_id: str = None) -> AsyncIterator:
download_urls = []
element = element.split("sediment://")[-1]
url = f"{cls.url}/backend-api/conversation/{conversation_id}/attachment/{element}/download"
debug.log(f"OpenaiChat: Downloading image: {url}")
is_sediment = False
if prompt is None:
try:
prompt = element["metadata"]["dalle"]["prompt"]
except KeyError:
pass
if "asset_pointer" in element:
element = element["asset_pointer"]
if isinstance(element, str) and element.startswith("file-service://"):
element = element.split("file-service://", 1)[-1]
if isinstance(element, str) and element.startswith("sediment://"):
is_sediment = True
element = element.split("sediment://")[-1]
else:
raise RuntimeError(f"Invalid image element: {element}")
if is_sediment:
url = f"{cls.url}/backend-api/conversation/{conversation_id}/attachment/{element}/download"
else:
url =f"{cls.url}/backend-api/files/{element}/download"
try:
async with session.get(url, headers=auth_result.headers) as response:
cls._update_request_args(auth_result, session)
await raise_for_status(response)
data = await response.json()
download_url = data.get("download_url")
download_urls.append(download_url)
if download_url is not None:
download_urls.append(download_url)
debug.log(f"OpenaiChat: Found image: {download_url}")
else:
debug.log("OpenaiChat: No download URL found in response: ", data)
except Exception as e:
debug.error("OpenaiChat: Download image failed")
debug.error(e)
return ImagePreview(download_urls, prompt)
if download_urls:
return ImagePreview(download_urls, prompt) if is_sediment else ImageResponse(download_urls, prompt)
@classmethod
async def create_authed(
@@ -402,7 +423,7 @@ class OpenaiChat(AsyncAuthedProvider, ProviderModelMixin):
if conversation.conversation_id is not None:
data["conversation_id"] = conversation.conversation_id
debug.log(f"OpenaiChat: Use conversation: {conversation.conversation_id}")
conversation.prompt = format_image_prompt(messages, prompt)
prompt = conversation.prompt = format_image_prompt(messages, prompt)
if action != "continue":
data["parent_message_id"] = getattr(conversation, "parent_message_id", conversation.message_id)
conversation.parent_message_id = None
@@ -430,6 +451,8 @@ class OpenaiChat(AsyncAuthedProvider, ProviderModelMixin):
cls._update_request_args(auth_result, session)
if response.status in (401, 403, 429):
raise MissingAuthError("Access token is not valid")
elif response.status == 422:
raise RuntimeError((await response.json()), data)
await raise_for_status(response)
buffer = u""
async for line in response.iter_lines():
@@ -515,7 +538,9 @@ class OpenaiChat(AsyncAuthedProvider, ProviderModelMixin):
elif m.get("p") == "/message/metadata/image_gen_title":
fields.prompt = m.get("v")
elif m.get("p") == "/message/content/parts/0/asset_pointer":
fields.generated_images = await cls.get_generated_images(session, auth_result, m.get("v"), fields.prompt, fields.conversation_id)
generated_images = fields.generated_images = await cls.get_generated_image(session, auth_result, m.get("v"), fields.prompt, fields.conversation_id)
if generated_images is not None:
yield generated_images
elif m.get("p") == "/message/metadata/search_result_groups":
for entry in [p.get("entries") for p in m.get("v")]:
for link in entry:
@@ -544,7 +569,9 @@ class OpenaiChat(AsyncAuthedProvider, ProviderModelMixin):
fields.is_thinking = True
yield Reasoning(status=m.get("metadata", {}).get("initial_text"))
if c.get("content_type") == "multimodal_text":
yield await cls.get_generated_images(session, auth_result, c.get("parts"), fields.prompt, fields.conversation_id)
for part in c.get("parts"):
if isinstance(part, dict) and part.get("content_type") == "image_asset_pointer":
yield await cls.get_generated_image(session, auth_result, part, fields.prompt, fields.conversation_id)
if m.get("author", {}).get("role") == "assistant":
if fields.parent_message_id is None:
fields.parent_message_id = v.get("message", {}).get("id")
@@ -291,16 +291,18 @@ class Completions:
max_tokens: Optional[int] = None,
stop: Optional[Union[list[str], str]] = None,
api_key: Optional[str] = None,
ignore_working: Optional[bool] = False,
ignore_stream: Optional[bool] = False,
**kwargs
) -> ChatCompletion:
if isinstance(messages, str):
messages = [{"role": "user", "content": messages}]
if image is not None:
kwargs["media"] = [(image, image_name)]
kwargs["media"] = (image, image_name)
elif "images" in kwargs:
kwargs["media"] = kwargs.pop("images")
for idx, media in kwargs.get("media", []):
if not isinstance(media, (list, tuple)):
kwargs["media"][idx] = (media[0], media[1] if media[1] is not None else getattr(image, "name", None))
if provider is None:
provider = self.provider
if provider is None:
@@ -493,7 +495,10 @@ class Images:
proxy = self.client.proxy
prompt = "create a variation of this image"
if image is not None:
kwargs["media"] = [(image, None)]
kwargs["media"] = image
for idx, media in kwargs.get("media", []):
if not isinstance(media, (list, tuple)):
kwargs["media"][idx] = (media[0], media[1] if media[1] is not None else getattr(image, "name", None))
error = None
response = None
@@ -591,7 +596,6 @@ class AsyncCompletions:
max_tokens: Optional[int] = None,
stop: Optional[Union[list[str], str]] = None,
api_key: Optional[str] = None,
ignore_working: Optional[bool] = False,
ignore_stream: Optional[bool] = False,
**kwargs
) -> Awaitable[ChatCompletion]:
@@ -601,6 +605,10 @@ class AsyncCompletions:
kwargs["media"] = [(image, image_name)]
elif "images" in kwargs:
kwargs["media"] = kwargs.pop("images")
for idx, media in kwargs.get("media", []):
if not isinstance(media, (list, tuple)):
kwargs["media"][idx] = (media[0], media[1] if media[1] is not None else getattr(image, "name", None))
if provider is None:
provider = self.provider
if provider is None:
@@ -4,7 +4,7 @@ import logging
import os
import asyncio
from typing import Iterator
from flask import send_from_directory
from flask import send_from_directory, request
from inspect import signature
from ...errors import VersionNotFoundError, MissingAuthError
@@ -87,7 +87,10 @@ class Api:
latest_version = None
try:
current_version = version.utils.current_version
latest_version = version.utils.latest_version
if request.args.get("cache"):
latest_version = version.utils.latest_version_cached
else:
latest_version = version.utils.latest_version
except VersionNotFoundError:
pass
return {
@@ -16,6 +16,12 @@ from pathlib import Path
from urllib.parse import quote_plus
from hashlib import sha256
try:
from markitdown import MarkItDown
has_markitdown = True
except ImportError:
has_markitdown = False
from ...client.service import convert_to_provider
from ...providers.asyncio import to_sync_generator
from ...providers.response import FinishReason
@@ -299,8 +305,24 @@ class Backend_Api(Api):
filenames = []
media = []
for file in request.files.getlist('files'):
try:
filename = secure_filename(file.filename)
# Copy the file to a temporary location
filename = secure_filename(file.filename)
copyfile = tempfile.NamedTemporaryFile(suffix=filename, delete=False)
shutil.copyfileobj(file.stream, copyfile)
copyfile.close()
file.stream.close()
result = None
if has_markitdown:
try:
md = MarkItDown()
result = md.convert(copyfile.name).text_content
with open(os.path.join(bucket_dir, f"{filename}.md"), 'w') as f:
f.write(f"{result.text_content}\n")
filenames.append(f"{filename}.md")
except Exception as e:
logger.exception(e)
if not result:
if is_allowed_extension(filename):
os.makedirs(media_dir, exist_ok=True)
newfile = os.path.join(media_dir, filename)
@@ -309,11 +331,10 @@ class Backend_Api(Api):
newfile = os.path.join(bucket_dir, filename)
filenames.append(filename)
else:
os.remove(copyfile.name)
continue
with open(newfile, 'wb') as f:
shutil.copyfileobj(file.stream, f)
finally:
file.stream.close()
shutil.copyfile(copyfile.name, newfile)
os.remove(copyfile.name)
with open(os.path.join(bucket_dir, "files.txt"), 'w') as f:
[f.write(f"{filename}\n") for filename in filenames]
return {"bucket_id": bucket_id, "files": filenames, "media": media}
@@ -187,7 +187,7 @@ def stream_read_files(bucket_dir: Path, filenames: list, delete_files: bool = Fa
else:
os.unlink(filepath)
continue
yield f"```{filename}\n"
yield f"```{filename}\n"
if has_pypdf2 and filename.endswith(".pdf"):
try:
reader = PyPDF2.PdfReader(file_path)
@@ -104,6 +104,10 @@ class VersionUtils:
return get_github_version(GITHUB_REPOSITORY)
return get_pypi_version(PACKAGE_NAME)
@cached_property
def latest_version_cached(self) -> str:
return self.latest_version
def check_version(self) -> None:
"""
Checks if the current version of 'g4f' is up to date with the latest version.