Costos y caching de embeddings
El espejismo del local-first
Sección titulada «El espejismo del local-first»Tu RAG corre 100% local. Sin API keys, sin tarjeta. Cada query es gratis. Cada re-ingest es gratis. Te ponés cómodo.
Llega el día que querés deployar. Tres opciones:
- Tu propia GPU en cloud: pagás horas-GPU, ~$0.50–$2 por hora en runpod / lambda. Manejable pero requiere ops.
- API managed (OpenAI, Anthropic, Cohere): pagás por token. Mucho más simple, pero necesitás saber cuánto vas a gastar.
- Cloudflare Workers AI: pagás por neuron-second. Híbrido, lo vemos en el cap 04.
Para 2 y 3, medir antes de movernos es lo que separa “ahorro previsible” de “factura sorpresa de $5k a fin de mes”.
La anatomía del costo de un RAG
Sección titulada «La anatomía del costo de un RAG»Tres fuentes de gasto, en orden de impacto típico:
| Fase | Cuándo | Cuánto suele pesar |
|---|---|---|
| Embed (ingest) | Cada vez que indexás corpus | Una vez (si cacheás), grande si no |
| Embed (query) | Cada query | Pequeño por unidad, escala con QPS |
| Generate | Cada query | El más caro por unidad |
| Rerank/judge (opt) | Cada query con LLM-as-reranker o eval | Multiplicador adicional |
El más sub-estimado es embed (ingest) porque es one-shot pero MUY grande con corpus chiquitos. Ejemplo: 10k chunks × 800 chars × ~200 tokens promedio = 2M tokens. A text-embedding-3-small ($0.00002 / 1k tokens) → $0.04 una vez. Manejable. Pero si reingestás todos los días por un cambio de chunker, es $1.20/mes solo por re-embeber lo mismo.
Solución: cachear embeddings. Si el contenido no cambió, no re-embedeás.
El módulo cost.ts
Sección titulada «El módulo cost.ts»packages/01-vercel-ai-sdk/src/ops/cost.ts tiene tres bloques:
1. Estimador de tokens
Sección titulada «1. Estimador de tokens»export function estimateTokens(text: string): number { return Math.max(1, Math.ceil(text.length / 4));}Heurística clásica: ~4 caracteres por token para texto en inglés/español. Subestima para código denso, sobreestima para emoji-heavy.
Para precisión exacta con OpenAI, instalás js-tiktoken. Para Ollama no hay tokenizador público accesible — la heurística es lo mejor que tenés. Para budgeting, alcanza: el error típico es ~10%.
2. Modelo de precios
Sección titulada «2. Modelo de precios»export const OPENAI_PRICING: PriceTable = { embed: 0.00002, // text-embedding-3-small generateInput: 0.0005, // gpt-4o-mini input generateOutput: 0.0015, // gpt-4o-mini output};
export function estimateCost( stage: 'embed' | 'generate', inputText: string, outputText = '', pricing: PriceTable = OPENAI_PRICING,): CostBreakdown { /* ... */ }Pasale precios reales del provider que vas a usar (ver openai.com/pricing o el equivalente de cada uno) y te calcula el cost por call. Este snapshot del 2026 cambia todo el tiempo — no lo trates como fuente de verdad permanente.
3. Cache de embeddings
Sección titulada «3. Cache de embeddings»export async function cachedEmbedMany( model: string, inputs: string[], embedFn: (toEmbed: string[]) => Promise<number[][]>,): Promise<{ embeddings: number[][]; stats: CachedEmbedStats }>;Por cada input, computa SHA1 de (model, content) y busca en .cache/embeddings/<sha1>.json. Hit = devuelve el cacheado, miss = manda a embedFn solo los inputs missing y persiste los nuevos.
Importante: el modelo es parte de la key. Si cambiás de nomic-embed-text a mxbai-embed-large, el cache no aplica (correcto: los vectores son incomparables).
Cómo integrarlo a ingest.ts
Sección titulada «Cómo integrarlo a ingest.ts»El módulo está integrado a ingest.ts pero detrás de una env var para que el código de Nivel 1 se vea limpio por default. Para activarlo:
EMBED_CACHE=1 pnpm vercel:ingestSin la env var, ingest.ts corre como en Nivel 1 — embed directo, sin cache. Con EMBED_CACHE=1, cada chunk se hashea por (model, content) y el primer run popula .cache/embeddings/. El segundo run sobre el mismo corpus es instantáneo del lado de embeddings.
# Primer run: 0 hits, 30 missesEMBED_CACHE=1 pnpm vercel:ingest# → "Embedded 30 chunks (cache: 0 hits, 30 misses)"
# Segundo run sin cambios: 30 hits, 0 missesEMBED_CACHE=1 pnpm vercel:ingest# → "Embedded 30 chunks (cache: 30 hits, 0 misses)"Si editás un solo doc, el segundo run reembed solo los chunks afectados — proporcional al delta, no al total. La integración interna en ingest.ts es ~10 líneas — abrí el archivo si querés ver el wiring.
Lab: medir tu corpus
Sección titulada «Lab: medir tu corpus»Snippet para ejecutar manual desde Node REPL o un script ad-hoc:
import { loadMarkdownDir, chunkDocs, DATA_DIR, OLLAMA_EMBED } from '@rag-lab/shared';import { estimateTokens, estimateCost } from '@rag-lab/01-vercel-ai-sdk/ops/cost';
const docs = await loadMarkdownDir(DATA_DIR);const chunks = chunkDocs(docs);const totalText = chunks.map(c => c.content).join('\n');const totalTokens = estimateTokens(totalText);const cost = estimateCost('embed', totalText);
console.log(`Corpus: ${chunks.length} chunks, ~${totalTokens} tokens`);console.log(`Embed cost (one-shot): $${cost.estimatedUSD.toFixed(4)}`);console.log(`Embed cost (daily reingest, 30 days): $${(cost.estimatedUSD * 30).toFixed(2)}`);Para el corpus del curso, los números van a ser triviales — chunks chiquitos, costo total del orden de centavos. Para tu corpus real, el ejercicio te va a dar respeto.
Cuando no estás cacheando, ¿cuánto te va a costar?
Sección titulada «Cuando no estás cacheando, ¿cuánto te va a costar?»Tres escenarios típicos para un RAG productivo:
| Escenario | Embed/mes | Generate/mes | Total/mes |
|---|---|---|---|
| 1k queries/día, sin reingest | $0.06 | $30 | $30 |
| 10k queries/día, sin reingest | $0.6 | $300 | $300 |
| 1k queries/día, reingest diario de 50k chunks | $30 | $30 | $60 |
| 10k queries/día, reingest diario de 50k chunks (sin cache) | $30 | $300 | $330 |
| 10k queries/día, reingest diario de 50k chunks (con cache, 95% hit) | $1.5 | $300 | $301.5 |
Lectura: el generate domina cuando hay mucha query. El embed (ingest) domina cuando hay muchos cambios de corpus. El cache reduce la cuota de embed casi a cero.
La regla del budget
Sección titulada «La regla del budget»Antes de migrar tu RAG a una API paga:
- Estimá con el módulo
cost.tscuánto te va a costar tu corpus + tu QPS proyectado. - Multiplicá por 2 (siempre subestimás).
- Pedile budget a quien corresponda.
- Si no hay budget → opción local (GPU propia, Ollama en serverless cheap como
together.ai) o reduciendo features (downgradeás del modelo grande al chico, bajás TOP_K, recortás re-ingest).
Lo que viene
Sección titulada «Lo que viene»Tu RAG mide su costo y cachea embeddings. Pero hay otra dimensión: el corpus cambia con el tiempo. Cada vez que un documento se edita, ¿re-indexás los 1000 docs que no cambiaron? No. El siguiente capítulo: refresh incremental.