gemma4 / app.py
Nodiw52992's picture
Update app.py
ea09d40 verified
Raw
History Blame Contribute Delete
14.6 kB
"""
Gemma 4 Supa-Fast Dual-API Server for HuggingFace Spaces (CPU, 16GB RAM)
Uses llama.cpp (GGUF) instead of transformers/PyTorch.
OpenAI: POST /v1/chat/completions
Anthropic: POST /v1/messages
"""
from __future__ import annotations
import os
import sys
import json
import time
import uuid
import asyncio
from contextlib import asynccontextmanager
from dataclasses import dataclass, field
from typing import AsyncGenerator, Dict, List, Literal, Optional, Union, Any
import huggingface_hub
from llama_cpp import Llama, CreateChatCompletionStreamResponse
from fastapi import FastAPI, HTTPException, Request, status
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse, StreamingResponse
from pydantic import BaseModel, Field
# ─────────────────────────── Configuration ───────────────────────────
MODEL_REPO = os.getenv("MODEL_REPO", "ggml-org/gemma-4-E4B-it-GGUF")
QUANT = os.getenv("QUANT", "Q5_K_M") # Q4_K_M is slightly faster; Q5_K_M is higher quality
MAX_CTX = int(os.getenv("MAX_CTX", "8192"))
N_THREADS = int(os.getenv("N_THREADS", str(os.cpu_count() or 2)))
N_BATCH = int(os.getenv("N_BATCH", "512"))
PORT = int(os.getenv("PORT", "7860"))
GGUF_FILENAME = f"gemma-4-E4B-it-{QUANT}.gguf"
# ─────────────────────────── Global State ────────────────────────────
@dataclass
class ModelState:
llm: Optional[Llama] = None
ready: bool = False
load_error: Optional[str] = None
load_time_sec: float = 0.0
STATE = ModelState()
# ─────────────────────────── Model Loading ───────────────────────────
def load_model() -> None:
"""Download GGUF (if needed) and load into llama.cpp."""
global STATE
start = time.time()
print(f"[INIT] Repo: {MODEL_REPO}")
print(f"[INIT] Quant: {QUANT}")
print(f"[INIT] Threads: {N_THREADS} | Ctx: {MAX_CTX} | Batch: {N_BATCH}")
try:
print("[INIT] Downloading / verifying GGUF...")
model_path = huggingface_hub.hf_hub_download(
repo_id=MODEL_REPO,
filename=GGUF_FILENAME,
local_files_only=False,
)
print(f"[INIT] GGUF path: {model_path}")
print("[INIT] Loading model into llama.cpp (mmap + mlock)...")
llm = Llama(
model_path=model_path,
n_ctx=MAX_CTX,
n_threads=N_THREADS,
n_batch=N_BATCH,
use_mmap=True, # memory-mapped = instant load, OS pages in on demand
use_mlock=True, # prevent swapping to disk
verbose=False,
)
elapsed = time.time() - start
STATE.llm = llm
STATE.ready = True
STATE.load_time_sec = elapsed
print(f"[INIT] Ready in {elapsed:.1f}s")
print(f"[INIT] Model size: ~{os.path.getsize(model_path) / 1e9:.1f} GB on disk")
except Exception as e:
STATE.load_error = str(e)
STATE.ready = False
print(f"[ERROR] {e}")
import traceback
traceback.print_exc()
# ─────────────────────────── Pydantic Schemas ────────────────────────
class OAIChatMessage(BaseModel):
role: Literal["system", "user", "assistant", "tool"] = "user"
content: str = ""
name: Optional[str] = None
class OAIChatRequest(BaseModel):
model: str = "gemma-4"
messages: List[OAIChatMessage]
temperature: Optional[float] = Field(default=0.7, ge=0.0, le=2.0)
top_p: Optional[float] = Field(default=0.9, ge=0.0, le=1.0)
max_tokens: Optional[int] = Field(default=2048, ge=1, le=8192)
stream: bool = False
stop: Optional[Union[str, List[str]]] = None
class OAIChoice(BaseModel):
index: int = 0
message: dict = Field(default_factory=dict)
finish_reason: Optional[str] = None
class OAIChunkChoice(BaseModel):
index: int = 0
delta: dict = Field(default_factory=dict)
finish_reason: Optional[str] = None
class OAIUsage(BaseModel):
prompt_tokens: int = 0
completion_tokens: int = 0
total_tokens: int = 0
class OAIChatResponse(BaseModel):
id: str = Field(default_factory=lambda: f"chatcmpl-{uuid.uuid4().hex[:12]}")
object: str = "chat.completion"
created: int = Field(default_factory=lambda: int(time.time()))
model: str = "gemma-4"
choices: List[OAIChoice]
usage: OAIUsage
class OAIChatStreamChunk(BaseModel):
id: str = ""
object: str = "chat.completion.chunk"
created: int = 0
model: str = "gemma-4"
choices: List[OAIChunkChoice]
# Anthropic schemas
class AnthropicMessageParam(BaseModel):
role: Literal["user", "assistant"] = "user"
content: str = ""
class AnthropicChatRequest(BaseModel):
model: str = "claude-sonnet-4"
messages: List[AnthropicMessageParam]
max_tokens: int = Field(default=4096, ge=1, le=8192)
system: Optional[str] = None
temperature: Optional[float] = Field(default=0.7, ge=0.0, le=1.0)
top_p: Optional[float] = Field(default=0.9, ge=0.0, le=1.0)
stream: bool = False
stop_sequences: Optional[List[str]] = None
class AnthropicUsage(BaseModel):
input_tokens: int = 0
output_tokens: int = 0
class AnthropicMessageResponse(BaseModel):
id: str = Field(default_factory=lambda: f"msg_{uuid.uuid4().hex[:24]}")
type: str = "message"
role: str = "assistant"
model: str = "gemma-4"
content: List[dict]
stop_reason: Optional[str] = "end_turn"
usage: AnthropicUsage
# ──────────────────────── Format Conversion ──────────────────────────
def _to_llama_messages(
messages: List[OAIChatMessage],
system: Optional[str] = None,
) -> List[Dict[str, str]]:
"""Convert to llama.cpp chat format."""
out: List[Dict[str, str]] = []
if system:
out.append({"role": "system", "content": system})
for m in messages:
out.append({"role": m.role, "content": m.content})
return out
# ─────────────────────────── FastAPI App ─────────────────────────────
@asynccontextmanager
async def lifespan(app: FastAPI):
print("[LIFESPAN] Starting up...")
loop = asyncio.get_event_loop()
await loop.run_in_executor(None, load_model)
yield
print("[LIFESPAN] Shutting down...")
app = FastAPI(
title="Gemma 4 Supa-Fast Server",
description="llama.cpp GGUF backend β€” OpenAI + Anthropic endpoints",
version="2.0.0",
lifespan=lifespan,
)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
@app.get("/")
def root():
return {
"status": "ok" if STATE.ready else "loading",
"model": MODEL_REPO,
"quant": QUANT,
"version": "2.0.0",
"endpoints": [
"POST /v1/chat/completions",
"POST /v1/messages",
"GET /health",
],
}
@app.get("/health")
def health():
return {
"status": "healthy" if STATE.ready else "loading",
"model_loaded": STATE.ready,
"load_time_sec": STATE.load_time_sec,
"error": STATE.load_error,
}
@app.get("/v1/models")
def list_models():
return {
"object": "list",
"data": [
{
"id": "gemma-4",
"object": "model",
"created": int(time.time()),
"owned_by": "google",
}
],
}
# ── OpenAI-compatible endpoint ──
@app.post("/v1/chat/completions")
async def openai_chat_completions(request: OAIChatRequest):
if not STATE.ready:
raise HTTPException(
status_code=503,
detail=f"Model not loaded. Error: {STATE.load_error}",
)
messages = _to_llama_messages(request.messages)
stop = request.stop or []
if isinstance(stop, str):
stop = [stop]
gen_kwargs = dict(
messages=messages,
max_tokens=request.max_tokens,
temperature=request.temperature,
top_p=request.top_p,
stop=stop,
stream=request.stream,
)
if request.stream:
return StreamingResponse(
_openai_stream(gen_kwargs),
media_type="text/event-stream",
)
# Non-streaming
t0 = time.time()
output = STATE.llm.create_chat_completion(**gen_kwargs)
elapsed = time.time() - t0
choice = output["choices"][0]
usage = output.get("usage", {})
return OAIChatResponse(
choices=[
OAIChoice(
index=0,
message=choice["message"],
finish_reason=choice.get("finish_reason", "stop"),
)
],
usage=OAIUsage(
prompt_tokens=usage.get("prompt_tokens", 0),
completion_tokens=usage.get("completion_tokens", 0),
total_tokens=usage.get("total_tokens", 0),
),
)
async def _openai_stream(gen_kwargs: dict):
"""SSE stream for OpenAI format."""
req_id = f"chatcmpl-{uuid.uuid4().hex[:12]}"
created = int(time.time())
# Role chunk
first = OAIChatStreamChunk(
id=req_id,
created=created,
choices=[OAIChunkChoice(index=0, delta={"role": "assistant"}, finish_reason=None)],
)
yield f"data: {first.model_dump_json()}\n\n"
content_buf = ""
prompt_tokens = 0
completion_tokens = 0
# llama.cpp streaming generator
stream = STATE.llm.create_chat_completion(**gen_kwargs)
for chunk in stream:
delta = chunk["choices"][0]["delta"]
finish = chunk["choices"][0].get("finish_reason")
if delta.get("content"):
content_buf += delta["content"]
completion_tokens += 1
out_chunk = OAIChatStreamChunk(
id=req_id,
created=created,
choices=[OAIChunkChoice(index=0, delta={"content": delta["content"]}, finish_reason=None)],
)
yield f"data: {out_chunk.model_dump_json()}\n\n"
if finish:
final = OAIChatStreamChunk(
id=req_id,
created=created,
choices=[OAIChunkChoice(index=0, delta={}, finish_reason=finish)],
)
yield f"data: {final.model_dump_json()}\n\n"
break
usage = {
"id": req_id,
"object": "chat.completion.chunk",
"created": created,
"model": "gemma-4",
"choices": [],
"usage": {
"prompt_tokens": prompt_tokens,
"completion_tokens": completion_tokens,
"total_tokens": prompt_tokens + completion_tokens,
},
}
yield f"data: {json.dumps(usage)}\n\n"
yield "data: [DONE]\n\n"
# ── Anthropic-compatible endpoint ──
@app.post("/v1/messages")
async def anthropic_messages(request: AnthropicChatRequest):
if not STATE.ready:
raise HTTPException(status_code=503, detail=f"Model not loaded. {STATE.load_error}")
# Flatten Anthropic format β†’ OpenAI format β†’ llama.cpp format
messages: List[Dict[str, str]] = []
if request.system:
messages.append({"role": "system", "content": request.system})
for m in request.messages:
messages.append({"role": m.role, "content": m.content})
stop = request.stop_sequences or []
gen_kwargs = dict(
messages=messages,
max_tokens=request.max_tokens,
temperature=request.temperature,
top_p=request.top_p,
stop=stop,
stream=request.stream,
)
if request.stream:
return StreamingResponse(
_anthropic_stream(gen_kwargs),
media_type="text/event-stream",
headers={"x-api-version": "2023-06-01"},
)
output = STATE.llm.create_chat_completion(**gen_kwargs)
text = output["choices"][0]["message"]["content"]
usage = output.get("usage", {})
return AnthropicMessageResponse(
content=[{"type": "text", "text": text}],
stop_reason="end_turn",
usage=AnthropicUsage(
input_tokens=usage.get("prompt_tokens", 0),
output_tokens=usage.get("completion_tokens", 0),
),
)
async def _anthropic_stream(gen_kwargs: dict):
msg_id = f"msg_{uuid.uuid4().hex[:24]}"
yield f"event: message_start\ndata: {json.dumps({'type': 'message_start', 'message': {'id': msg_id, 'type': 'message', 'role': 'assistant', 'model': 'gemma-4', 'content': [], 'stop_reason': None, 'stop_sequence': None, 'usage': {'input_tokens': 0, 'output_tokens': 0}}})}\n\n"
yield f"event: content_block_start\ndata: {json.dumps({'type': 'content_block_start', 'index': 0, 'content_block': {'type': 'text', 'text': ''}})}\n\n"
stream = STATE.llm.create_chat_completion(**gen_kwargs)
output_tokens = 0
for chunk in stream:
delta = chunk["choices"][0]["delta"]
if delta.get("content"):
output_tokens += 1
payload = {
"type": "content_block_delta",
"index": 0,
"delta": {"type": "text_delta", "text": delta["content"]},
}
yield f"event: content_block_delta\ndata: {json.dumps(payload)}\n\n"
yield f"event: content_block_stop\ndata: {json.dumps({'type': 'content_block_stop', 'index': 0})}\n\n"
yield f"event: message_delta\ndata: {json.dumps({'type': 'message_delta', 'delta': {'stop_reason': 'end_turn', 'stop_sequence': None}, 'usage': {'output_tokens': output_tokens}})}\n\n"
yield f"event: message_stop\ndata: {json.dumps({'type': 'message_stop'})}\n\n"
# ─────────────────────────── Entry Point ─────────────────────────────
if __name__ == "__main__":
import uvicorn
print("=" * 60)
print(" Gemma 4 Supa-Fast Server (llama.cpp GGUF)")
print(" OpenAI: POST /v1/chat/completions")
print(" Anthropic: POST /v1/messages")
print(f" Repo: {MODEL_REPO} | Quant: {QUANT}")
print(f" Port: {PORT}")
print("=" * 60)
uvicorn.run("app:app", host="0.0.0.0", port=PORT, workers=1)