Spaces:
Runtime error
Runtime error
| import os | |
| import logging | |
| from typing import Optional | |
| from datetime import datetime | |
| import time | |
| from fastapi import FastAPI, HTTPException | |
| from fastapi.middleware.cors import CORSMiddleware | |
| from pydantic import BaseModel, Field | |
| import torch | |
| from transformers import AutoTokenizer, AutoModelForCausalLM | |
| # Logging | |
| logging.basicConfig(level=logging.INFO) | |
| logger = logging.getLogger(__name__) | |
| app = FastAPI(title="Gemma 4 Inference API") | |
| # CORS | |
| app.add_middleware( | |
| CORSMiddleware, | |
| allow_origins=["*"], | |
| allow_credentials=True, | |
| allow_methods=["*"], | |
| allow_headers=["*"], | |
| ) | |
| # Config | |
| MODEL_NAME = os.getenv("MODEL_NAME", "google/gemma-4-E4B-it") | |
| # Global model/tokenizer | |
| model = None | |
| tokenizer = None | |
| # Pydantic models | |
| class Message(BaseModel): | |
| role: str | |
| content: str | |
| class ChatRequest(BaseModel): | |
| 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) | |
| class ChatChoice(BaseModel): | |
| index: int | |
| message: Message | |
| finish_reason: str | |
| class ChatUsage(BaseModel): | |
| completion_tokens: int | |
| total_tokens: int | |
| class ChatResponse(BaseModel): | |
| model: str | |
| object: str = "chat.completion" | |
| created: int | |
| choices: list[ChatChoice] | |
| usage: ChatUsage | |
| def load_model(): | |
| """Load model and tokenizer on startup.""" | |
| global model, tokenizer | |
| logger.info(f"Loading {MODEL_NAME}...") | |
| tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME) | |
| # Load with 4-bit quantization to fit in 16GB | |
| model = AutoModelForCausalLM.from_pretrained( | |
| MODEL_NAME, | |
| device_map="auto", | |
| torch_dtype=torch.bfloat16, | |
| load_in_4bit=True, | |
| low_cpu_mem_usage=True, | |
| ) | |
| logger.info(f"✓ {MODEL_NAME} loaded successfully") | |
| async def startup(): | |
| load_model() | |
| def health(): | |
| return {"status": "ok", "model": MODEL_NAME} | |
| def list_models(): | |
| return { | |
| "object": "list", | |
| "data": [ | |
| { | |
| "id": "gemma-4", | |
| "object": "model", | |
| "owned_by": "google", | |
| "created": int(time.time()), | |
| } | |
| ] | |
| } | |
| def chat_completions(request: ChatRequest): | |
| """OpenAI-compatible chat completions endpoint.""" | |
| try: | |
| # Build prompt from messages | |
| prompt = "" | |
| for msg in request.messages: | |
| if msg.role == "system": | |
| prompt += f"<|system|>\n{msg.content}<|end_of_turn|>\n" | |
| elif msg.role == "user": | |
| prompt += f"<|user|>\n{msg.content}<|end_of_turn|>\n" | |
| elif msg.role == "assistant": | |
| prompt += f"<|assistant|>\n{msg.content}<|end_of_turn|>\n" | |
| prompt += "<|assistant|>\n" | |
| # Tokenize | |
| inputs = tokenizer(prompt, return_tensors="pt").to(model.device) | |
| input_length = inputs.input_ids.shape[1] | |
| # Generate | |
| start_time = time.time() | |
| outputs = model.generate( | |
| **inputs, | |
| max_new_tokens=request.max_tokens, | |
| temperature=request.temperature, | |
| top_p=request.top_p, | |
| do_sample=request.temperature > 0, | |
| pad_token_id=tokenizer.eos_token_id, | |
| ) | |
| # Decode | |
| full_text = tokenizer.decode(outputs[0], skip_special_tokens=True) | |
| # Extract just the response | |
| if "<|assistant|>" in full_text: | |
| response_text = full_text.split("<|assistant|>")[-1].strip() | |
| else: | |
| response_text = full_text | |
| tokens_generated = outputs.shape[1] - input_length | |
| return ChatResponse( | |
| model="gemma-4", | |
| created=int(time.time()), | |
| choices=[ | |
| ChatChoice( | |
| index=0, | |
| message=Message(role="assistant", content=response_text), | |
| finish_reason="stop", | |
| ) | |
| ], | |
| usage=ChatUsage( | |
| completion_tokens=tokens_generated, | |
| total_tokens=tokens_generated, | |
| ), | |
| ) | |
| except Exception as e: | |
| logger.error(f"Error: {e}") | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| def chat_completions_no_v1(request: ChatRequest): | |
| """Alias without /v1/ prefix.""" | |
| return chat_completions(request) | |
| def root(): | |
| return { | |
| "name": "Gemma 4 API", | |
| "model": MODEL_NAME, | |
| "docs": "Use /v1/chat/completions for OpenAI compatibility", | |
| "example": { | |
| "url": "/v1/chat/completions", | |
| "method": "POST", | |
| "body": { | |
| "messages": [{"role": "user", "content": "Hello"}], | |
| "temperature": 0.7, | |
| "max_tokens": 512, | |
| } | |
| } | |
| } | |
| if __name__ == "__main__": | |
| import uvicorn | |
| uvicorn.run(app, host="0.0.0.0", port=7860) |