mirror-1 / app.py
Juanoto2012's picture
Update app.py
bb43e32 verified
Raw
History Blame Contribute Delete
4.78 kB
import os
import json
import gc
import asyncio
from fastapi import FastAPI, Request, HTTPException
from fastapi.responses import StreamingResponse, JSONResponse
from duckduckgo_search import DDGS
from llama_cpp import Llama
app = FastAPI(title="Optimized LLM Server")
AVAILABLE_MODELS = {
"gemma-4-e2b-it": {
"repo_id": "google/gemma-4-E2B-it-qat-q4_0-gguf",
"filename": "*q4_0-it.gguf"
},
"jan-3.5-4b": {
"repo_id": "janhq/Jan-v3.5-4B-gguf",
"filename": "*Q4_K_M.gguf"
}
}
DEFAULT_MODEL = "gemma-4-e2b-it"
active_model_id = None
llm = None
HF_TOKEN = os.environ.get("HF_TOKEN")
# SEMÁFORO: Limita a 1 ejecución simultánea del modelo para evitar que la RAM/CPU exploten.
# Las demás peticiones esperarán su turno ordenadamente sin bloquear el servidor web.
model_semaphore = asyncio.Semaphore(1)
def search_web_sync(query):
"""Función bloqueante original de búsqueda"""
try:
results = DDGS().text(query, max_results=3)
if not results:
return ""
context = "CONTEXTO DE INTERNET ACTUALIZADO:\n"
for r in results:
context += f"- {r.get('title')}: {r.get('body')}\n"
return context
except Exception:
return ""
async def search_web_async(query):
"""Ejecuta la búsqueda en un hilo separado para no bloquear el servidor"""
return await asyncio.to_thread(search_web_sync, query)
def load_model_into_memory(model_id):
global active_model_id, llm
if model_id not in AVAILABLE_MODELS:
model_id = DEFAULT_MODEL
if active_model_id == model_id:
return
if llm is not None:
del llm
gc.collect() # Liberar RAM explícitamente
model_info = AVAILABLE_MODELS[model_id]
llm = Llama.from_pretrained(
repo_id=model_info["repo_id"],
filename=model_info["filename"],
n_ctx=4096,
n_threads=2,
verbose=False
)
active_model_id = model_id
@app.get("/")
async def health_check():
return {"status": "online", "role": "mirror_worker", "backend": "FastAPI + llama.cpp"}
@app.get("/v1/models")
async def list_models():
data = [{"id": m, "object": "model"} for m in AVAILABLE_MODELS.keys()]
return {"object": "list", "data": data}
@app.post("/v1/chat/completions")
async def chat_completions(request: Request):
data = await request.json()
if not data or 'messages' not in data:
raise HTTPException(status_code=400, detail="Petición inválida")
messages = data.get('messages', [])
requested_model = data.get('model', DEFAULT_MODEL)
temperature = data.get('temperature', 0.6)
max_tokens = min(data.get('max_tokens', 1024), 2048)
stream = data.get('stream', False)
use_web_search = data.get('web_search', False)
# 1. Búsqueda web asíncrona (si se solicita)
if use_web_search and messages and messages[-1]['role'] == 'user':
user_query = messages[-1]['content']
web_context = await search_web_async(user_query)
if web_context:
messages[-1]['content'] = f"Responde usando esta info de internet si es útil:\n{web_context}\n\nPregunta: {user_query}"
# 2. Cola de inferencia protegida por el semáforo
async def generate_response():
async with model_semaphore:
# Cargamos el modelo (si es necesario) dentro del bloqueo de seguridad
await asyncio.to_thread(load_model_into_memory, requested_model)
# Ejecutamos la inferencia en un hilo separado
response = await asyncio.to_thread(
llm.create_chat_completion,
messages=messages,
max_tokens=max_tokens,
temperature=temperature,
stream=stream
)
return response
try:
response_data = await generate_response()
if stream:
async def event_generator():
# Iteramos sobre el generador síncrono en un hilo para no bloquear
def get_next_chunk():
try:
return next(response_data)
except StopIteration:
return None
while True:
chunk = await asyncio.to_thread(get_next_chunk)
if chunk is None:
break
yield f"data: {json.dumps(chunk)}\n\n"
yield "data: [DONE]\n\n"
return StreamingResponse(event_generator(), media_type="text/event-stream")
else:
return JSONResponse(content=response_data)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))