Backend en FastAPI que actúa como proxy inteligente para llama-server (llama.cpp), exponiendo una API compatible con OpenAI para un asistente virtual en PWA.
Diseñado para producción con streaming SSE, arquitectura desacoplada y despliegue con Podman + systemd + Caddy.
- 🔌 API compatible con OpenAI: Endpoints estándar
/v1/chat/completionsy/v1/models - ⚡ Streaming SSE nativo: Respuestas en tiempo real desde el LLM
- 🐳 Desacoplado por capas: LLM en el host (systemd), API en contenedor (Podman)
- 🚀 Máximo rendimiento: Binario nativo con acceso directo a GPU
- 🔒 Seguro: Usuario no-root en contenedor, secrets fuera de la imagen
- 📦 Moderno: Gestión de paquetes con
uv, Python 3.12, tipado estricto - 🛡️ HTTPS automático: Caddy como proxy inverso con Let's Encrypt
┌────────────────────────────────────────────────────────┐
│ TU SERVIDOR │
├────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌─────────────────────┐ │
│ │ CADDY │───────▶│ FastAPI (Podman) │ │
│ │ :80 / :443 │ :8000 │ puerto 8000 │ │
│ │ (systemd) │ │ usuario: appuser │ │
│ └──────────────┘ └──────────┬──────────┘ │
│ │ │
│ │ HTTP │
│ │ host.containers │
│ │ .internal:8080 │
│ ▼ │
│ ┌──────────────────────────────────────────────┐ │
│ │ llama-server (systemd - binario nativo) │ │
│ │ puerto 8080 │ GPU acceso directo │ │
│ └──────────────────────────────────────────────┘ │
│ │
└────────────────────────────────────────────────────────┘
¿Por qué esta arquitectura?
- llama-server en systemd: Máximo rendimiento con acceso directo a GPU/CPU sin overhead de contenedor
- FastAPI en Podman: Aislamiento del código Python, fácil despliegue y reproducibilidad
- Caddy como proxy inverso: HTTPS automático, HTTP/3, y manejo de conexiones persistentes para streaming
- Linux (probado en Debian/Ubuntu, Fedora)
- Podman >= 4.0 (no Docker, para
host.containers.internal) - Caddy >= 2.7
- Python 3.12+ (solo para desarrollo local)
- uv >= 0.4 (gestor de paquetes)
- llama.cpp compilado o binario oficial descargado
- GPU con drivers (NVIDIA CUDA, AMD ROCm, o Vulkan) - opcional pero recomendado
# Fedora/RHEL
sudo dnf install podman caddy
# Debian/Ubuntu
sudo apt install podman caddy
# Instalar uv
curl -LsSf https://astral.sh/uv/install.sh | shPWA-virtual-assistant-backend/
├── app/
│ ├── api/
│ │ └── endpoints.py # Rutas FastAPI (chat, models, health)
│ ├── core/
│ │ ├── config.py # Configuración con pydantic-settings
│ │ └── schemas.py # Modelos Pydantic (compatibles OpenAI)
│ ├── services/
│ │ └── llm_service.py # Cliente HTTP hacia llama-server
│ └── main.py # App FastAPI + middlewares
├── Containerfile # Imagen Podman optimizada con uv
├── pyproject.toml # Dependencias del proyecto
├── uv.lock # Lockfile reproducible
├── .python-version # 3.12
└── README.md
Crea un archivo .env en la raíz del proyecto (ya está en .gitignore):
# URL del llama-server en el host
LLAMA_SERVER_URL=http://host.containers.internal:8080
# Timeout para respuestas del LLM (segundos)
LLAMA_TIMEOUT=300
# API Key opcional para proteger el endpoint
API_KEY=tu-super-secreto-aqui
# CORS - dominios permitidos (cambia en producción)
CORS_ORIGINS=["https://tu-dominio.com"]💡 Nota:
host.containers.internales el hostname especial de Podman que resuelve al host desde dentro del contenedor.
git clone https://github.com/BufferRing/PWA-virtual-assistant-backend.git
cd PWA-virtual-assistant-backend
uv sync./llama-server \
--model /ruta/a/tu/modelo.gguf \
--host 127.0.0.1 \
--port 8080 \
--ctx-size 4096 \
--n-gpu-layers 99# Para desarrollo local, usa 127.0.0.1 en vez de host.containers.internal
LLAMA_SERVER_URL=http://127.0.0.1:8080 uv run uvicorn app.main:app --reloadLa API estará en http://127.0.0.1:8000 y la documentación interactiva en http://127.0.0.1:8000/v1/openapi.json.
uv run ruff check . # Linter
uv run ruff format . # Formatter
uv run pytest # TestsCrea el archivo /etc/systemd/system/llama-server.service:
[Unit]
Description=LLama.cpp Server
After=network.target
[Service]
Type=simple
User=llama-user
Group=llama-user
WorkingDirectory=/opt/llama-server
ExecStart=/opt/llama-server/llama-server \
--model /ruta/a/tu/modelo.gguf \
--host 0.0.0.0 \
--port 8080 \
--ctx-size 4096 \
--n-gpu-layers 99 \
--threads 8
Restart=on-failure
RestartSec=5s
LimitNOFILE=65536
Environment="CUDA_VISIBLE_DEVICES=0"
[Install]
WantedBy=multi-user.target
⚠️ Importante:--host 0.0.0.0es necesario para que el contenedor de FastAPI pueda conectarse.
Activar el servicio:
sudo systemctl daemon-reload
sudo systemctl enable --now llama-server
sudo systemctl status llama-server# Construir imagen
podman build -t fastapi-app:latest -f Containerfile .
# Detener contenedor anterior si existe
podman stop fastapi-app 2>/dev/null || true
podman rm fastapi-app 2>/dev/null || true
# Ejecutar nuevo contenedor
podman run -d \
--name fastapi-app \
--env-file .env \
--network slirp4netns:allow_host_loopback=true \
-p 127.0.0.1:8000:8000 \
--restart=always \
fastapi-app:latestFlags clave explicados:
--env-file .env: Inyecta las variables sin embeberlas en la imagen--network slirp4netns:allow_host_loopback=true: Habilitahost.containers.internal-p 127.0.0.1:8000:8000: Solo accesible desde localhost (Caddy)--restart=always: Reinicia si crashea
mkdir -p ~/.config/systemd/user/
podman generate systemd --new --name fastapi-app > ~/.config/systemd/user/fastapi-app.service
systemctl --user daemon-reload
systemctl --user enable --now fastapi-appEdita /etc/caddy/Caddyfile:
tu-dominio.com {
reverse_proxy localhost:8000 {
# Crítico para streaming SSE
flush_interval -1
# Timeouts largos para respuestas de LLM
transport http {
keepalive 300s
keepalive_idle_conns 10
}
}
request_body {
max_size 10MB
}
}Recargar Caddy:
sudo systemctl reload caddyEnvía mensajes al LLM y recibe respuestas (compatible con OpenAI).
Request:
{
"model": "tu-modelo",
"messages": [
{"role": "system", "content": "Eres un asistente útil."},
{"role": "user", "content": "Hola, ¿cómo estás?"}
],
"temperature": 0.7,
"max_tokens": 512,
"stream": true
}Response (streaming):
data: {"id":"chatcmpl-...","choices":[{"delta":{"content":"¡Hola"}}]}
data: {"id":"chatcmpl-...","choices":[{"delta":{"content":"!"}}]}
data: [DONE]
Lista los modelos disponibles en llama-server.
Healthcheck para Caddy. Devuelve:
{"status": "healthy", "llm_server": "connected"}Endpoint raíz de verificación.
- Verifica que llama-server escuche en
0.0.0.0:8080(no127.0.0.1) - Prueba desde dentro del contenedor:
podman exec -it fastapi-app curl http://host.containers.internal:8080/health - Revisa que el contenedor tenga el flag
--network slirp4netns:allow_host_loopback=true
Asegúrate de que tu Caddyfile tenga flush_interval -1. Sin esto, Caddy buferiza las respuestas SSE y el cliente no recibe los chunks en tiempo real.
Verifica los logs:
podman logs fastapi-app
journalctl -u llama-server -fMIT © 2026 BufferRing. Ver LICENSE para más detalles.
Las contribuciones son bienvenidas. Por favor, abre un issue primero para discutir cambios importantes.
Hecho con ❤️ usando FastAPI, llama.cpp, Podman y Caddy.