From 2e531d227c5672c169c26251368c925abb775c80 Mon Sep 17 00:00:00 2001 From: Heiner Lohaus Date: Mon, 6 Jan 2025 23:20:29 +0100 Subject: [PATCH 1/5] Fix invalid escape in requests module Add none auth with OpenAI using nodriver Fix missing 1 required positional argument: 'cls' Update count tokens in GUI Fix streaming example in requests guide Remove ChatGptEs as default model --- docs/requests.md | 41 +++---- g4f/Provider/DDG.py | 2 +- g4f/Provider/needs_auth/OpenaiChat.py | 42 ++++---- g4f/Provider/openai/har_file.py | 2 +- g4f/api/__init__.py | 4 +- g4f/client/__init__.py | 39 +++++-- g4f/client/stubs.py | 7 +- g4f/gui/client/index.html | 3 +- g4f/gui/client/static/js/chat.v1.js | 150 ++++++++++++++++---------- g4f/gui/server/api.py | 4 +- g4f/models.py | 5 +- g4f/providers/base_provider.py | 58 +++++++--- g4f/providers/response.py | 3 + g4f/providers/retry_provider.py | 31 +++--- g4f/requests/__init__.py | 5 +- g4f/tools/files.py | 4 +- 16 files changed, 238 insertions(+), 162 deletions(-) diff --git a/docs/requests.md b/docs/requests.md index e185cec54b2..8de78963154 100644 --- a/docs/requests.md +++ b/docs/requests.md @@ -73,7 +73,6 @@ For scenarios where you want to receive partial responses or stream data as it's ```python import requests import json -from queue import Queue def fetch_response(url, model, messages): """ @@ -87,7 +86,7 @@ def fetch_response(url, model, messages): Returns: requests.Response: The streamed response object. """ - payload = {"model": model, "messages": messages} + payload = {"model": model, "messages": messages, "stream": True} headers = { "Content-Type": "application/json", "Accept": "text/event-stream", @@ -99,7 +98,7 @@ def fetch_response(url, model, messages): ) return response -def process_stream(response, output_queue): +def process_stream(response): """ Processes the streamed response and extracts messages. @@ -111,37 +110,31 @@ def process_stream(response, output_queue): if line: line = line.decode("utf-8") if line == "data: [DONE]": + print("\n\nConversation completed.") break if line.startswith("data: "): try: data = json.loads(line[6:]) - message = data.get("message", "") + message = data.get("choices", [{}])[0].get("delta", {}).get("content") if message: - output_queue.put(message) - except json.JSONDecodeError: + print(message, end="", flush=True) + except json.JSONDecodeError as e: + print(f"Error decoding JSON: {e}") continue # Define the API endpoint -chat_url = "http://localhost/v1/chat/completions" +chat_url = "http://localhost:8080/v1/chat/completions" # Define the payload -model = "gpt-4o" -messages = [{"role": "system", "content": "Hello, how are you?"}] - -# Initialize the queue to store output messages -output_queue = Queue() +model = "" +messages = [{"role": "user", "content": "Hello, how are you?"}] try: # Fetch the streamed response response = fetch_response(chat_url, model, messages) # Process the streamed response - process_stream(response, output_queue) - - # Retrieve messages from the queue - while not output_queue.empty(): - msg = output_queue.get() - print(msg) + process_stream(response) except Exception as e: print(f"An error occurred: {e}") @@ -150,23 +143,21 @@ except Exception as e: **Explanation:** - **`fetch_response` Function:** - Sends a POST request to the streaming chat completions endpoint with the specified model and messages. - - Sets the `Accept` header to `text/event-stream` to enable streaming. + - Sets `stream` parameter to `true` to enable streaming. - Raises an exception if the request fails. - **`process_stream` Function:** - Iterates over each line in the streamed response. - Decodes the line and checks for the termination signal `"data: [DONE]"`. - Parses lines that start with `"data: "` to extract the message content. - - Enqueues the extracted messages into `output_queue` for further processing. - **Main Execution:** - Defines the API endpoint, model, and messages. - - Initializes a `Queue` to store incoming messages. - Fetches and processes the streamed response. - - Retrieves and prints messages from the queue. + - Retrieves and prints messages. **Usage Tips:** -- Ensure your local server supports streaming and the `Accept` header appropriately. +- Ensure your local server supports streaming. - Adjust the `chat_url` if your local server runs on a different port or path. - Use threading or asynchronous programming for handling streams in real-time applications. @@ -286,7 +277,7 @@ async def fetch_response_async(url, model, messages, output_queue): messages (list): A list of message dictionaries. output_queue (Queue): A queue to store the extracted messages. """ - payload = {"model": model, "messages": messages} + payload = {"model": model, "messages": messages, "stream": True} headers = { "Content-Type": "application/json", "Accept": "text/event-stream", @@ -305,7 +296,7 @@ async def fetch_response_async(url, model, messages, output_queue): if decoded_line.startswith("data: "): try: data = json.loads(decoded_line[6:]) - message = data.get("message", "") + message = data.get("choices", [{}])[0].get("delta", {}).get("content") if message: output_queue.put(message) except json.JSONDecodeError: diff --git a/g4f/Provider/DDG.py b/g4f/Provider/DDG.py index 78e13568cdc..19ba747a868 100644 --- a/g4f/Provider/DDG.py +++ b/g4f/Provider/DDG.py @@ -73,7 +73,7 @@ async def create_async_generator( "Content-Type": "application/json", }, cookies: dict = None, - max_retries: int = 3, + max_retries: int = 0, **kwargs ) -> AsyncResult: if cookies is None and conversation is not None: diff --git a/g4f/Provider/needs_auth/OpenaiChat.py b/g4f/Provider/needs_auth/OpenaiChat.py index 0fe7cafe7eb..72465ec2659 100644 --- a/g4f/Provider/needs_auth/OpenaiChat.py +++ b/g4f/Provider/needs_auth/OpenaiChat.py @@ -106,9 +106,8 @@ class OpenaiChat(AsyncAuthedProvider, ProviderModelMixin): @classmethod async def on_auth_async(cls, **kwargs) -> AsyncIterator: - if cls.needs_auth: - async for chunk in cls.login(): - yield chunk + async for chunk in cls.login(): + yield chunk yield AuthResult( api_key=cls._api_key, cookies=cls._cookies or RequestConfig.cookies or {}, @@ -335,11 +334,11 @@ async def create_authed( cls._update_request_args(auth_result, session) await raise_for_status(response) else: - if cls._headers is None: + if cls._headers is None and getattr(auth_result, "cookies", None): cls._create_request_args(auth_result.cookies, auth_result.headers) - if not cls._set_api_key(auth_result.api_key): - raise MissingAuthError("Access token is not valid") - async with session.get(cls.url, headers=auth_result.headers) as response: + if not cls._set_api_key(getattr(auth_result, "api_key", None)): + raise MissingAuthError("Access token is not valid") + async with session.get(cls.url, headers=cls._headers) as response: cls._update_request_args(auth_result, session) await raise_for_status(response) try: @@ -349,9 +348,11 @@ async def create_authed( debug.log(f"{e.__class__.__name__}: {e}") model = cls.get_model(model) if conversation is None: - conversation = Conversation(conversation_id, str(uuid.uuid4())) + conversation = Conversation(conversation_id, str(uuid.uuid4()), getattr(auth_result, "cookies", {}).get("oai-did")) else: conversation = copy(conversation) + if getattr(auth_result, "cookies", {}).get("oai-did") != conversation.user_id: + conversation = Conversation(None, str(uuid.uuid4())) if cls._api_key is None: auto_continue = False conversation.finish_reason = None @@ -361,11 +362,11 @@ async def create_authed( f"{cls.url}/backend-anon/sentinel/chat-requirements" if cls._api_key is None else f"{cls.url}/backend-api/sentinel/chat-requirements", - json={"p": None if not getattr(auth_result, "proof_token") else get_requirements_token(auth_result.proof_token)}, + json={"p": None if not getattr(auth_result, "proof_token", None) else get_requirements_token(getattr(auth_result, "proof_token", None))}, headers=cls._headers ) as response: - if response.status == 401: - cls._headers = cls._api_key = None + if response.status in (401, 403): + auth_result.reset() else: cls._update_request_args(auth_result, session) await raise_for_status(response) @@ -380,14 +381,13 @@ async def create_authed( # cls._set_api_key(auth_result.access_token) # if auth_result.arkose_token is None: # raise MissingAuthError("No arkose token found in .har file") - if "proofofwork" in chat_requirements: - if auth_result.proof_token is None: + if getattr(auth_result, "proof_token") is None: auth_result.proof_token = get_config(auth_result.headers.get("user-agent")) proofofwork = generate_proof_token( **chat_requirements["proofofwork"], - user_agent=auth_result.headers.get("user-agent"), - proof_token=getattr(auth_result, "proof_token") + user_agent=getattr(auth_result, "headers", {}).get("user-agent"), + proof_token=getattr(auth_result, "proof_token", None) ) [debug.log(text) for text in ( #f"Arkose: {'False' if not need_arkose else auth_result.arkose_token[:12]+'...'}", @@ -434,7 +434,7 @@ async def create_authed( # headers["openai-sentinel-arkose-token"] = RequestConfig.arkose_token if proofofwork is not None: headers["openai-sentinel-proof-token"] = proofofwork - if need_turnstile and auth_result.turnstile_token is not None: + if need_turnstile and getattr(auth_result, "turnstile_token", None) is not None: headers['openai-sentinel-turnstile-token'] = auth_result.turnstile_token async with session.post( f"{cls.url}/backend-anon/conversation" @@ -653,7 +653,7 @@ def on_request(event: nodriver.cdp.network.RequestWillBeSent): await page.evaluate("document.getElementById('prompt-textarea').innerText = 'Hello'") await page.evaluate("document.querySelector('[data-testid=\"send-button\"]').click()") while True: - if cls._api_key is not None: + if cls._api_key is not None or not cls.needs_auth: break body = await page.evaluate("JSON.stringify(window.__remixContext)") if body: @@ -689,8 +689,9 @@ def _create_request_args(cls, cookies: Cookies = None, headers: dict = None, use @classmethod def _update_request_args(cls, auth_result: AuthResult, session: StreamSession): - for c in session.cookie_jar if hasattr(session, "cookie_jar") else session.cookies.jar: - auth_result.cookies[getattr(c, "key", getattr(c, "name", ""))] = c.value + if hasattr(auth_result, "cookies"): + for c in session.cookie_jar if hasattr(session, "cookie_jar") else session.cookies.jar: + auth_result.cookies[getattr(c, "key", getattr(c, "name", ""))] = c.value cls._update_cookie_header() @classmethod @@ -717,12 +718,13 @@ class Conversation(JsonConversation): """ Class to encapsulate response fields. """ - def __init__(self, conversation_id: str = None, message_id: str = None, finish_reason: str = None, parent_message_id: str = None): + def __init__(self, conversation_id: str = None, message_id: str = None, user_id: str = None, finish_reason: str = None, parent_message_id: str = None): self.conversation_id = conversation_id self.message_id = message_id self.finish_reason = finish_reason self.is_recipient = False self.parent_message_id = message_id if parent_message_id is None else parent_message_id + self.user_id = user_id def get_cookies( urls: Optional[Iterator[str]] = None diff --git a/g4f/Provider/openai/har_file.py b/g4f/Provider/openai/har_file.py index 989a9efc44a..2acd34fbad7 100644 --- a/g4f/Provider/openai/har_file.py +++ b/g4f/Provider/openai/har_file.py @@ -32,7 +32,7 @@ class RequestConfig: arkose_token: str = None headers: dict = {} cookies: dict = {} - data_build: str = "prod-697873d7e78bb14df6e13af3a91fa237cc4db415" + data_build: str = "prod-db8e51e8414e068257091cf5003a62d3d4ee6ed0" class arkReq: def __init__(self, arkURL, arkBx, arkHeader, arkBody, arkCookies, userAgent): diff --git a/g4f/api/__init__.py b/g4f/api/__init__.py index 73a9f64e64e..d4a3bd76a77 100644 --- a/g4f/api/__init__.py +++ b/g4f/api/__init__.py @@ -432,7 +432,7 @@ def upload_cookies(files: List[UploadFile]): HTTP_404_NOT_FOUND: {"model": ErrorResponseModel}, }) def read_files(request: Request, bucket_id: str, delete_files: bool = True, refine_chunks_with_spacy: bool = False): - bucket_dir = os.path.join(get_cookies_dir(), bucket_id) + bucket_dir = os.path.join(get_cookies_dir(), "buckets", bucket_id) event_stream = "text/event-stream" in request.headers.get("accept", "") if not os.path.isdir(bucket_dir): return ErrorResponse.from_message("Bucket dir not found", 404) @@ -443,7 +443,7 @@ def read_files(request: Request, bucket_id: str, delete_files: bool = True, refi HTTP_200_OK: {"model": UploadResponseModel} }) def upload_files(bucket_id: str, files: List[UploadFile]): - bucket_dir = os.path.join(get_cookies_dir(), bucket_id) + bucket_dir = os.path.join(get_cookies_dir(), "buckets", bucket_id) os.makedirs(bucket_dir, exist_ok=True) filenames = [] for file in files: diff --git a/g4f/client/__init__.py b/g4f/client/__init__.py index cca85db4de1..cd2a43feee0 100644 --- a/g4f/client/__init__.py +++ b/g4f/client/__init__.py @@ -63,7 +63,7 @@ def iter_response( tool_calls = chunk.get_list() continue elif isinstance(chunk, Usage): - usage = chunk.get_dict() + usage = chunk continue elif isinstance(chunk, BaseConversation): yield chunk @@ -90,19 +90,23 @@ def iter_response( idx += 1 if usage is None: - usage = Usage(prompt_tokens=0, completion_tokens=idx, total_tokens=idx).get_dict() + usage = Usage(prompt_tokens=0, completion_tokens=idx, total_tokens=idx) + finish_reason = "stop" if finish_reason is None else finish_reason if stream: - yield ChatCompletionChunk.model_construct(None, finish_reason, completion_id, int(time.time())) + yield ChatCompletionChunk.model_construct( + None, finish_reason, completion_id, int(time.time()), + usage=usage.get_dict() + ) else: if response_format is not None and "type" in response_format: if response_format["type"] == "json_object": content = filter_json(content) - yield ChatCompletion.model_construct(content, finish_reason, completion_id, int(time.time()), **filter_none( - tool_calls=tool_calls, - usage=usage - )) + yield ChatCompletion.model_construct( + content, finish_reason, completion_id, int(time.time()), + usage=usage.get_dict(), **filter_none(tool_calls=tool_calls) + ) # Synchronous iter_append_model_and_provider function def iter_append_model_and_provider(response: ChatCompletionResponseType, last_model: str, last_provider: ProviderType) -> ChatCompletionResponseType: @@ -126,6 +130,8 @@ async def async_iter_response( finish_reason = None completion_id = ''.join(random.choices(string.ascii_letters + string.digits, k=28)) idx = 0 + tool_calls = None + usage = None try: async for chunk in response: @@ -135,6 +141,12 @@ async def async_iter_response( elif isinstance(chunk, BaseConversation): yield chunk continue + elif isinstance(chunk, ToolCalls): + tool_calls = chunk.get_list() + continue + elif isinstance(chunk, Usage): + usage = chunk + continue elif isinstance(chunk, SynthesizeData) or not chunk: continue @@ -158,13 +170,22 @@ async def async_iter_response( finish_reason = "stop" if finish_reason is None else finish_reason + if usage is None: + usage = Usage(prompt_tokens=0, completion_tokens=idx, total_tokens=idx) + if stream: - yield ChatCompletionChunk.model_construct(None, finish_reason, completion_id, int(time.time())) + yield ChatCompletionChunk.model_construct( + None, finish_reason, completion_id, int(time.time()), + usage=usage.get_dict() + ) else: if response_format is not None and "type" in response_format: if response_format["type"] == "json_object": content = filter_json(content) - yield ChatCompletion.model_construct(content, finish_reason, completion_id, int(time.time())) + yield ChatCompletion.model_construct( + content, finish_reason, completion_id, int(time.time()), + usage=usage.get_dict(), **filter_none(tool_calls=tool_calls) + ) finally: await safe_aclose(response) diff --git a/g4f/client/stubs.py b/g4f/client/stubs.py index 8f3425de9b4..f1d07fe703d 100644 --- a/g4f/client/stubs.py +++ b/g4f/client/stubs.py @@ -36,6 +36,7 @@ class ChatCompletionChunk(BaseModel): model: str provider: Optional[str] choices: List[ChatCompletionDeltaChoice] + usage: Usage @classmethod def model_construct( @@ -43,7 +44,8 @@ def model_construct( content: str, finish_reason: str, completion_id: str = None, - created: int = None + created: int = None, + usage: Usage = None ): return super().model_construct( id=f"chatcmpl-{completion_id}" if completion_id else None, @@ -54,7 +56,8 @@ def model_construct( choices=[ChatCompletionDeltaChoice.model_construct( ChatCompletionDelta.model_construct(content), finish_reason - )] + )], + **filter_none(usage=usage) ) class ChatCompletionMessage(BaseModel): diff --git a/g4f/gui/client/index.html b/g4f/gui/client/index.html index b06da6fbada..09f27063559 100644 --- a/g4f/gui/client/index.html +++ b/g4f/gui/client/index.html @@ -36,6 +36,7 @@ import llamaTokenizer from "llama-tokenizer-js" +