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.)
286 lines
9.4 KiB
Markdown
286 lines
9.4 KiB
Markdown
# 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-python` embebido (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:
|
|
1. Se rechaza con HTTP 503 + `Retry-After` mientras dura el swap
|
|
2. El admin puede disparar el swap manualmente vía `POST /v1/models/{name}/load`
|
|
3. Ocurre automáticamente en lazy mode si `MANAGE_LLAMA_SERVER=true`
|
|
- El swap es secuencial (un `asyncio.Lock` evita 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
|
|
|
|
```env
|
|
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_prefix`
|
|
- `scopes`: CSV de permisos (`chat,models,usage,admin`)
|
|
- `is_active`, `is_admin`, `created_at`, `last_used_at`, `owner_label`
|
|
|
|
### Model
|
|
- `id`, `name`, `model_path`, `alias`
|
|
- `ctx_size`, `n_gpu_layers`, `extra_args` (JSON)
|
|
- `is_default`, `is_enabled`, `is_active`, `loaded_at`
|
|
|
|
### UsageLog
|
|
- `api_key_id`, `model_name`, `endpoint`
|
|
- `prompt_tokens`, `completion_tokens`, `total_tokens`
|
|
- `latency_ms`, `status`, `ip_address`, `user_agent`
|
|
- `error_message`, `created_at`
|
|
|
|
### Quota
|
|
- `api_key_id`, `model_scope` (None = global)
|
|
- `period_start`, `period_end`
|
|
- `tokens_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:
|
|
|
|
```json
|
|
{
|
|
"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
|