Ir al contenido

Costos y caching de embeddings

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:

  1. Tu propia GPU en cloud: pagás horas-GPU, ~$0.50–$2 por hora en runpod / lambda. Manejable pero requiere ops.
  2. API managed (OpenAI, Anthropic, Cohere): pagás por token. Mucho más simple, pero necesitás saber cuánto vas a gastar.
  3. 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”.

Tres fuentes de gasto, en orden de impacto típico:

FaseCuándoCuánto suele pesar
Embed (ingest)Cada vez que indexás corpusUna vez (si cacheás), grande si no
Embed (query)Cada queryPequeño por unidad, escala con QPS
GenerateCada queryEl más caro por unidad
Rerank/judge (opt)Cada query con LLM-as-reranker o evalMultiplicador 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.

packages/01-vercel-ai-sdk/src/ops/cost.ts tiene tres bloques:

cost.ts: estimateTokens
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%.

cost.ts: pricing
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.

cost.ts: cachedEmbedMany
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).

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:

Ventana de terminal
EMBED_CACHE=1 pnpm vercel:ingest

Sin 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.

Ventana de terminal
# Primer run: 0 hits, 30 misses
EMBED_CACHE=1 pnpm vercel:ingest
# → "Embedded 30 chunks (cache: 0 hits, 30 misses)"
# Segundo run sin cambios: 30 hits, 0 misses
EMBED_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.

Snippet para ejecutar manual desde Node REPL o un script ad-hoc:

cost-demo.ts (ejemplo)
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:

EscenarioEmbed/mesGenerate/mesTotal/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.

Antes de migrar tu RAG a una API paga:

  1. Estimá con el módulo cost.ts cuánto te va a costar tu corpus + tu QPS proyectado.
  2. Multiplicá por 2 (siempre subestimás).
  3. Pedile budget a quien corresponda.
  4. 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).

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.