From f0d096b4d68a6f100204c259304a60fa207ae21f Mon Sep 17 00:00:00 2001 From: Nite07 Date: Wed, 18 Sep 2024 11:29:01 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=8E=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/docker.yml | 30 ++++++++ Dockerfile | 20 ++++++ README.md | 1 + app.py | 136 +++++++++++++++++++++++++++++++++++ requirements.txt | 5 ++ 5 files changed, 192 insertions(+) create mode 100644 .github/workflows/docker.yml create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 app.py create mode 100644 requirements.txt diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 0000000..c151c0a --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,30 @@ +name: build-docker + +on: + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Check out code + uses: actions/checkout@v4 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Build and push + uses: docker/build-push-action@v5 + with: + push: true + tags: nite07/poe2openai:latest + platforms: linux/amd64,linux/arm64,linux/arm/v7 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..83f48b6 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,20 @@ +# Use an official Python runtime as a parent image +FROM python:3.9-slim + +# Set the working directory in the container +WORKDIR /app + +# Copy the current directory contents into the container at /app +COPY . /app + +# Install any needed packages specified in requirements.txt +RUN pip install --no-cache-dir -r requirements.txt + +# Make port 10000 available to the world outside this container +EXPOSE 10000 + +# Define environment variable +ENV PORT=10000 + +# Run app.py when the container launches +CMD ["python", "app.py"] \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..6fa7215 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +source: https://linux.do/t/topic/98971 diff --git a/app.py b/app.py new file mode 100644 index 0000000..15b9237 --- /dev/null +++ b/app.py @@ -0,0 +1,136 @@ +# Poe Documentation: https://creator.poe.com/docs/server-bots-functional-guides +# OpenAI Documentation: https://platform.openai.com/docs/api-reference/chat/create + +# Get Environment Variables +import os + +DEFAULT_MODEL = os.getenv("BOT", default="GPT-4o") +LISTEN_PORT = int(os.getenv("PORT", default=10000)) +BASE_URL = os.getenv("BASE", default="https://api.poe.com/bot/") + +# Proxy Server +import uvicorn +import json + +from fastapi import FastAPI, Request, Header +from fastapi.middleware.cors import CORSMiddleware +from fastapi.responses import StreamingResponse +from typing import Any, AsyncGenerator +from fastapi_poe.types import ProtocolMessage +from fastapi_poe.client import get_bot_response +from fastapi.middleware.cors import CORSMiddleware + +app = FastAPI() + +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], # Allow all origins + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + + +def openai_format_messages_to_poe_format(openai_format_messages: list) -> list: + """Convert OpenAI formatted messages to POE formatted messages.""" + poe_format_messages = [ + # Convert 'assistant' to 'bot' or we get an error + ProtocolMessage( + role=msg["role"].lower().replace("assistant", "bot"), + content=msg["content"], + temperature=msg.get("temperature", 0.5), + ) + for msg in openai_format_messages + ] + return poe_format_messages + + +async def get_poe_bot_stream_partials( + api_key: str, poe_format_messages: list, bot_name: str +) -> AsyncGenerator[str, None]: + async for partial in get_bot_response( + messages=poe_format_messages, + bot_name=bot_name, + api_key=api_key, + base_url=BASE_URL, + skip_system_prompt=False, + ): + yield partial.text + + +# by @OvO +async def adaptive_streamer( + poe_bot_stream_partials_generator, is_sse_enabled=False +) -> AsyncGenerator[str, Any]: + + STREAM_PREFIX = 'data:{"id":"chatcmpl-1","object":"chat.completion.chunk","created":1,"model":"a","choices":[{"index":0,"delta":{"content":' + STREAM_SUFFIX = "}}]}\n\n" + + ENDING_CHUNK = 'data: {"id":"chatcmpl-123","object":"chat.completion.chunk","created":1694268190,"model":"gpt-4","choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}\n\ndata: [DONE]\n\n' + + NON_STREAM_PREFIX = '{"id":"chatcmpl-123","object":"chat.completion","created":1694268190,"model":"gpt-4","choices":[{"index":0,"message":{"role":"assistant","content":"' + NON_STREAM_SUFFIX = '"},"logprobs":null,"finish_reason":"stop"}],"usage":{"prompt_tokens":0,"completion_tokens":0,"total_tokens":0},"system_fingerprint":"abc"}\n\n' + + if is_sse_enabled: + chat_prefix, chat_suffix = STREAM_PREFIX, STREAM_SUFFIX + _json_dumps = lambda data: json.dumps(data) + else: + chat_prefix, chat_suffix = "", "" + _json_dumps = lambda data: json.dumps(data)[1:-1] + yield NON_STREAM_PREFIX + + async for partial in poe_bot_stream_partials_generator: + try: + yield chat_prefix + yield _json_dumps(partial) + yield chat_suffix + except: + continue + + if is_sse_enabled: + yield ENDING_CHUNK + else: + yield NON_STREAM_SUFFIX + + return + + +@app.post("/v1/chat/completions") +async def chat_completions( + request: Request, authorization: str = Header(None) +) -> StreamingResponse: + + # Assuming the header follows the standard format: "Bearer $API_KEY" + api_key = authorization.split(" ")[1] + body = await request.json() + + # Extract bot_name (model) and messages from the request body + bot_name = body.get("model", DEFAULT_MODEL) + openai_format_messages = body.get("messages", []) + is_stream = body.get("stream", False) + + # Convert OpenAI formatted messages to POE formatted messages + poe_format_messages = openai_format_messages_to_poe_format(openai_format_messages) + + # Get poe bot response + poe_bot_stream_partials_generator = get_poe_bot_stream_partials( + api_key, poe_format_messages, bot_name + ) + + return StreamingResponse( + adaptive_streamer(poe_bot_stream_partials_generator, is_stream), + media_type=( + ("text/event-stream" if is_stream else "application/json") + + ";charset=UTF-8" + ), + ) + + +if __name__ == "__main__": + try: + import uvloop + except ImportError: + uvloop = None + if uvloop: + uvloop.install() + uvicorn.run(app, host="0.0.0.0", port=LISTEN_PORT) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..3763327 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +fastapi +uvicorn +fastapi-poe +python-dotenv +uvloop \ No newline at end of file