# 📊 Analytics — klaude-proxy
## 🤔 ¿Qué hago? ¿Cómo lo hago? ¿Y para qué lo hago?
**¿Qué hago?**
Registro cada petición que pasa por el proxy — tanto cache HITs como MISSes — con sus tokens consumidos y el coste estimado en USD. Los datos sobreviven a reinicios del contenedor gracias a SQLite.
**¿Cómo lo hago?**
El módulo `proxy/analytics.py` mantiene dos capas de datos:
- **Agregados de sesión** en memoria (`_SessionStats`): contadores que se resetean al reiniciar el proxy. Actualizados de forma síncrona en cada petición.
- **Persistencia histórica** en SQLite (`/data/analytics.db`): cada petición se escribe en background de forma no bloqueante, protegida con `asyncio.Lock`.
**¿Y para qué lo hago?**
Para dar visibilidad real del coste de inferencia — cuánto se gasta en Anthropic, cuánto se ahorra gracias a la caché — sin depender de dashboards externos ni APIs de billing de terceros.
---
## 🧮 Modelo de costes
El coste se calcula en base al pricing de Anthropic (mid-2026, USD por millón de tokens):
| Modelo | Input ($/M tok) | Output ($/M tok) |
| --- | --- | --- |
| `claude-haiku-4-5-20251001` | $0.80 | $4.00 |
| `claude-haiku-4-5` | $0.80 | $4.00 |
| `claude-sonnet-4-6` | $3.00 | $15.00 |
| `claude-sonnet-4-5` | $3.00 | $15.00 |
| `claude-opus-4-8` | $15.00 | $75.00 |
| `claude-opus-4-5` | $15.00 | $75.00 |
| _(desconocido)_ | $3.00 | $15.00 |
Los cache **HITs** tienen coste real $0 — pero el sistema calcula el **coste ahorrado** (lo que habría costado si hubiera ido a Anthropic).
---
## 🗄️ Esquema SQLite
```sql
CREATE TABLE requests (
id INTEGER PRIMARY KEY AUTOINCREMENT,
ts TEXT NOT NULL, -- ISO8601 UTC
model TEXT NOT NULL,
source TEXT NOT NULL, -- "cache" | "anthropic"
input_tokens INTEGER NOT NULL DEFAULT 0,
output_tokens INTEGER NOT NULL DEFAULT 0,
cost_usd REAL NOT NULL DEFAULT 0.0,
request_preview TEXT -- primeros 400 chars de la petición
);
```
**Ruta en contenedor:** `/data/analytics.db`
**Volumen Podman:** `analytics-data` → montado en `/data`
**Resultado:** los datos persisten entre `podman-compose up/down`.
---
## 📡 Endpoints REST
### `GET /analytics/stats`
Devuelve los agregados de la sesión actual (desde el último reinicio):
```json
{
"requests_total": 142,
"cache_hits": 98,
"provider_calls_out": 44,
"provider_calls_in": 44,
"cost_real_usd": 0.012840,
"cost_saved_usd": 0.041200,
"tokens_in_total": 18420,
"tokens_out_total": 62300,
"by_model": {
"claude-haiku-4-5-20251001": {
"hits": 98, "misses": 44, "cost_usd": 0.012840,
"tokens_in": 18420, "tokens_out": 62300
}
}
}
```
---
### `GET /analytics/requests?n=50`
Devuelve las últimas N peticiones de SQLite (por defecto 50), ordenadas por más reciente primero:
```json
[
{
"ts": "2026-07-14T09:30:00+00:00",
"model": "claude-haiku-4-5-20251001",
"source": "anthropic",
"input_tokens": 1240,
"output_tokens": 420,
"cost_usd": 0.002668,
"request_preview": "¿Cuál es la arquitectura de klaude-proxy?..."
}
]
```
---
### `GET /analytics/history`
Agrega todas las peticiones históricas en SQLite (acumulado de todas las sesiones):
```json
{
"total_requests": 1842,
"total_tokens_in": 284200,
"total_tokens_out": 920100,
"total_cost_usd": 0.198440,
"total_cache_hits": 1320,
"total_provider_calls": 522
}
```
---
## 🔄 Flujo de registro
```mermaid
flowchart TD
A[Petición completada\ncache HIT o MISS] --> B[record_request\nasync]
B --> C[Actualizar _SessionStats\nen memoria]
C --> D{source?}
D -->|cache| E[cache_hits++\ncost_saved_usd += estimado]
D -->|anthropic| F[provider_calls_out/in++\ncost_real_usd += real]
E --> G[asyncio.create_task\npersistir en SQLite]
F --> G
G --> H{_db_lock\nasyncio.Lock}
H --> I[INSERT INTO requests\nbest-effort]
I -->|excepción| J[log.warning\nno bloquea respuesta]
I -->|ok| K[commit]
subgraph Dashboard
L[GET /analytics/stats] --> C
M[GET /analytics/requests] --> I
N[GET /analytics/history] --> I
end
```
---
## 🧩 Componentes internos
| Símbolo | Tipo | Propósito |
| --- | --- | --- |
| `_PRICING` | `dict` | Tabla de precios por modelo |
| `_DEFAULT_PRICING` | `dict` | Fallback para modelos desconocidos |
| `DB_PATH` | `Path` | `/data/analytics.db` |
| `_db_lock` | `asyncio.Lock` | Serializa escrituras SQLite |
| `_conn` | `sqlite3.Connection` | Conexión reutilizable (singleton) |
| `_SessionStats` | `@dataclass` | Agregados en memoria de la sesión |
| `_session` | instancia | El objeto singleton de sesión |
| `calculate_cost()` | `fn` | Calcula coste USD para un par (modelo, tokens) |
| `record_request()` | `async fn` | Punto de entrada — actualiza sesión y persiste |
| `get_session_stats()` | `fn` | Devuelve dict para `/analytics/stats` |
| `get_recent_requests()` | `fn` | Devuelve list para `/analytics/requests` |
| `get_historical_stats()` | `fn` | Devuelve dict para `/analytics/history` |
---
## ⚙️ Configuración de infraestructura
El volumen `analytics-data` se declara en `podman/compose.yaml`:
```yaml
services:
proxy:
volumes:
- analytics-data:/data
volumes:
analytics-data:
driver: local
```
Para inspeccionar la base de datos directamente:
```bash
# Entrar al contenedor
podman exec -it klaus-proxy sh
# Consultar SQLite
sqlite3 /data/analytics.db "SELECT * FROM requests ORDER BY id DESC LIMIT 10;"
# Totales
sqlite3 /data/analytics.db \
"SELECT source, COUNT(*), SUM(cost_usd) FROM requests GROUP BY source;"
```
---
## 🔗 Documentos relacionados
- [dashboard.md](dashboard.md) — Dashboard web que consume estos datos
- [proxy.md](proxy.md) — Referencia completa de endpoints
- [architecture.md](architecture.md) — Visión general del sistema