# 🏗️ Arquitectura — klaude-proxy
## 🤔 ¿Qué hago? ¿Cómo lo hago? ¿Y para qué lo hago?
**¿Qué hago?**
Soy un proxy HTTP que se interpone entre Claude Code y la API oficial de Anthropic. Cada petición pasa primero por mí antes de llegar a `api.anthropic.com`.
**¿Cómo lo hago?**
Genero un embedding semántico de cada conversación usando Fastembed local (`nomic-ai/nomic-embed-text-v1.5`, sin API externa) y lo busco en Qdrant (vector DB). Si la similitud con una conversación anterior supera el umbral configurado, devuelvo la respuesta cacheada sin llamar a Anthropic. Si no hay hit, reenvío la petición, capturo la respuesta y la almaceno para futuros hits.
**¿Y para qué lo hago?**
Para reducir el coste de llamadas a Anthropic, añadir memoria persistente semántica al trabajo diario y permitir que múltiples instancias de Claude Code compartan la misma caché.
---
## 📐 Diagrama de flujo
```mermaid
flowchart TD
A[Claude Code cliente] -->|POST /v1/messages| B[klaude-proxy :8080]
B --> C{Generar embedding\nFastembed nomic-v1.5}
C --> D{klaude_cache\nCosine search}
D -->|score ≥ 0.98\nDIRECT HIT| E[Devolver respuesta\ncacheada]
E --> K[X-Cache: HIT]
D -->|0.87 ≤ score < 0.98\nCACHE RAG| CRAG[Inyectar respuesta\ncacheada en system]
D -->|score < 0.87\nMISS| KB{klaude_knowledge\nBúsqueda híbrida\nBM25 + dense RRF}
KB -->|chunks ≥ 0.75| KRAG[Inyectar chunks KB\nen system prompt]
KB -->|sin resultados| SRC{klaude_sources\nBúsqueda densa}
SRC -->|chunks ≥ 0.75| SRAG[Inyectar chunks Sources\nen system prompt]
SRC -->|sin resultados| F_PLAIN[Forward a\napi.anthropic.com / Ollama]
CRAG --> F[Forward a\napi.anthropic.com / Ollama]
KRAG --> F
SRAG --> F
F --> G{¿Respuesta\ncacheable?}
G -->|end_turn + solo texto| H[Almacenar en Qdrant\nklaude_cache]
G -->|tool_use / no end_turn| I[Devolver sin cachear]
H --> J[Devolver respuesta]
F_PLAIN --> G
J --> L[X-Cache: MISS]
I --> L
F --> RAG_HDR[X-Cache: *-RAG\nsegún fuentes activas]
subgraph Podman
B
M[(Qdrant\nklaude_cache\nklaude_knowledge\nklaude_sources)]
end
D <--> M
KB <--> M
SRC <--> M
H --> M
```
---
## 🧩 Componentes
| Componente | Archivo | Responsabilidad |
| --- | --- | --- |
| **FastAPI app** | `proxy/main.py` | Enrutamiento, orquestación RAG, respuestas, dashboard HTML |
| **Configuración** | `proxy/config.py` | Variables de entorno con defaults seguros |
| **Modelos** | `proxy/models.py` | Pydantic schemas mirroring Anthropic API |
| **Embeddings** | `proxy/embeddings.py` | Serialización de request + Fastembed local |
| **Cache** | `proxy/cache.py` | Colección `klaude_cache`: init, search (dual-threshold), store, stats, flush |
| **Knowledge base** | `proxy/knowledge.py` | Colección `klaude_knowledge`: búsqueda híbrida BM25+dense (RRF), ingest, stats, flush |
| **Sources** | `proxy/sources.py` | Colección `klaude_sources`: ingest web/markdown/PDF, búsqueda dense, stats, flush |
| **Cliente Anthropic** | `proxy/anthropic_client.py` | Forward blocking/stream + reconstrucción SSE |
| **Cliente Ollama** | `proxy/ollama_client.py` | Forward a servidor Ollama local, compatible con Anthropic SDK |
| **Crawl4AI client** | `proxy/crawl4ai_client.py` | Crawl de URLs via Crawl4AI REST (sync + polling fallback) |
| **Analytics** | `proxy/analytics.py` | Costes, tokens, SQLite persistente, endpoints `/analytics/*` |
| **Log buffer** | `proxy/log_buffer.py` | Ring buffer de logs para SSE dashboard |
| **Podman Compose** | `podman/compose.yaml` | Orquestación Qdrant + proxy + Crawl4AI + volumen analytics-data |
---
## 🔑 Decisiones de diseño clave
### ¿Qué se cachea?
Solo respuestas con `stop_reason = "end_turn"` y content blocks exclusivamente de tipo `text`. Las respuestas con `tool_use` son **stateful** (dependen del estado actual del sistema de ficheros, output de comandos, etc.) y no se pueden reutilizar de forma segura.
### Clave de embedding
Se serializa: `model + system + messages[]` concatenados. Esto captura el contexto completo de la conversación, no solo el último mensaje. Una misma pregunta con diferente contexto previo genera un embedding diferente → entradas diferentes en la caché.
### Umbral de similitud — dual threshold
El caché semántico tiene dos umbrales independientes:
| Variable | Default | Comportamiento |
| --- | --- | --- |
| `CACHE_RAG_DIRECT_THRESHOLD` | `0.98` | score ≥ 0.98 → retorno directo de caché (`X-Cache: HIT`), sin llamar al LLM |
| `SIMILARITY_THRESHOLD` | `0.87` | score ≥ 0.87 y < 0.98 → inyecta la respuesta cacheada como RAG context y llama al LLM (`X-Cache: CACHE-RAG`) |
| — | — | score < 0.87 → MISS, continúa a Knowledge y Sources search |
Knowledge y Sources tienen su propio umbral (`KNOWLEDGE_SIMILARITY_THRESHOLD=0.75`, `SOURCES_SIMILARITY_THRESHOLD=0.75`) sobre la búsqueda dense (Prefetch de la query híbrida en knowledge, búsqueda directa en sources).
### Streaming transparente
En cache **hit** con `stream=true`: el proxy reconstruye el formato SSE completo de Anthropic (events: `message_start`, `content_block_start`, `content_block_delta`..., `message_stop`) para que Claude Code no note diferencia con una respuesta real.
En cache **miss** con `stream=true`: el proxy hace "tee" del stream — lo reenvía al cliente en tiempo real y simultáneamente captura los chunks para almacenar la respuesta completa al finalizar.
### Multicliente
Qdrant expone el puerto 6333 en la red del host de Podman. Cualquier instancia de Claude Code en la misma red (LAN, VPN, mismo host) puede apuntar al proxy y compartir la caché.
---
## 🔗 Documentos relacionados
- [proxy.md](proxy.md) — Detalles de la API del proxy
- [cache.md](cache.md) — Operaciones de caché y estrategia de embeddings
- [setup.md](setup.md) — Guía de instalación y configuración