From cf2cff84defd2eb4ab9425988dd23964c252042d Mon Sep 17 00:00:00 2001 From: LanQian <5499636+LanQian528@users.noreply.github.com> Date: Mon, 22 Apr 2024 21:42:25 +0800 Subject: [PATCH] feat support not stream dalle and tools fix headers --- README.md | 5 +- chatgpt/ChatService.py | 196 ++++++++++++++++++++++++++++------------ chatgpt/reverseProxy.py | 55 ++++++++++- utils/authorization.py | 12 +-- 4 files changed, 199 insertions(+), 69 deletions(-) diff --git a/README.md b/README.md index 3ff0bc6..3a442f8 100644 --- a/README.md +++ b/README.md @@ -22,13 +22,12 @@ > - [x] 配置 BASE_URL > - [x] 重试次数设置 > - [x] ArkoseToken -> - [x] GPT4.0 画图、工具 (仅流式,测试阶段) +> - [x] GPT4.0 画图、工具 (测试中,可能有bug) > - [x] 使用 RefreshToken 代替 AccessToken -> - [x] 反向代理 UI (http://127.0.0.1:5005) +> - [x] 反向代理 UI (http://127.0.0.1:5005, 不支持登录) > > TODO > -> - [ ] GPT4.0 画图、工具(非流式) > - [ ] GPTs ## Deploy diff --git a/chatgpt/ChatService.py b/chatgpt/ChatService.py index 135da98..35d1e8b 100644 --- a/chatgpt/ChatService.py +++ b/chatgpt/ChatService.py @@ -6,8 +6,7 @@ import uuid from fastapi import HTTPException -from api.chat_completions import num_tokens_from_messages, model_system_fingerprint, model_proxy, \ - split_tokens_from_content +from api.chat_completions import num_tokens_from_messages, model_system_fingerprint, model_proxy, num_tokens_from_content from chatgpt.proofofwork import calc_proof_token from utils.Client import Client from utils.Logger import Logger @@ -27,6 +26,7 @@ async def stream_response(service, response, model, max_tokens): last_recipient = None end = False message_id = None + all_text = "" async for chunk in response.aiter_lines(): chunk = chunk.decode("utf-8") if end: @@ -94,7 +94,10 @@ async def stream_response(service, response, model, max_tokens): Logger.debug(f"asset_pointer: {asset_pointer}") image_download_url = await service.get_image_download_url(asset_pointer) Logger.debug(f"image_download_url: {image_download_url}") - delta = {"content": f"\n```\n![image]({image_download_url})\n"} + if image_download_url: + delta = {"content": f"\n```\n![image]({image_download_url})\n"} + else: + delta = {"content": f"\n```\nFailed to load the image.\n"} elif not message.get("end_turn") or not message.get("metadata").get("finish_details"): message_id = None len_last_content = 0 @@ -105,6 +108,7 @@ async def stream_response(service, response, model, max_tokens): end = True else: continue + all_text += delta.get("content", "") chunk_new_data = { "id": chat_id, "object": "chat.completion.chunk", @@ -127,23 +131,119 @@ async def stream_response(service, response, model, max_tokens): continue -async def chat_response(resp, model, prompt_tokens, max_tokens): - last_resp = None - for i in reversed(resp): - if i != "data: [DONE]" and i.startswith("data: "): - try: - last_resp = json.loads(i[6:]) - if not last_resp.get("message"): - raise +async def chat_response(service, response, prompt_tokens, model, max_tokens): + chat_id = f"chatcmpl-{''.join(random.choice(string.ascii_letters + string.digits) for _ in range(29))}" + system_fingerprint_list = model_system_fingerprint.get(model, None) + system_fingerprint = random.choice(system_fingerprint_list) if system_fingerprint_list else None + created_time = int(time.time()) + finish_reason = "stop" + completion_tokens = -1 + len_last_content = 0 + last_content_type = None + last_recipient = None + end = False + message_id = None + all_text = "" + async for chunk in response.aiter_lines(): + chunk = chunk.decode("utf-8") + if end: + break + try: + if chunk == "data: [DONE]": break - except Exception: - Logger.error(f"Error: {i}") + elif not chunk.startswith("data: "): continue - usage, system_fingerprint, finish_reason, message = await init_param(last_resp, max_tokens, model, prompt_tokens) + else: + chunk_old_data = json.loads(chunk[6:]) + finish_reason = None + message = chunk_old_data.get("message", {}) + status = message.get("status") + content = message.get("content", {}) + recipient = message.get("recipient", "") + if not message and chunk_old_data.get("type") == "moderation": + delta = {"role": "assistant", "content": moderation_message} + finish_reason = "stop" + end = True + elif status == "in_progress": + outer_content_type = content.get("content_type") + if outer_content_type == "text": + part = content.get("parts", [])[0] + if not part: + message_id = message.get("id") + new_text = "" + else: + if message_id != message.get("id"): + continue + new_text = part[len_last_content:] + len_last_content = len(part) + else: + text = content.get("text", "") + if outer_content_type == "code" and last_content_type != "code": + new_text = "\n```" + recipient + "\n" + text[len_last_content:] + elif outer_content_type == "execution_output" and last_content_type != "execution_output": + new_text = "\n```" + "Output" + "\n" + text[len_last_content:] + else: + new_text = text[len_last_content:] + len_last_content = len(text) + if last_content_type == "code" and outer_content_type != "code": + new_text = "\n```\n" + new_text + elif last_content_type == "execution_output" and outer_content_type != "execution_output": + new_text = "\n```\n" + new_text + if recipient == "dalle.text2im" and last_recipient != "dalle.text2im": + new_text = "\n```" + "json" + "\n" + new_text + delta = {"content": new_text} + last_content_type = outer_content_type + last_recipient = recipient + if completion_tokens >= max_tokens: + delta = {} + finish_reason = "length" + end = True + elif status == "finished_successfully": + if content.get("content_type") == "multimodal_text": + parts = content.get("parts", []) + delta = {} + for part in parts: + inner_content_type = part.get('content_type') + if inner_content_type == "image_asset_pointer": + last_content_type = "image_asset_pointer" + asset_pointer = part.get('asset_pointer').replace('file-service://', '') + Logger.debug(f"asset_pointer: {asset_pointer}") + image_download_url = await service.get_image_download_url(asset_pointer) + Logger.debug(f"image_download_url: {image_download_url}") + if image_download_url: + delta = {"content": f"\n```\n![image]({image_download_url})\n"} + else: + delta = {"content": f"\n```\nFailed to load the image.\n"} + elif not message.get("end_turn") or not message.get("metadata").get("finish_details"): + message_id = None + len_last_content = 0 + continue + else: + delta = {} + finish_reason = "stop" + end = True + else: + continue + all_text += delta.get("content", "") + completion_tokens += 1 + except Exception: + Logger.error(f"Error: {chunk}") + continue + + completion_tokens = num_tokens_from_content(all_text, model) + message = { + "role": "assistant", + "content": all_text, + } + usage = { + "prompt_tokens": prompt_tokens, + "completion_tokens": completion_tokens, + "total_tokens": prompt_tokens + completion_tokens + } return { - "id": f"chatcmpl-{''.join(random.choice(string.ascii_letters + string.digits) for _ in range(29))}", + "id": chat_id, "object": "chat.completion", - "created": int(time.time()), + "created": created_time, "model": model, "choices": [ { @@ -158,29 +258,6 @@ async def chat_response(resp, model, prompt_tokens, max_tokens): } -async def init_param(last_resp, max_tokens, model, prompt_tokens): - if last_resp.get("type") == "moderation": - message_content = moderation_message - completion_tokens = 53 - finish_reason = "stop" - else: - message_content = last_resp["message"]["content"]["parts"][0] - message_content, completion_tokens, finish_reason = split_tokens_from_content(message_content, max_tokens, - model) - message = { - "role": "assistant", - "content": message_content, - } - usage = { - "prompt_tokens": prompt_tokens, - "completion_tokens": completion_tokens, - "total_tokens": prompt_tokens + completion_tokens - } - system_fingerprint_list = model_system_fingerprint.get(model, None) - system_fingerprint = random.choice(system_fingerprint_list) if system_fingerprint_list else None - return usage, system_fingerprint, finish_reason, message - - def api_messages_to_chat(api_messages): chat_messages = [] for api_message in api_messages: @@ -248,6 +325,8 @@ class ChatService: turnstile = resp.get('turnstile', {}) arkose_required = arkose.get('required') if arkose_required: + if not self.arkose_token_url: + raise HTTPException(status_code=403, detail="Arkose service required") arkose_dx = arkose.get("dx") arkose_client = Client() try: @@ -258,7 +337,7 @@ class ChatService: ) self.arkose_token = r2.json()['token'] except Exception as e: - raise HTTPException(status_code=403, detail="Arkose required") + raise HTTPException(status_code=403, detail="Failed to get Arkose token") proofofwork_required = proofofwork.get('required') if proofofwork_required: @@ -341,33 +420,36 @@ class ChatService: async def send_conversation_for_stream(self): url = f'{self.base_url}/conversation' model = model_proxy.get(self.model, self.model) - r = await self.s.post(url, headers=self.headers, json=self.chat_request, timeout=600, stream=True) - if r.status_code == 200: - return stream_response(self, r, model, self.max_tokens) - else: - rtext = await r.atext() - if "application/json" == r.headers.get("Content-Type", ""): - detail = json.loads(rtext).get("detail", json.loads(rtext)) + try: + r = await self.s.post(url, headers=self.headers, json=self.chat_request, timeout=600, stream=True) + if r.status_code == 200: + return stream_response(self, r, model, self.max_tokens) else: - detail = rtext - if r.status_code != 200: - if r.status_code == 403: - raise HTTPException(status_code=r.status_code, detail="cf-please-wait") - raise HTTPException(status_code=r.status_code, detail=detail) + rtext = await r.atext() + if "application/json" == r.headers.get("Content-Type", ""): + detail = json.loads(rtext).get("detail", json.loads(rtext)) + else: + detail = rtext + if r.status_code != 200: + if r.status_code == 403: + raise HTTPException(status_code=r.status_code, detail="cf-please-wait") + raise HTTPException(status_code=r.status_code, detail=detail) + except HTTPException as e: + raise HTTPException(status_code=e.status_code, detail=str(e)) async def send_conversation(self): url = f'{self.base_url}/conversation' model = model_proxy.get(self.model, self.model) try: - r = await self.s.post(url, headers=self.headers, json=self.chat_request, timeout=600) + r = await self.s.post(url, headers=self.headers, json=self.chat_request, timeout=600, stream=True) if r.status_code == 200: - rtext = r.text.split("\n") - return await chat_response(rtext, model, self.prompt_tokens, self.max_tokens) + return await chat_response(self, r, self.prompt_tokens, model, self.max_tokens) else: + rtext = await r.atext() if "application/json" == r.headers.get("Content-Type", ""): - detail = r.json().get("detail", r.json()) + detail = json.loads(rtext).get("detail", json.loads(rtext)) else: - detail = r.content + detail = rtext if r.status_code == 403: raise HTTPException(status_code=r.status_code, detail="cf-please-wait") raise HTTPException(status_code=r.status_code, detail=detail) diff --git a/chatgpt/reverseProxy.py b/chatgpt/reverseProxy.py index 856b1af..e70d389 100644 --- a/chatgpt/reverseProxy.py +++ b/chatgpt/reverseProxy.py @@ -6,6 +6,57 @@ from utils.Client import Client from utils.config import chatgpt_base_url_list, proxy_url_list +headers_reject_list = [ + "x-real-ip", + "x-forwarded-for", + "x-forwarded-proto", + "x-forwarded-port", + "x-forwarded-host", + "x-forwarded-server", + "cf-warp-tag-id", + "cf-visitor", + "cf-ray", + "cf-connecting-ip", + "cf-ipcountry", + "cdn-loop", + "remote-host", + "x-frame-options", + "x-xss-protection", + "x-content-type-options", + "content-security-policy", + "host", + "cookie", + "connection", + "content-length", + "content-encoding", + "x-middleware-prefetch", + "x-nextjs-data", + "purpose", + "x-forwarded-uri", + "x-forwarded-path", + "x-forwarded-method", + "x-forwarded-protocol", + "x-forwarded-scheme", + "cf-request-id", + "cf-worker", + "cf-access-client-id", + "cf-access-client-device-type", + "cf-access-client-device-model", + "cf-access-client-device-name", + "cf-access-client-device-brand", + "x-middleware-prefetch", + "x-forwarded-for", + "x-forwarded-host", + "x-forwarded-proto", + "x-forwarded-server", + "x-real-ip", + "x-forwarded-port", + "cf-connecting-ip", + "cf-ipcountry", + "cf-ray", + "cf-visitor", +] + async def chatgpt_reverse_proxy(request: Request, path: str): try: base_url = random.choice(chatgpt_base_url_list) @@ -23,9 +74,7 @@ async def chatgpt_reverse_proxy(request: Request, path: str): params = dict(request.query_params) headers = { key: value for key, value in request.headers.items() - if (key.lower() not in ["host", "origin", "referer", "user-agent", "authorization"] - and not - (key.lower().startswith("cf") or key.lower().startswith("cdn") or key.lower().startswith("x"))) + if (key.lower() not in ["host", "origin", "referer", "user-agent", "authorization"] and key.lower() not in headers_reject_list) } cookies = dict(request.cookies) diff --git a/utils/authorization.py b/utils/authorization.py index 2ee7e5d..f1ff94e 100644 --- a/utils/authorization.py +++ b/utils/authorization.py @@ -8,13 +8,13 @@ oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token", auto_error=False) async def verify_token(token: str = Depends(oauth2_scheme)): - if not authorization_list: + if token and token.startswith("eyJhbGciOi"): return token - elif token in authorization_list: - return token - elif token and token.startswith("eyJhbGciOi"): - return token - elif len(token) == 45: + elif token and len(token) == 45: return await rt2ac(token) + elif not authorization_list: + return token + elif token and token in authorization_list: + return token else: raise HTTPException(status_code=401, detail="Not authenticated")