gpt / app.py
chizk's picture
Upload 7 files
e3ae57a verified
Raw
History Blame Contribute Delete
5.81 kB
# --------------------------------------------------------------
# app.py
# --------------------------------------------------------------
# 1️⃣ Load the Qwen2.5‑1.5B‑Instruct model (float16, 4‑bit quantised)
# 2️⃣ Expose two things:
# • OpenAI‑compatible endpoint → /v1/chat/completions
# • Static UI (HTML/CSS/JS) → /
# --------------------------------------------------------------
import os
import torch
from fastapi import FastAPI, HTTPException, Request
from fastapi.responses import JSONResponse, HTMLResponse, FileResponse
from fastapi.staticfiles import StaticFiles
from pydantic import BaseModel, Field
from transformers import AutoModelForCausalLM, AutoTokenizer
import uvicorn
import time
# --------------------------------------------------------------
# 1️⃣ Model loading (once, shared by all requests)
# --------------------------------------------------------------
MODEL_NAME = "Qwen/Qwen2.5-1.5B-Instruct"
print("🚀 Loading model – this may take a minute …")
tokenizer = AutoTokenizer.from_pretrained(
MODEL_NAME,
trust_remote_code=True,
use_fast=True
)
model = AutoModelForCausalLM.from_pretrained(
MODEL_NAME,
torch_dtype=torch.float16, # float16 works on the free tier (CPU‑only)
device_map="auto",
trust_remote_code=True
)
# --------------------------------------------------------------
# 2️⃣ FastAPI app
# --------------------------------------------------------------
app = FastAPI(
title="Qwen2.5‑1.5B‑Instruct (Traditional Chinese) – OpenAI‑compatible",
description="Chat endpoint (`/v1/chat/completions`) + modern web UI (`/`).",
version="0.1.0"
)
# Serve static files from ./static
app.mount("/static", StaticFiles(directory="static"), name="static")
# --------------------------------------------------------------
# 3️⃣ Pydantic models – OpenAI‑compatible request/response
# --------------------------------------------------------------
class Message(BaseModel):
role: str = Field(..., description="`system`, `user` or `assistant`")
content: str
class ChatRequest(BaseModel):
model: str = Field(default="qwen2.5-1.5b-instruct")
messages: list[Message]
temperature: float = Field(default=0.7, ge=0.0, le=2.0)
max_tokens: int = Field(default=512, ge=1, le=2048)
top_p: float = Field(default=0.9, ge=0.0, le=1.0)
stream: bool = Field(default=False)
class Choice(BaseModel):
index: int = 0
message: Message
finish_reason: str = "stop"
class Usage(BaseModel):
prompt_tokens: int
completion_tokens: int
total_tokens: int
class ChatResponse(BaseModel):
id: str = "chatcmpl-123"
object: str = "chat.completion"
created: int = Field(default_factory=lambda: int(time.time()))
model: str
choices: list[Choice]
usage: Usage
# --------------------------------------------------------------
# 4️⃣ Helper – generate a response from the model
# --------------------------------------------------------------
def _generate(request: ChatRequest) -> str:
# Convert list[Message] → the format the tokenizer expects
msgs = [{"role": m.role, "content": m.content} for m in request.messages]
# Build the prompt using the model’s chat template
prompt = tokenizer.apply_chat_template(
msgs,
tokenize=False,
add_generation_prompt=True
)
# Tokenise
inputs = tokenizer([prompt], return_tensors="pt").to(model.device)
# Generation
with torch.no_grad():
output_ids = model.generate(
**inputs,
max_new_tokens=request.max_tokens,
temperature=request.temperature,
top_p=request.top_p,
do_sample=True,
repetition_penalty=1.1,
pad_token_id=tokenizer.eos_token_id,
)
# Remove the prompt part
generated = output_ids[0][len(inputs.input_ids[0]):]
text = tokenizer.decode(generated, skip_special_tokens=True)
return text
# --------------------------------------------------------------
# 5️⃣ OpenAI‑compatible endpoint
# --------------------------------------------------------------
@app.post("/v1/chat/completions", response_model=ChatResponse)
async def chat_completions(req: ChatRequest):
try:
answer = _generate(req)
# Token counts (approximate)
prompt_tokens = len(tokenizer.encode(req.messages[-1].content))
completion_tokens = len(tokenizer.encode(answer))
total_tokens = prompt_tokens + completion_tokens
response = ChatResponse(
model=req.model,
choices=[
Choice(
index=0,
message=Message(role="assistant", content=answer),
finish_reason="stop"
)
],
usage=Usage(
prompt_tokens=prompt_tokens,
completion_tokens=completion_tokens,
total_tokens=total_tokens,
),
)
return response
except Exception as exc:
raise HTTPException(status_code=500, detail=str(exc))
# --------------------------------------------------------------
# 6️⃣ UI entry point – serve the HTML page
# --------------------------------------------------------------
@app.get("/", response_class=HTMLResponse)
async def index():
# The file lives in ./static/index.html
with open(os.path.join("static", "index.html"), encoding="utf-8") as f:
return HTMLResponse(f.read())
# --------------------------------------------------------------
# 7️⃣ Run locally (useful for debugging)
# --------------------------------------------------------------
if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=7860)