nayara-v1 / source /api.py
sriharithebest's picture
Upload folder using huggingface_hub
0990c3f verified
Raw History Blame Contribute Delete
5.74 kB
from __future__ import annotations
import json
import os
import secrets
import threading
import time
import uuid
from contextlib import asynccontextmanager
from typing import Any, Literal
from fastapi import Depends, FastAPI, Header, HTTPException
from fastapi.concurrency import run_in_threadpool
from fastapi.responses import StreamingResponse
from pydantic import BaseModel, Field
from tokenizers import Tokenizer
from nayara.artifacts import materialize_checkpoint
from nayara.generate import load_model, stream_generate
class CompletionRequest(BaseModel):
model: str = "nayara-v1"
prompt: str
max_tokens: int = Field(192, ge=1, le=512)
temperature: float = Field(0.2, ge=0, le=2)
top_k: int = Field(40, ge=1, le=32000)
stream: bool = False
class ChatMessage(BaseModel):
role: Literal["system", "user", "assistant"]
content: str
class ChatRequest(BaseModel):
model: str = "nayara-v1"
messages: list[ChatMessage]
max_tokens: int = Field(192, ge=1, le=512)
temperature: float = Field(0.2, ge=0, le=2)
top_k: int = Field(40, ge=1, le=32000)
stream: bool = False
runtime: dict[str, Any] = {"ready": False}
generation_lock = threading.Lock()
MODEL_ID = os.environ.get("NAYARA_MODEL_ID", "nayara-v1")
def _authorize(authorization: str | None = Header(default=None)) -> None:
expected = os.environ.get("NAYARA_API_KEY")
if not expected:
return
supplied = authorization.removeprefix("Bearer ") if authorization else ""
if not secrets.compare_digest(supplied, expected):
raise HTTPException(status_code=401, detail="invalid bearer token")
def _chat_prompt(messages: list[ChatMessage]) -> str:
if not messages or not any(message.role == "user" for message in messages):
raise HTTPException(status_code=422, detail="messages must contain at least one user message")
parts: list[str] = []
for message in messages:
if message.role == "system":
parts.append(f"<|user|>\nSystem guidance: {message.content.strip()}")
else:
parts.append(f"<|{message.role}|>\n{message.content.strip()}")
parts.append("<|assistant|>\n")
return "\n".join(parts)
def _generate(prompt: str, request: CompletionRequest | ChatRequest):
return stream_generate(
runtime["model"], runtime["tokenizer"], prompt,
max_new_tokens=request.max_tokens, temperature=request.temperature, top_k=request.top_k,
)
def _complete(prompt: str, request: CompletionRequest | ChatRequest) -> str:
final = ""
with generation_lock:
for final in _generate(prompt, request):
pass
return final
def _sse(prompt: str, request: CompletionRequest | ChatRequest, chat: bool):
request_id = f"nayara-{uuid.uuid4().hex}"
previous = ""
with generation_lock:
for partial in _generate(prompt, request):
delta, previous = partial[len(previous) :], partial
if chat:
payload = {"id": request_id, "object": "chat.completion.chunk", "model": MODEL_ID, "choices": [{"index": 0, "delta": {"content": delta}, "finish_reason": None}]}
else:
payload = {"id": request_id, "object": "text_completion", "model": MODEL_ID, "choices": [{"index": 0, "text": delta, "finish_reason": None}]}
yield f"data: {json.dumps(payload)}\n\n"
yield "data: [DONE]\n\n"
@asynccontextmanager
async def lifespan(_: FastAPI):
if os.environ.get("NAYARA_REQUIRE_API_KEY") == "1" and not os.environ.get("NAYARA_API_KEY"):
raise RuntimeError("NAYARA_REQUIRE_API_KEY=1 but NAYARA_API_KEY is unset")
checkpoint = await run_in_threadpool(materialize_checkpoint)
threads = int(os.environ.get("NAYARA_THREADS", max(1, (os.cpu_count() or 4) // 2)))
runtime["tokenizer"] = Tokenizer.from_file(str(checkpoint / "tokenizer.json"))
runtime["model"] = await run_in_threadpool(load_model, checkpoint, True, threads)
runtime.update({"ready": True, "checkpoint": str(checkpoint), "loaded_at": time.time()})
yield
runtime.clear()
runtime["ready"] = False
app = FastAPI(title="NAYARA API", version="1.0.0", lifespan=lifespan)
@app.get("/healthz")
def health() -> dict[str, str]:
return {"status": "ok"}
@app.get("/readyz")
def ready() -> dict[str, Any]:
if not runtime.get("ready"):
raise HTTPException(status_code=503, detail="model is not loaded")
return {"status": "ready", "model": MODEL_ID, "loaded_at": runtime["loaded_at"]}
@app.get("/v1/models", dependencies=[Depends(_authorize)])
def models() -> dict[str, Any]:
return {"object": "list", "data": [{"id": MODEL_ID, "object": "model", "owned_by": "nayara"}]}
@app.post("/v1/completions", dependencies=[Depends(_authorize)])
async def completions(request: CompletionRequest):
prompt = request.prompt
if request.stream:
return StreamingResponse(_sse(prompt, request, False), media_type="text/event-stream")
text = await run_in_threadpool(_complete, prompt, request)
return {"id": f"nayara-{uuid.uuid4().hex}", "object": "text_completion", "model": MODEL_ID, "choices": [{"index": 0, "text": text, "finish_reason": "stop"}]}
@app.post("/v1/chat/completions", dependencies=[Depends(_authorize)])
async def chat_completions(request: ChatRequest):
prompt = _chat_prompt(request.messages)
if request.stream:
return StreamingResponse(_sse(prompt, request, True), media_type="text/event-stream")
text = await run_in_threadpool(_complete, prompt, request)
return {"id": f"nayara-{uuid.uuid4().hex}", "object": "chat.completion", "model": MODEL_ID, "choices": [{"index": 0, "message": {"role": "assistant", "content": text}, "finish_reason": "stop"}]}