4c9ed3c24b
Complete rewrite from Python/FastAPI to Go/Gin: - Go backend: auth (API keys + bcrypt), llama.cpp subprocess manager, hot-swap multi-model, rate limiting, quota system, webhooks - Vue 3 SPA admin panel (src/) with Tailwind CSS - Deployment: Docker multi-stage, docker-compose, nginx, systemd - GORM/SQLite models: ApiKey, Model, UsageLog, Quota, Webhook - REST API: /api/v1/admin/* (keys, models, chat, usage, health) - Embedded frontend via go:embed (build output at web/dist/) Removed legacy Python artifacts (app/, tests/, pyproject.toml, etc.)
9.4 KiB
9.4 KiB
LlamaLink — Especificación Técnica (Living Document)
Este documento se actualiza conforme evoluciona la implementación. Última actualización: implementación inicial.
1. Descripción General
LlamaLink es un servidor middleware/gateway que permite ejecutar modelos de lenguaje (LLMs) de forma local usando llama.cpp, exponiéndolos de manera segura a través de una API REST propia protegida con API keys, permitiendo así el acceso remoto controlado a la inferencia.
Objetivos principales
- Ejecutar modelos GGUF localmente mediante llama.cpp.
- Exponer un endpoint HTTP compatible con el estándar OpenAI (
/v1/chat/completions). - Autenticar y autorizar clientes remotos mediante API keys.
- Controlar uso, límites y seguridad del acceso a los modelos.
2. Arquitectura
Cliente remoto (con API key)
│
▼
┌─────────────────────┐
│ LlamaLink API │ ← Gateway (FastAPI)
│ - Autenticación │
│ - Rate limiting │
│ - Enrutamiento │
│ - Logs y métricas │
└──────────┬───────────┘
│ (proxy HTTP)
▼
┌─────────────────────┐
│ llama.cpp │ ← Motor de inferencia
│ (llama-server) │
└─────────────────────┘
Integración con llama.cpp
- Opción A (implementada): LlamaLink actúa como gateway/proxy delante del servidor HTTP nativo de llama.cpp (
llama-server). - Opción B: Uso de
llama-cpp-pythonembebido (futuro).
Multi-modelo
- Se soporta la configuración de múltiples modelos en la base de datos.
- Solo un modelo está activo/cargado en llama-server en un momento dado.
- Si llega una request para un modelo diferente al activo:
- Se rechaza con HTTP 503 +
Retry-Aftermientras dura el swap - El admin puede disparar el swap manualmente vía
POST /v1/models/{name}/load - Ocurre automáticamente en lazy mode si
MANAGE_LLAMA_SERVER=true
- Se rechaza con HTTP 503 +
- El swap es secuencial (un
asyncio.Lockevita swaps concurrentes).
3. Stack Tecnológico
| Componente | Tecnología | Justificación |
|---|---|---|
| Motor de inferencia | llama.cpp (C++) | Ya compilado y optimizado, no requiere reescritura |
| API Gateway | Python 3.11+ / FastAPI | Desarrollo rápido, ecosistema maduro, buena documentación automática (OpenAPI/Swagger) |
| Servidor ASGI | Uvicorn | Estándar para FastAPI, soporta async y alto rendimiento |
| Base de datos | SQLite (MVP) → PostgreSQL (producción) | Escalable según necesidad |
| ORM | SQLAlchemy 2.0 (async) | Manejo de modelos y migraciones |
| Autenticación | API Keys con hash SHA-256 | Nunca se almacenan en texto plano |
| Rate limiting | slowapi (memoria o Redis) | Control de abuso por IP |
| Reverse proxy / TLS | Nginx o Caddy | HTTPS obligatorio en producción |
| Contenedores | Docker + docker-compose | Despliegue reproducible |
4. Estructura de Carpetas
llamalink/
├── app/
│ ├── main.py # Punto de entrada FastAPI + lifespan
│ ├── config.py # Configuración pydantic-settings
│ ├── __init__.py
│ ├── core/
│ │ ├── __init__.py
│ │ ├── security.py # Hash/verify API keys (SHA-256)
│ │ ├── errors.py # JSON error handlers estandarizados
│ │ └── logging.py # Structured logging
│ ├── db/
│ │ ├── __init__.py
│ │ ├── base.py # SQLAlchemy declarative base
│ │ ├── session.py # Engine + session factory
│ │ └── models.py # ORM models
│ ├── schemas/
│ │ ├── __init__.py
│ │ ├── common.py # ErrorResponse, HealthResponse
│ │ ├── chat.py # OpenAI-compat request/response
│ │ └── keys.py # Key management schemas
│ ├── auth/
│ │ ├── __init__.py
│ │ ├── service.py # CRUD keys, hashing
│ │ └── dependencies.py # Bearer auth FastAPI deps
│ ├── routers/
│ │ ├── __init__.py
│ │ ├── chat.py # /v1/chat/completions
│ │ ├── keys.py # /v1/keys CRUD
│ │ └── health.py # /health, /ready
│ └── services/
│ ├── __init__.py
│ ├── llama_proxy.py # httpx async client → llama-server
│ ├── rate_limiter.py # slowapi setup
│ ├── usage_tracker.py # Token counting + DB logs
│ ├── quota.py # Chequeo de cuota mensual
│ ├── webhook.py # Notificaciones al superar cuota
│ ├── model_manager.py # Subprocess lifecycle + state machine
│ └── models_registry.py # Mapa de modelos configurados
├── migrations/ # Alembic
├── deploy/ # Nginx, Caddy, systemd units
├── docker/ # Dockerfiles
├── scripts/ # CLI tools
├── tests/
├── pyproject.toml
├── .env.example
└── README.md
5. Endpoints Implementados
| Método | Endpoint | Descripción | Auth |
|---|---|---|---|
| POST | /v1/chat/completions |
Enviar prompt y recibir respuesta del modelo | Sí |
| POST | /v1/keys |
Crear nueva API key | Sí (admin) |
| GET | /v1/keys |
Listar API keys existentes | Sí (admin) |
| DELETE | /v1/keys/{key_id} |
Revocar una API key | Sí (admin) |
| GET | /health |
Verificar estado del servicio | No |
| GET | /ready |
Verificar listo para servir | No |
Pendiente de implementar (Fase 4+)
| Método | Endpoint | Descripción |
|---|---|---|
| GET | /v1/models |
Listar modelos configurados |
| GET | /v1/models/active |
Mostrar cuál está cargado |
| POST | /v1/models/{name}/load |
Cargar/swap a un modelo |
| GET | /v1/usage/{key_id} |
Métricas de uso de una key |
| GET/DELETE | /admin/* |
Panel web de administración |
6. Variables de Entorno
LLAMALINK_ENV=development
LLAMALINK_HOST=0.0.0.0
LLAMALINK_PORT=8000
LLAMALINK_SECRET_KEY=changeme
DATABASE_URL=sqlite+aiosqlite:///./llamalink.db
MANAGE_LLAMA_SERVER=true
LLAMA_SERVER_BIN=llama-server
LLAMA_SERVER_HOST=127.0.0.1
LLAMA_SERVER_PORT=8080
LLAMA_SERVER_STARTUP_TIMEOUT=120
MODEL_SWAP_COOLDOWN=2
LLAMA_SERVER_STOP_TIMEOUT=10
RATE_LIMIT_PER_MINUTE=60
RATE_LIMIT_STORAGE=memory
LOG_LEVEL=INFO
LOG_FORMAT=json
ADMIN_API_KEY=changeme
7. Modelo de Datos
ApiKey
id,name,hashed_key,key_prefixscopes: CSV de permisos (chat,models,usage,admin)is_active,is_admin,created_at,last_used_at,owner_label
Model
id,name,model_path,aliasctx_size,n_gpu_layers,extra_args(JSON)is_default,is_enabled,is_active,loaded_at
UsageLog
api_key_id,model_name,endpointprompt_tokens,completion_tokens,total_tokenslatency_ms,status,ip_address,user_agenterror_message,created_at
Quota
api_key_id,model_scope(None = global)period_start,period_endtokens_limit,tokens_used
Webhook
api_key_id,url,event,secret(HMAC)is_active,created_at
8. Estados del ModelManager
STOPPED ──load()──> LOADING ──success──> READY
↑ │
│ fail
│ ↓
└──<─────── FAILED FAILED
│
└──swap()──> SWAPPING ──success──> READY
9. Formato de Errores
Todos los errores siguen este formato:
{
"error": {
"code": "error_code_string",
"message": "Descripción legible",
"retry_after_seconds": 30
}
}
Códigos: invalid_api_key, api_key_revoked, rate_limit_exceeded, model_not_found, model_loading, quota_exceeded, upstream_error, internal_error, validation_error, unauthorized
10. Roadmap de Implementación
✅ Fase 0 — Esqueleto
- pyproject.toml, config, main, db, .env.example, .gitignore
✅ Fase 1 — MVP
- Auth (hash, service, deps)
- llama_proxy (httpx async, streaming)
- /v1/chat/completions (non-stream + stream)
- /v1/keys CRUD
- /health, /ready
- Error handlers estandarizados
- Quota service (stub)
- Rate limiter (stub)
🔄 Fase 2 — Endurecimiento
- Rate limiting (slowapi)
- Usage tracker middleware
- Logging estructurado JSON
📋 Fase 3 — Docker + Producción
- docker-compose.yml
- Dockerfile.api, Dockerfile.llama
- Nginx.conf, Caddyfile
- systemd units
📋 Fase 4 — Multi-modelo + Streaming
- model_manager (subprocess lifecycle, state machine)
- streaming en /v1/chat/completions
- /v1/models CRUD
- Lazy swap + admin trigger
- 503 + Retry-After durante swap
📋 Fase 5 — Cuotas + Webhooks
- Quota enforcement middleware
- Webhook delivery con HMAC
- Background task para notificaciones
📋 Fase 6 — Panel Admin Web
- Jinja2 templates + htmx
- /admin/* routes
- Crear/revocar keys desde UI
- Ver métricas y logs
📋 Fase 7 — Polish
- Validación estricta de inputs
- Auto-revoke en actividad sospechosa
- Cobertura de tests ≥ 80%
- ruff + mypy limpios