Artificial Intelligence (AI)

How to build an OpenAI-compatible API | by Saar Berkovich | Mar, 2024

Written by smirow

Wir beginnen mit der Implementierung des No-Streaming-Bits. Beginnen wir mit der Modellierung unserer Abfrage:

from typing import List, Optional

from pydantic import BaseModel

class ChatMessage(BaseModel):
role: str
content: str

class ChatCompletionRequest(BaseModel):
model: str = "mock-gpt-model"
messages: List[ChatMessage]
max_tokens: Optional[int] = 512
temperature: Optional[float] = 0.1
stream: Optional[bool] = False

Das PyDantic-Modell stellt die Anfrage des Kunden dar und zielt darauf ab, die API-Referenz zu replizieren. Der Kürze halber implementiert diese Vorlage nicht die gesamte Spezifikation, sondern nur die wesentlichen Elemente, die für ihre Funktionsfähigkeit erforderlich sind. Wenn Ihnen ein Parameter fehlt, der Teil der API-Spezifikationen ist (z. B top_p), können Sie es einfach zur Vorlage hinzufügen.

DER ChatCompletionRequest modelliert die Parameter, die OpenAI in seinen Abfragen verwendet. Die Chat-API-Spezifikation erfordert die Angabe einer Liste von ChatMessage (Wie bei einem Chat-Verlauf ist der Kunde normalerweise dafür verantwortlich, ihn zu pflegen und bei jeder Anfrage bereitzustellen). Jede Chat-Nachricht hat eine role Attribut (normalerweise system, assistant Oder user ) und ein content Attribut, das den tatsächlichen Text der Nachricht enthält.

Als Nächstes schreiben wir unseren FastAPI-Chat-Abschlussendpunkt:

import time

from fastapi import FastAPI

app = FastAPI(title="OpenAI-compatible API")

@app.post("/chat/completions")
async def chat_completions(request: ChatCompletionRequest):

if request.messages and request.messages[0].role == 'user':
resp_content = "As a mock AI Assitant, I can only echo your last message:" + request.messages[-1].content
else:
resp_content = "As a mock AI Assitant, I can only echo your last message, but there were no messages!"

return {
"id": "1337",
"object": "chat.completion",
"created": time.time(),
"model": request.model,
"choices": [{
"message": ChatMessage(role="assistant", content=resp_content)
}]
}

Es ist einfach.

Testen Sie unsere Implementierung

Angenommen, beide Codeblöcke befinden sich in einer Datei namens main.pyWir werden zwei Python-Bibliotheken in der Umgebung unserer Wahl installieren (es ist immer besser, eine neue zu erstellen): pip install fastapi openai und starten Sie den Server von einem Terminal aus:

uvicorn main:app

Mit einem anderen Terminal (oder durch Starten des Servers im Hintergrund) öffnen wir eine Python-Konsole und kopieren und fügen den folgenden Code ein, der direkt aus der Python-Client-Referenz von OpenAI stammt:

from openai import OpenAI

# init client and connect to localhost server
client = OpenAI(
api_key="fake-api-key",
base_url="http://localhost:8000" # change the default port if needed
)

# call API
chat_completion = client.chat.completions.create(
messages=[
{
"role": "user",
"content": "Say this is a test",
}
],
model="gpt-1337-turbo-pro-max",
)

# print the top "choice"
print(chat_completion.choices[0].message.content)

Wenn Sie alles richtig gemacht haben, sollte die Serverantwort korrekt gedruckt werden. Auch eine Besichtigung lohnt sich chat_completion Objekt, um zu sehen, dass alle relevanten Attribute so sind, wie sie von unserem Server gesendet wurden. Sie sollten etwa Folgendes sehen:

Mit Carbon formatierter Autorencode

Da die LLM-Generierung tendenziell langsam (rechenintensiv) ist, lohnt es sich, den generierten Inhalt an den Client zurückzusenden, damit der Benutzer die generierte Antwort sehen kann, ohne auf deren Abschluss warten zu müssen. Wenn Sie sich erinnern, haben wir gegeben ChatCompletionRequest ein boolescher Wert stream Eigenschaft – Dies ermöglicht es dem Client, die Rücksendung der Daten anzufordern, anstatt sie alle auf einmal zu senden.

Das macht die Sache etwas komplexer. Wir werden eine Generatorfunktion erstellen, um unsere Dummy-Antwort zu verpacken (in einem realen Szenario benötigen wir einen Generator, der mit unserer LLM-Generierung verbunden ist).

import asyncio
import json

async def _resp_async_generator(text_resp: str):
# let's pretend every word is a token and return it over time
tokens = text_resp.split(" ")

for i, token in enumerate(tokens):
chunk = {
"id": i,
"object": "chat.completion.chunk",
"created": time.time(),
"model": "blah",
"choices": [{"delta": {"content": token + " "}}],
}
yield f"data: {json.dumps(chunk)}\n\n"
await asyncio.sleep(1)
yield "data: [DONE]\n\n"

Und jetzt würden wir unseren ursprünglichen Endpunkt so ändern, dass er eine StreamingResponse zurückgibt, wenn stream==True

import time

from starlette.responses import StreamingResponse

app = FastAPI(title="OpenAI-compatible API")

@app.post("/chat/completions")
async def chat_completions(request: ChatCompletionRequest):

if request.messages:
resp_content = "As a mock AI Assitant, I can only echo your last message:" + request.messages[-1].content
else:
resp_content = "As a mock AI Assitant, I can only echo your last message, but there wasn't one!"
if request.stream:
return StreamingResponse(_resp_async_generator(resp_content), media_type="application/x-ndjson")

return {
"id": "1337",
"object": "chat.completion",
"created": time.time(),
"model": request.model,
"choices": [{
"message": ChatMessage(role="assistant", content=resp_content) }]
}

Testen der Streaming-Implementierung

Nach dem Neustart des uvicorn-Servers öffnen wir eine Python-Konsole und fügen diesen Code ein (wiederum aus der Dokumentation der OpenAI-Bibliothek entnommen).

from openai import OpenAI

# init client and connect to localhost server
client = OpenAI(
api_key="fake-api-key",
base_url="http://localhost:8000" # change the default port if needed
)

stream = client.chat.completions.create(
model="mock-gpt-model",
messages=[{"role": "user", "content": "Say this is a test"}],
stream=True,
)
for chunk in stream:
print(chunk.choices[0].delta.content or "")

Sie sollten sehen, dass jedes Wort der Serverantwort langsam gedruckt wird und so die Token-Generierung nachahmt. Wir können den letzten inspizieren chunk Objekt sehen etwa so:

Mit Carbon formatierter Autorencode

Fügen Sie alles zusammen

Schließlich können Sie im Kern unten den gesamten Servercode sehen.

About the author

smirow

Leave a Comment