Saltar al contenido
# 📊 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