Update docker tags in workfloe for slim images,

Update read har file in OpenaiChat provider
Remove webdriver in OpenaiChat provider
Add supported_encodings and supported_encodings in OpenaiChat
This commit is contained in:
Heiner Lohaus 2024-11-17 19:51:26 +01:00
parent ea1448001d
commit b7a8e03220
4 changed files with 104 additions and 191 deletions

View File

@ -55,8 +55,8 @@ jobs:
file: docker/Dockerfile-slim file: docker/Dockerfile-slim
push: true push: true
tags: | tags: |
hlohaus789/g4f=slim hlohaus789/g4f:slim-latest
hlohaus789/g4f=${{ github.ref_name }}-slim hlohaus789/g4f:${{ github.ref_name }}-slim
labels: ${{ steps.metadata.outputs.labels }} labels: ${{ steps.metadata.outputs.labels }}
build-args: | build-args: |
G4F_VERSION=${{ github.ref_name }} G4F_VERSION=${{ github.ref_name }}

View File

@ -9,22 +9,19 @@ from aiohttp import ClientWebSocketResponse
from copy import copy from copy import copy
try: try:
import webview import nodriver
has_webview = True has_nodriver = True
except ImportError: except ImportError:
has_webview = False has_nodriver = False
try: try:
from selenium.webdriver.common.by import By from platformdirs import user_config_dir
from selenium.webdriver.support.ui import WebDriverWait has_platformdirs = True
from selenium.webdriver.support import expected_conditions as EC
except ImportError: except ImportError:
pass has_platformdirs = False
from ..base_provider import AsyncGeneratorProvider, ProviderModelMixin from ..base_provider import AsyncGeneratorProvider, ProviderModelMixin
from ...webdriver import get_browser
from ...typing import AsyncResult, Messages, Cookies, ImageType, AsyncIterator from ...typing import AsyncResult, Messages, Cookies, ImageType, AsyncIterator
from ...requests import get_args_from_browser, raise_for_status from ...requests.raise_for_status import raise_for_status
from ...requests.aiohttp import StreamSession from ...requests.aiohttp import StreamSession
from ...image import ImageResponse, ImageRequest, to_image, to_bytes, is_accepted_format from ...image import ImageResponse, ImageRequest, to_image, to_bytes, is_accepted_format
from ...errors import MissingAuthError, ResponseError from ...errors import MissingAuthError, ResponseError
@ -63,11 +60,6 @@ class OpenaiChat(AsyncGeneratorProvider, ProviderModelMixin):
default_vision_model = "gpt-4o" default_vision_model = "gpt-4o"
models = [ "auto", "gpt-4o-mini", "gpt-4o", "gpt-4", "gpt-4-gizmo"] models = [ "auto", "gpt-4o-mini", "gpt-4o", "gpt-4", "gpt-4-gizmo"]
model_aliases = {
#"gpt-4-turbo": "gpt-4",
#"gpt-4": "gpt-4-gizmo",
#"dalle": "gpt-4",
}
_api_key: str = None _api_key: str = None
_headers: dict = None _headers: dict = None
_cookies: Cookies = None _cookies: Cookies = None
@ -219,9 +211,12 @@ class OpenaiChat(AsyncGeneratorProvider, ProviderModelMixin):
""" """
# Create a message object with the user role and the content # Create a message object with the user role and the content
messages = [{ messages = [{
"id": str(uuid.uuid4()),
"author": {"role": message["role"]}, "author": {"role": message["role"]},
"content": {"content_type": "text", "parts": [message["content"]]}, "content": {"content_type": "text", "parts": [message["content"]]},
"id": str(uuid.uuid4()),
"create_time": int(time.time()),
"id": str(uuid.uuid4()),
"metadata": {"serialization_metadata": {"custom_symbol_offsets": []}}
} for message in messages] } for message in messages]
# Check if there is an image response # Check if there is an image response
@ -250,7 +245,7 @@ class OpenaiChat(AsyncGeneratorProvider, ProviderModelMixin):
return messages return messages
@classmethod @classmethod
async def get_generated_image(cls, session: StreamSession, headers: dict, line: dict) -> ImageResponse: async def get_generated_image(cls, session: StreamSession, headers: dict, element: dict) -> ImageResponse:
""" """
Retrieves the image response based on the message content. Retrieves the image response based on the message content.
@ -269,15 +264,8 @@ class OpenaiChat(AsyncGeneratorProvider, ProviderModelMixin):
Raises: Raises:
RuntimeError: If there'san error in downloading the image, including issues with the HTTP request or response. RuntimeError: If there'san error in downloading the image, including issues with the HTTP request or response.
""" """
if "parts" not in line["message"]["content"]: prompt = element["metadata"]["dalle"]["prompt"]
return file_id = element["asset_pointer"].split("file-service://", 1)[1]
first_part = line["message"]["content"]["parts"][0]
if "asset_pointer" not in first_part or "metadata" not in first_part:
return
if first_part["metadata"] is None or first_part["metadata"]["dalle"] is None:
return
prompt = first_part["metadata"]["dalle"]["prompt"]
file_id = first_part["asset_pointer"].split("file-service://", 1)[1]
try: try:
async with session.get(f"{cls.url}/backend-api/files/{file_id}/download", headers=headers) as response: async with session.get(f"{cls.url}/backend-api/files/{file_id}/download", headers=headers) as response:
cls._update_request_args(session) cls._update_request_args(session)
@ -365,9 +353,10 @@ class OpenaiChat(AsyncGeneratorProvider, ProviderModelMixin):
if cls._expires is not None and cls._expires < time.time(): if cls._expires is not None and cls._expires < time.time():
cls._headers = cls._api_key = None cls._headers = cls._api_key = None
arkose_token = None arkose_token = None
proofTokens = None proofToken = None
turnstileToken = None
try: try:
arkose_token, api_key, cookies, headers, proofTokens = await getArkoseAndAccessToken(proxy) arkose_token, api_key, cookies, headers, proofToken, turnstileToken = await getArkoseAndAccessToken(proxy)
cls._create_request_args(cookies, headers) cls._create_request_args(cookies, headers)
cls._set_api_key(api_key) cls._set_api_key(api_key)
except NoValidHarFileError as e: except NoValidHarFileError as e:
@ -400,32 +389,28 @@ class OpenaiChat(AsyncGeneratorProvider, ProviderModelMixin):
f"{cls.url}/backend-anon/sentinel/chat-requirements" f"{cls.url}/backend-anon/sentinel/chat-requirements"
if cls._api_key is None else if cls._api_key is None else
f"{cls.url}/backend-api/sentinel/chat-requirements", f"{cls.url}/backend-api/sentinel/chat-requirements",
json={"p": generate_proof_token(True, user_agent=cls._headers["user-agent"], proofTokens=proofTokens)}, json={"p": generate_proof_token(True, user_agent=cls._headers["user-agent"], proofToken=proofToken)},
headers=cls._headers headers=cls._headers
) as response: ) as response:
cls._update_request_args(session) cls._update_request_args(session)
await raise_for_status(response) await raise_for_status(response)
requirements = await response.json() chat_requirements = await response.json()
text_data = json.loads(requirements.get("text", "{}")) need_turnstile = chat_requirements.get("turnstile", {}).get("required", False)
need_arkose = text_data.get("turnstile", {}).get("required", False) need_arkose = chat_requirements.get("arkose", {}).get("required", False)
if need_arkose: chat_token = chat_requirements.get("token")
arkose_token = text_data.get("turnstile", {}).get("dx")
else:
need_arkose = requirements.get("arkose", {}).get("required", False)
chat_token = requirements["token"]
if need_arkose and arkose_token is None: if need_arkose and arkose_token is None:
arkose_token, api_key, cookies, headers, proofTokens = await getArkoseAndAccessToken(proxy) arkose_token, api_key, cookies, headers, proofToken, turnstileToken = await getArkoseAndAccessToken(proxy)
cls._create_request_args(cookies, headers) cls._create_request_args(cookies, headers)
cls._set_api_key(api_key) cls._set_api_key(api_key)
if arkose_token is None: if arkose_token is None:
raise MissingAuthError("No arkose token found in .har file") raise MissingAuthError("No arkose token found in .har file")
if "proofofwork" in requirements: if "proofofwork" in chat_requirements:
proofofwork = generate_proof_token( proofofwork = generate_proof_token(
**requirements["proofofwork"], **chat_requirements["proofofwork"],
user_agent=cls._headers["user-agent"], user_agent=cls._headers["user-agent"],
proofTokens=proofTokens proofToken=proofToken
) )
if debug.logging: if debug.logging:
print( print(
@ -441,15 +426,18 @@ class OpenaiChat(AsyncGeneratorProvider, ProviderModelMixin):
websocket_request_id = str(uuid.uuid4()) websocket_request_id = str(uuid.uuid4())
data = { data = {
"action": action, "action": action,
"conversation_mode": {"kind": "primary_assistant"}, "messages": None,
"force_paragen": False,
"force_rate_limit": False,
"conversation_id": conversation.conversation_id,
"parent_message_id": conversation.message_id, "parent_message_id": conversation.message_id,
"model": model, "model": model,
"paragen_cot_summary_display_override": "allow",
"history_and_training_disabled": history_disabled and not auto_continue and not return_conversation, "history_and_training_disabled": history_disabled and not auto_continue and not return_conversation,
"websocket_request_id": websocket_request_id "conversation_mode": {"kind":"primary_assistant"},
"websocket_request_id": websocket_request_id,
"supported_encodings": ["v1"],
"supports_buffering": True
} }
if conversation.conversation_id is not None:
data["conversation_id"] = conversation.conversation_id
if action != "continue": if action != "continue":
messages = messages if conversation_id is None else [messages[-1]] messages = messages if conversation_id is None else [messages[-1]]
data["messages"] = cls.create_messages(messages, image_request) data["messages"] = cls.create_messages(messages, image_request)
@ -458,10 +446,12 @@ class OpenaiChat(AsyncGeneratorProvider, ProviderModelMixin):
"Openai-Sentinel-Chat-Requirements-Token": chat_token, "Openai-Sentinel-Chat-Requirements-Token": chat_token,
**cls._headers **cls._headers
} }
if need_arkose: if arkose_token:
headers["Openai-Sentinel-Arkose-Token"] = arkose_token headers["Openai-Sentinel-Arkose-Token"] = arkose_token
if proofofwork is not None: if proofofwork is not None:
headers["Openai-Sentinel-Proof-Token"] = proofofwork headers["Openai-Sentinel-Proof-Token"] = proofofwork
if need_turnstile and turnstileToken is not None:
headers['openai-sentinel-turnstile-token'] = turnstileToken
async with session.post( async with session.post(
f"{cls.url}/backend-anon/conversation" f"{cls.url}/backend-anon/conversation"
if cls._api_key is None else if cls._api_key is None else
@ -510,7 +500,6 @@ class OpenaiChat(AsyncGeneratorProvider, ProviderModelMixin):
fields: Conversation, fields: Conversation,
ws = None ws = None
) -> AsyncIterator: ) -> AsyncIterator:
last_message: int = 0
async for message in messages: async for message in messages:
if message.startswith(b'{"wss_url":'): if message.startswith(b'{"wss_url":'):
message = json.loads(message) message = json.loads(message)
@ -527,10 +516,6 @@ class OpenaiChat(AsyncGeneratorProvider, ProviderModelMixin):
async for chunk in cls.iter_messages_line(session, message, fields): async for chunk in cls.iter_messages_line(session, message, fields):
if fields.finish_reason is not None: if fields.finish_reason is not None:
break break
elif isinstance(chunk, str):
if len(chunk) > last_message:
yield chunk[last_message:]
last_message = len(chunk)
else: else:
yield chunk yield chunk
if fields.finish_reason is not None: if fields.finish_reason is not None:
@ -548,82 +533,50 @@ class OpenaiChat(AsyncGeneratorProvider, ProviderModelMixin):
line = json.loads(line[6:]) line = json.loads(line[6:])
except: except:
return return
if "message" not in line: if isinstance(line, dict) and "v" in line:
return v = line.get("v")
if "error" in line and line["error"]: r = ""
raise RuntimeError(line["error"]) if isinstance(v, str):
if "message_type" not in line["message"]["metadata"]: yield v
return elif isinstance(v, list):
image_response = await cls.get_generated_image(session, cls._headers, line) for m in v:
if image_response is not None: if m.get("p") == "/message/content/parts/0":
yield image_response yield m.get("v")
if line["message"]["author"]["role"] != "assistant": elif m.get("p") == "/message/metadata":
return fields.finish_reason = m.get("v", {}).get("finish_details", {}).get("type")
if line["message"]["content"]["content_type"] != "text": break
return elif isinstance(v, dict):
if line["message"]["metadata"]["message_type"] not in ("next", "continue", "variant"):
return
if line["message"]["recipient"] != "all":
return
if fields.conversation_id is None: if fields.conversation_id is None:
fields.conversation_id = line["conversation_id"] fields.conversation_id = v.get("conversation_id")
fields.message_id = line["message"]["id"] fields.message_id = v.get("message", {}).get("id")
if "parts" in line["message"]["content"]: c = v.get("message", {}).get("content", {})
yield line["message"]["content"]["parts"][0] if c.get("content_type") == "multimodal_text":
if "finish_details" in line["message"]["metadata"]: generated_images = []
fields.finish_reason = line["message"]["metadata"]["finish_details"]["type"] for element in c.get("parts"):
if element.get("content_type") == "image_asset_pointer":
@classmethod generated_images.append(
async def webview_access_token(cls) -> str: cls.get_generated_image(session, cls._headers, element)
window = webview.create_window("OpenAI Chat", cls.url) )
await asyncio.sleep(3) elif element.get("content_type") == "text":
prompt_input = None for part in element.get("parts", []):
while not prompt_input: yield part
try: for image_response in await asyncio.gather(*generated_images):
await asyncio.sleep(1) yield image_response
prompt_input = window.dom.get_element("#prompt-textarea") return
except: if "error" in line and line.get("error"):
... raise RuntimeError(line.get("error"))
window.evaluate_js("""
this._fetch = this.fetch;
this.fetch = async (url, options) => {
const response = await this._fetch(url, options);
if (url == "https://chatgpt.com/backend-api/conversation") {
this._headers = options.headers;
return response;
}
return response;
};
""")
window.evaluate_js("""
document.querySelector('.from-token-main-surface-secondary').click();
""")
headers = None
while headers is None:
headers = window.evaluate_js("this._headers")
await asyncio.sleep(1)
headers["User-Agent"] = window.evaluate_js("this.navigator.userAgent")
cookies = [list(*cookie.items()) for cookie in window.get_cookies()]
window.destroy()
cls._cookies = dict([(name, cookie.value) for name, cookie in cookies])
cls._headers = headers
cls._expires = int(time.time()) + 60 * 60 * 4
cls._update_cookie_header()
@classmethod @classmethod
async def nodriver_access_token(cls, proxy: str = None): async def nodriver_access_token(cls, proxy: str = None):
try: if not has_nodriver:
import nodriver as uc
except ImportError:
return return
try: if has_platformdirs:
from platformdirs import user_config_dir
user_data_dir = user_config_dir("g4f-nodriver") user_data_dir = user_config_dir("g4f-nodriver")
except: else:
user_data_dir = None user_data_dir = None
if debug.logging: if debug.logging:
print(f"Open nodriver with user_dir: {user_data_dir}") print(f"Open nodriver with user_dir: {user_data_dir}")
browser = await uc.start( browser = await nodriver.start(
user_data_dir=user_data_dir, user_data_dir=user_data_dir,
browser_args=None if proxy is None else [f"--proxy-server={proxy}"], browser_args=None if proxy is None else [f"--proxy-server={proxy}"],
) )
@ -649,48 +602,6 @@ this.fetch = async (url, options) => {
cls._create_request_args(cookies, user_agent=user_agent) cls._create_request_args(cookies, user_agent=user_agent)
cls._set_api_key(api_key) cls._set_api_key(api_key)
@classmethod
def browse_access_token(cls, proxy: str = None, timeout: int = 1200) -> None:
"""
Browse to obtain an access token.
Args:
proxy (str): Proxy to use for browsing.
Returns:
tuple[str, dict]: A tuple containing the access token and cookies.
"""
driver = get_browser(proxy=proxy)
try:
driver.get(f"{cls.url}/")
WebDriverWait(driver, timeout).until(EC.presence_of_element_located((By.ID, "prompt-textarea")))
access_token = driver.execute_script(
"let session = await fetch('/api/auth/session');"
"let data = await session.json();"
"let accessToken = data['accessToken'];"
"let expires = new Date(); expires.setTime(expires.getTime() + 60 * 60 * 4 * 1000);"
"document.cookie = 'access_token=' + accessToken + ';expires=' + expires.toUTCString() + ';path=/';"
"return accessToken;"
)
args = get_args_from_browser(f"{cls.url}/", driver, do_bypass_cloudflare=False)
cls._headers = args["headers"]
cls._cookies = args["cookies"]
cls._update_cookie_header()
cls._set_api_key(access_token)
finally:
driver.close()
@classmethod
async def fetch_access_token(cls, session: StreamSession, headers: dict):
async with session.get(
f"{cls.url}/api/auth/session",
headers=headers
) as response:
if response.ok:
data = await response.json()
if "accessToken" in data:
return data["accessToken"]
@staticmethod @staticmethod
def get_default_headers() -> dict: def get_default_headers() -> dict:
return { return {

View File

@ -27,16 +27,18 @@ class arkReq:
self.arkCookies = arkCookies self.arkCookies = arkCookies
self.userAgent = userAgent self.userAgent = userAgent
arkPreURL = "https://tcr9i.chat.openai.com/fc/gt2/public_key/35536E1E-65B4-4D96-9D97-6ADB7EFF8147" arkoseURL = "https://tcr9i.chat.openai.com/fc/gt2/public_key/35536E1E-65B4-4D96-9D97-6ADB7EFF8147"
sessionUrl = "https://chatgpt.com/" startUrl = "https://chatgpt.com/"
chatArk: arkReq = None conversationUrl = "https://chatgpt.com/c/"
arkoseRequest: arkReq = None
accessToken: str = None accessToken: str = None
cookies: dict = None cookies: dict = None
headers: dict = None headers: dict = None
proofTokens: list = [] proofToken: list = []
turnstileToken: str = None
def readHAR(): def readHAR():
global proofTokens global arkoseRequest, accessToken, proofToken, turnstileToken
harPath = [] harPath = []
chatArks = [] chatArks = []
accessToken = None accessToken = None
@ -58,15 +60,17 @@ def readHAR():
v_headers = get_headers(v) v_headers = get_headers(v)
try: try:
if "openai-sentinel-proof-token" in v_headers: if "openai-sentinel-proof-token" in v_headers:
proofTokens.append(json.loads(base64.b64decode( proofToken = json.loads(base64.b64decode(
v_headers["openai-sentinel-proof-token"].split("gAAAAAB", 1)[-1].encode() v_headers["openai-sentinel-proof-token"].split("gAAAAAB", 1)[-1].encode()
).decode())) ).decode())
if "openai-sentinel-turnstile-token" in v_headers:
turnstileToken = v_headers["openai-sentinel-turnstile-token"]
except Exception as e: except Exception as e:
if debug.logging: if debug.logging:
print(f"Read proof token: {e}") print(f"Read proof token: {e}")
if arkPreURL in v['request']['url']: if arkoseURL in v['request']['url']:
chatArks.append(parseHAREntry(v)) arkoseRequest = parseHAREntry(v)
elif v['request']['url'] == sessionUrl: elif v['request']['url'] == startUrl or v['request']['url'].startswith(conversationUrl):
try: try:
match = re.search(r'"accessToken":"(.*?)"', v["response"]["content"]["text"]) match = re.search(r'"accessToken":"(.*?)"', v["response"]["content"]["text"])
if match: if match:
@ -78,8 +82,8 @@ def readHAR():
if not accessToken: if not accessToken:
raise NoValidHarFileError("No accessToken found in .har files") raise NoValidHarFileError("No accessToken found in .har files")
if not chatArks: if not chatArks:
return None, accessToken, cookies, headers return cookies, headers
return chatArks.pop(), accessToken, cookies, headers return cookies, headers
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(':')}
@ -110,7 +114,7 @@ def genArkReq(chatArk: arkReq) -> arkReq:
tmpArk.arkHeader['x-ark-esync-value'] = bw tmpArk.arkHeader['x-ark-esync-value'] = bw
return tmpArk return tmpArk
async def sendRequest(tmpArk: arkReq, proxy: str = None): async def sendRequest(tmpArk: arkReq, proxy: str = None) -> str:
async with StreamSession(headers=tmpArk.arkHeader, cookies=tmpArk.arkCookies, proxies={"https": proxy}) as session: async with StreamSession(headers=tmpArk.arkHeader, cookies=tmpArk.arkCookies, proxies={"https": proxy}) as session:
async with session.post(tmpArk.arkURL, data=tmpArk.arkBody) as response: async with session.post(tmpArk.arkURL, data=tmpArk.arkBody) as response:
data = await response.json() data = await response.json()
@ -144,10 +148,10 @@ def getN() -> str:
return base64.b64encode(timestamp.encode()).decode() return base64.b64encode(timestamp.encode()).decode()
async def getArkoseAndAccessToken(proxy: str) -> tuple[str, str, dict, dict]: async def getArkoseAndAccessToken(proxy: str) -> tuple[str, str, dict, dict]:
global chatArk, accessToken, cookies, headers, proofTokens global arkoseRequest, accessToken, cookies, headers, proofToken, turnstileToken
if chatArk is None or accessToken is None: if arkoseRequest is None or accessToken is None:
chatArk, accessToken, cookies, headers = readHAR() cookies, headers = readHAR()
if chatArk is None: if arkoseRequest is None:
return None, accessToken, cookies, headers, proofTokens return None, accessToken, cookies, headers, proofToken, turnstileToken
newReq = genArkReq(chatArk) newReq = genArkReq(arkoseRequest)
return await sendRequest(newReq, proxy), accessToken, cookies, headers, proofTokens return await sendRequest(newReq, proxy), accessToken, cookies, headers, proofToken, turnstileToken

View File

@ -4,18 +4,16 @@ import json
import base64 import base64
from datetime import datetime, timezone from datetime import datetime, timezone
def generate_proof_token(required: bool, seed: str = "", difficulty: str = "", user_agent: str = None, proofTokens: list = None): def generate_proof_token(required: bool, seed: str = "", difficulty: str = "", user_agent: str = None, proofToken: str = None):
if not required: if not required:
return return
if proofTokens: if proofToken is None:
config = proofTokens[-1]
else:
screen = random.choice([3008, 4010, 6000]) * random.choice([1, 2, 4]) screen = random.choice([3008, 4010, 6000]) * random.choice([1, 2, 4])
# Get current UTC time # Get current UTC time
now_utc = datetime.now(timezone.utc) now_utc = datetime.now(timezone.utc)
parse_time = now_utc.strftime('%a, %d %b %Y %H:%M:%S GMT') parse_time = now_utc.strftime('%a, %d %b %Y %H:%M:%S GMT')
config = [ proofToken = [
screen, parse_time, screen, parse_time,
None, 0, user_agent, None, 0, user_agent,
"https://tcr9i.chat.openai.com/v2/35536E1E-65B4-4D96-9D97-6ADB7EFF8147/api.js", "https://tcr9i.chat.openai.com/v2/35536E1E-65B4-4D96-9D97-6ADB7EFF8147/api.js",
@ -28,8 +26,8 @@ def generate_proof_token(required: bool, seed: str = "", difficulty: str = "", u
diff_len = len(difficulty) diff_len = len(difficulty)
for i in range(100000): for i in range(100000):
config[3] = i proofToken[3] = i
json_data = json.dumps(config) json_data = json.dumps(proofToken)
base = base64.b64encode(json_data.encode()).decode() base = base64.b64encode(json_data.encode()).decode()
hash_value = hashlib.sha3_512((seed + base).encode()).digest() hash_value = hashlib.sha3_512((seed + base).encode()).digest()