Exemplo de API de cache de prompt: corte de custos de LLM do Node.js

O cache de prompt reduz os custos do token LLM ao reutilizar um prefixo de prompt processado anteriormente para que o provedor ignore a necessidade de re-tokenizar e recodificar-lo em solicitações subsequentes. Este tutorial percorre a construção de um construtor de solicitação TypeScript e um auxiliar de registro de uso que normaliza o comportamento de cache de prompt em todos os três provedores em Node.js.
Índice
Por que o cache de prompt é importante para desenvolvedores Node.js
O cache de prompt reduz os custos do token LLM ao reutilizar um prefixo de prompt processado anteriormente para que o provedor ignore a necessidade de re-tokenizar e recodificar-lo em solicitações subsequentes. Para desenvolvedores de Node.js que fazem chamadas repetidas de API com instruções de sistema estáveis, exemplos de poucas tentativas ou documentos de referência longos, o cache imediato pode proporcionar uma redução de custos de até 90% em tokens armazenados em cache.
O problema é que três grandes provedores – OpenAI, Anthropic e Gemini – implementam cada um o cache rápido de maneira diferente. Os mecanismos de aceitação, os limites mínimos de token, o comportamento do TTL, os indicadores de acerto de cache e as taxas de desconto variam. Os desenvolvedores devem reunir documentação incompatível.
O resultado é uma única função de fábrica que constrói solicitações otimizadas para cache e um criador de logs unificado que verifica ocorrências de cache, independentemente do provedor.
Este tutorial percorre a construção de um construtor de solicitação TypeScript e um auxiliar de registro de uso que normaliza o comportamento de cache de prompt em todos os três provedores em Node.js. O resultado é uma única função de fábrica que constrói solicitações otimizadas para cache e um criador de logs unificado que verifica ocorrências de cache, independentemente do provedor.
Pré-requisitos: Node.js 20+, TypeScript 5+, uma chave de API para pelo menos um dos três provedores e familiaridade com async/await e consumo de API REST.
Como funciona o cache de prompt em todos os provedores
O princípio compartilhado: prefixos de prompt estáveis
Todos os três provedores armazenam em cache desde o início do prompt — apenas o prefixo se qualifica. Um “prefixo estável” é qualquer conteúdo que não muda entre solicitações: instruções do sistema, exemplos rápidos, documentos de referência longos ou contexto clichê. Mantenha o prefixo idêntico em bytes nas chamadas. Qualquer mutação, mesmo uma alteração de espaço em branco, invalida o cache e força uma passagem completa de reprocessamento.
O design imediato é importante. Carregue todo o conteúdo estático e anexe conteúdo variável (a pergunta real do usuário, contexto dinâmico) no final.
Regras de cache específicas do provedor
A tabela a seguir captura as diferenças críticas entre OpenAI, Anthropic e Gemini:
DimensãoOpenAIAntrópicoGêmeosMecanismo de aceitaçãoAutomático (sem necessidade de sinalizador)cache_control ponto de interrupção em mensagenscachedContent recurso criado por meio de endpoint separadoComprimento mínimo do prefixo1.024 fichas1.024 tokens para a maioria dos modelos atuais; 4.096 para Opus 4.5/4.6 e Haiku 4.5; 2.048 para Haiku 3,532.768 fichasCache TTL5 a 10 min (automático, não configurável; a OpenAI não publica um TTL exato e os valores observados caem nesta faixa em meados de 2025)5 minutos padrão; configurável com ephemeral tipoTTL explícito definido na criação (de acordo com a documentação do Gemini no momento da escrita)Indicador de acerto de cacheusage.prompt_tokens_details.cached_tokensusage.cache_read_input_tokensusageMetadata.cachedContentTokenCountDesconto de custo50% em tokens armazenados em cache90% em tokens de leitura em cache75% em tokens em cache (75% para Gemini 1.5 Pro; taxa de flash não confirmada no momento da escrita)Modelos suportadosModelos de bate-papo GPT-4o e posterioresClaude 3.5 e posterior (os mínimos variam de acordo com o modelo)Cache explícito no Gemini 1.5 e modelos 2.5/3 suportados; cache implícito ativado por padrão para Gemini 2.5+Para cache explícito, o Gemini requer o maior prefixo mínimo de 32.768 tokens, tornando-o adequado apenas para janelas de contexto muito grandes. Gemini 2.5 e versões mais recentes também permitem cache implícito por padrão, que funciona mais como a abordagem automática do OpenAI. A Anthropic oferece o maior desconto de token de leitura de 90%, embora a Anthropic cobre chamadas de criação de cache 25% acima do preço de entrada padrão. OpenAI não tem configuração, mas oferece o menor desconto de 50%.
Configuração do projeto e tipos compartilhados
Inicializando o projeto Node.js
mkdir prompt-cache-demo && cd prompt-cache-demo npm init -y npm install openai@^4.0.0 @anthropic-ai/sdk@^0.24.0 @google/generative-ai@^0.21.0 dotenv@^16.4.0 npm install -D typescript @types/node npx tsc --initAtualizar tsconfig.json com estes fundamentos:
{ "compilerOptions": { "target": "ES2022", "module": "NodeNext", "moduleResolution": "NodeNext", "outDir": "./dist", "rootDir": "./src", "strict": true, "esModuleInterop": true }, "include": ("src") }Crie um .env arquivo:
OPENAI_API_KEY=your_openai_api_key_here ANTHROPIC_API_KEY=your_anthropic_api_key_here GEMINI_API_KEY=your_gemini_api_key_hereDefinindo interfaces TypeScript compartilhadas
export type Provider = "openai" | "anthropic" | "gemini"; export interface CacheAwareRequest { systemPrompt: string; userMessage: string; provider: Provider; } export interface CacheUsageReport { totalTokens: number; cachedTokens: number; cacheHit: boolean; provider: Provider; cachedTokenDiscountPercent: number; estimatedCostSavingsPercent: number; }O CacheAwareRequest mantém o formato de entrada independente do provedor. O CacheUsageReport normaliza a saída para que o código de chamada nunca precise saber qual campo específico do provedor indica uma ocorrência no cache. O cachedTokenDiscountPercent reflete a taxa de desconto do provedor em tokens armazenados em cache, enquanto estimatedCostSavingsPercent aproxima a economia real em relação ao total de tokens.
Construindo o construtor de solicitações TypeScript
Estruturando prefixos estáveis para OpenAI
O OpenAI armazena em cache automaticamente. Não há nenhum sinalizador para definir, nenhum recurso para criar. O único requisito é que o prefixo do prompt tenha pelo menos 1.024 tokens. A mensagem do sistema é o local natural para o prefixo estável.
import "dotenv/config"; import OpenAI from "openai"; import type { ChatCompletionCreateParams } from "openai/resources/chat/completions"; const client = new OpenAI(); export async function buildOpenAIRequest( systemPrompt: string, userMessage: string ) { const params: ChatCompletionCreateParams = { model: "gpt-4o", messages: ( { role: "system", content: systemPrompt }, { role: "user", content: userMessage }, ), }; try { const response = await client.chat.completions.create(params); return response; } catch (err) { const message = err instanceof Error ? err.message : String(err); throw new Error(`OpenAI API request failed: ${message}`, { cause: err }); } }O systemPrompt string deve conter o prefixo estável completo. Contanto que exceda 1.024 tokens e permaneça idêntico em todas as chamadas, o OpenAI irá armazená-lo em cache automaticamente dentro de sua janela TTL.
Estruturando Prefixos Estáveis para Antrópicos
A Anthropic exige aceitação explícita por meio de um cache_control ponto de interrupção. O ponto de interrupção é colocado no último bloco de conteúdo do prefixo estável. Tudo antes (e inclusive) desse bloco é armazenado em cache; tudo depois é processado fresco.
import "dotenv/config"; import Anthropic from "@anthropic-ai/sdk"; const client = new Anthropic(); export async function buildAnthropicRequest( systemPrompt: string, userMessage: string ) { try { const response = await client.messages.create({ model: "claude-3-5-sonnet-20241022", max_tokens: 1024, system: ( { type: "text", text: systemPrompt, cache_control: { type: "ephemeral" }, }, ), messages: ({ role: "user", content: userMessage }), }); return response; } catch (err) { const message = err instanceof Error ? err.message : String(err); throw new Error(`Anthropic API request failed: ${message}`, { cause: err }); } }O cache_control: { type: "ephemeral" } a anotação marca o limite. Na primeira chamada, a Antrópica informa cache_creation_input_tokens. Nas chamadas subsequentes dentro do TTL de 5 minutos, ele reporta cache_read_input_tokens em vez de.
Estruturando prefixos estáveis para Gemini
Gêmeos usa um processo de duas etapas. Primeiro, crie um cachedContent recurso por meio de uma chamada de API separada, especificando o conteúdo e um TTL explícito. Em seguida, faça referência a esse recurso nas próximas generateContent chamadas. O prefixo mínimo é 32.768 tokens.
Observação: O GoogleAICacheManager classe pode ser exportada de @google/generative-ai/server em vez do nível superior @google/generative-ai dependendo da versão do SDK instalada. Se a importação abaixo falhar, tente import { GoogleAICacheManager } from "@google/generative-ai/server". Verifique com: node -e "const m = require('@google/generative-ai'); console.log(typeof m.GoogleAICacheManager)" – se imprimir undefineduse o /server subcaminho.
import "dotenv/config"; import { GoogleGenerativeAI, GoogleAICacheManager, } from "@google/generative-ai"; import type { CachedContent } from "@google/generative-ai"; const GEMINI_API_KEY = process.env.GEMINI_API_KEY; if (!GEMINI_API_KEY) { throw new Error( "GEMINI_API_KEY environment variable is not set. " + "Add it to your .env file before importing this module." ); } const genAI = new GoogleGenerativeAI(GEMINI_API_KEY); const cacheManager = new GoogleAICacheManager(GEMINI_API_KEY); export async function buildGeminiCachedContent(systemPrompt: string) { try { const cachedContent = await cacheManager.create({ model: "models/gemini-1.5-pro-001", contents: ( { role: "user", parts: ({ text: systemPrompt }), }, ), ttlSeconds: 3600, }); return cachedContent; } catch (err) { const message = err instanceof Error ? err.message : String(err); throw new Error(`Gemini cache creation failed: ${message}`, { cause: err }); } } export async function buildGeminiRequest( cachedContent: CachedContent, userMessage: string ) { try { const model = genAI.getGenerativeModelFromCachedContent(cachedContent); const result = await model.generateContent(userMessage); return result.response; } catch (err) { const message = err instanceof Error ? err.message : String(err); throw new Error(`Gemini API request failed: ${message}`, { cause: err }); } }O objeto de conteúdo em cache deve ser armazenado e reutilizado. Se o TTL expirar, o recurso deverá ser recriado. Na produção, persista o nome do conteúdo armazenado em cache em um banco de dados ou armazenamento de cache para sobreviver às reinicializações do processo; a memória Map usado na fábrica abaixo é perdido a cada partida a frio, causando cachedContent criação e faturamento adicional.
Unificando-se com um Request-Builder Factory
Uma única função de fábrica é despachada para o construtor específico do provedor correto e retorna uma forma normalizada junto com a resposta bruta para inspeção de uso.
import { createHash } from "node:crypto"; import { buildOpenAIRequest } from "./builders/openai.js"; import { buildAnthropicRequest } from "./builders/anthropic.js"; import { buildGeminiCachedContent, buildGeminiRequest, } from "./builders/gemini.js"; import type { Provider } from "./types.js"; import type { CachedContent } from "@google/generative-ai"; const geminiCacheObjects = new Map<string, CachedContent>(); const geminiCacheInflight = new Map<string, Promise<CachedContent>>(); export async function buildCachedRequest( provider: Provider, systemPrompt: string, userMessage: string ) { switch (provider) { case "openai": { const response = await buildOpenAIRequest(systemPrompt, userMessage); return { text: response.choices(0)?.message?.content ?? "", raw: response }; } case "anthropic": { const response = await buildAnthropicRequest(systemPrompt, userMessage); const textBlock = response.content.find((b) => b.type === "text"); return { text: textBlock?.text ?? "", raw: response }; } case "gemini": { const cacheKey = createHash("sha256").update(systemPrompt).digest("hex"); if (!geminiCacheObjects.has(cacheKey)) { if (!geminiCacheInflight.has(cacheKey)) { const inflight = buildGeminiCachedContent(systemPrompt).then((cached) => { geminiCacheObjects.set(cacheKey, cached); geminiCacheInflight.delete(cacheKey); return cached; }); geminiCacheInflight.set(cacheKey, inflight); } await geminiCacheInflight.get(cacheKey)!; } const response = await buildGeminiRequest( geminiCacheObjects.get(cacheKey)!, userMessage ); const candidate = response.candidates?.(0); const text = candidate?.content?.parts ?.map((p: { text?: string }) => p.text ?? "") .join("") ?? ""; return { text, raw: response }; } default: { const _exhaustive: never = provider; throw new Error( `Unsupported provider: "${_exhaustive}". ` + `Valid values are: "openai" | "anthropic" | "gemini".` ); } } }Uma única função de fábrica é despachada para o construtor específico do provedor correto e retorna uma forma normalizada junto com a resposta bruta para inspeção de uso.
O caminho Gemini armazena objetos de conteúdo em cache em um Map codificado por um hash SHA-256 do prompt completo do sistema, evitando a criação de cache redundante em chamadas repetidas e colisões de chave de cache que poderiam ocorrer com chaves baseadas em prefixo. Um voo secundário Map garante que solicitações simultâneas com o mesmo prompt não acionem chamadas duplicadas de criação de cache.
Verificando ocorrências de cache com um auxiliar de registro de uso
Análise de métricas de uso do OpenAI
OpenAI expõe dados de cache em response.usage.prompt_tokens_details.cached_tokens. Quando esse valor excede zero, a solicitação atinge o cache.
Análise de métricas de uso antrópico
A Anthropic divide o cache em dois campos: response.usage.cache_creation_input_tokens (preenchido na primeira solicitação quando o provedor grava o cache) e response.usage.cache_read_input_tokens (preenchido em solicitações subsequentes que o leem). Verifique se cache_read_input_tokens > 0 para confirmar um acerto.
Análise de métricas de uso do Gemini
Verificar response.usageMetadata.cachedContentTokenCount. Se exceder zero, o recurso de conteúdo em cache foi usado para essa solicitação.
O auxiliar logCacheUsage unificado
import { createHash } from "node:crypto"; import type { Provider, CacheUsageReport } from "./types.js"; const DISCOUNT_RATES: Record<Provider, number> = { openai: 50, anthropic: 90, gemini: 75, }; const PROVIDER_MINIMUMS: Record<Provider, number> = { openai: 1024, anthropic: 1024, gemini: 32768, }; interface OpenAIRaw { usage?: { total_tokens?: number; prompt_tokens_details?: { cached_tokens?: number }; }; } interface AnthropicRaw { usage?: { input_tokens?: number; output_tokens?: number; cache_read_input_tokens?: number; }; } interface GeminiRaw { usageMetadata?: { promptTokenCount?: number; candidatesTokenCount?: number; cachedContentTokenCount?: number; }; } type RawResponse = OpenAIRaw | AnthropicRaw | GeminiRaw; export function logCacheUsage(raw: RawResponse, provider: Provider): CacheUsageReport { let totalTokens = 0; let cachedTokens = 0; switch (provider) { case "openai": { const r = raw as OpenAIRaw; totalTokens = r.usage?.total_tokens ?? 0; cachedTokens = r.usage?.prompt_tokens_details?.cached_tokens ?? 0; break; } case "anthropic": { const r = raw as AnthropicRaw; totalTokens = (r.usage?.input_tokens ?? 0) + (r.usage?.output_tokens ?? 0); cachedTokens = r.usage?.cache_read_input_tokens ?? 0; break; } case "gemini": { const r = raw as GeminiRaw; totalTokens = (r.usageMetadata?.promptTokenCount ?? 0) + (r.usageMetadata?.candidatesTokenCount ?? 0); cachedTokens = r.usageMetadata?.cachedContentTokenCount ?? 0; break; } } const cacheHit = cachedTokens > 0; const cachedTokenDiscountPercent = cacheHit ? DISCOUNT_RATES(provider) : 0; const estimatedCostSavingsPercent = totalTokens > 0 && cacheHit ? (cachedTokens / totalTokens) * DISCOUNT_RATES(provider) : 0; const report: CacheUsageReport = { totalTokens, cachedTokens, cacheHit, provider, cachedTokenDiscountPercent, estimatedCostSavingsPercent, }; console.table(report); return report; } export function logCacheUsageDebug( raw: RawResponse, provider: Provider, systemPrompt: string ): CacheUsageReport { const report = logCacheUsage(raw, provider); const prefixHash = createHash("sha256").update(systemPrompt).digest("hex").slice(0, 16); const estimatedTokens = Math.ceil(systemPrompt.split(/\s+/).length * 1.3); const minimum = PROVIDER_MINIMUMS(provider); console.log(`(DEBUG) Prefix hash: ${prefixHash}`); console.log(`(DEBUG) Estimated prefix tokens: ${estimatedTokens}`); console.log(`(DEBUG) Provider minimum: ${minimum}`); console.log(`(DEBUG) Meets minimum: ${estimatedTokens >= minimum}`); console.log(`(DEBUG) Timestamp: ${new Date().toISOString()}`); if (!report.cacheHit && estimatedTokens < minimum) { console.warn(`(WARN) Prefix below ${provider} minimum of ${minimum} tokens`); } return report; }O cachedTokenDiscountPercent reflete a taxa de desconto do provedor aplicada especificamente aos tokens armazenados em cache. O estimatedCostSavingsPercent aproxima a economia real em relação ao total de tokens na solicitação — por exemplo, se apenas 100 dos 10.000 tokens fossem armazenados em cache na taxa de 90% da Antthropic, a economia estimada seria de aproximadamente 0,9%, não de 90%. A comparação de hash entre solicitações revela se o prefixo foi alterado. A estimativa do token usa um multiplicador de 1,3x na contagem de palavras como uma aproximação aproximada (+-30% de erro); os sistemas de produção devem usar tokenizadores específicos do provedor (por exemplo, tiktoken para OpenAI) para precisão.
Inspecionando falhas de cache e depuração
Razões comuns para falhas de cache
As falhas de cache acontecem por vários motivos concretos. Você alterou o prefixo entre as solicitações (até mesmo os espaços em branco ou as diferenças de nova linha à direita contam). Seu prefixo está abaixo do limite mínimo de token do provedor. O TTL expirou – o padrão de 5 minutos da Anthropic é particularmente agressivo. Você alternou o parâmetro do modelo entre solicitações. Ou, no caso de Gêmeos, você não passou no cachedContent nome para a chamada de geração.
Demonstração completa: executando duas solicitações consecutivas
import "dotenv/config"; import { buildCachedRequest } from "./requestBuilder.js"; import { logCacheUsageDebug } from "./cacheLogger.js"; import type { Provider } from "./types.js"; const VALID_PROVIDERS: Provider() = ("openai", "anthropic", "gemini"); const rawProvider = process.argv(2) ?? process.env.PROVIDER ?? "openai"; if (!VALID_PROVIDERS.includes(rawProvider as Provider)) { throw new Error( `Invalid provider: "${rawProvider}". Valid values are: ${VALID_PROVIDERS.join(", ")}` ); } const provider: Provider = rawProvider as Provider; const systemPrompt = `You are an expert legal analyst. Below is the full text of the terms of service for analysis: ${"Section: The following terms govern use of the platform and all associated services. Users must comply with all applicable regulations and guidelines as specified herein. ".repeat(60)}`; const userMessage = "Summarize the key obligations for end users."; async function main() { console.log(` --- Request 1 (${provider}): Expect cache MISS ---`); const result1 = await buildCachedRequest(provider, systemPrompt, userMessage); logCacheUsageDebug(result1.raw, provider, systemPrompt); if (provider === "openai") { await new Promise((r) => setTimeout(r, 2000)); } console.log(` --- Request 2 (${provider}): Expect cache HIT ---`); const result2 = await buildCachedRequest(provider, systemPrompt, userMessage); logCacheUsageDebug(result2.raw, provider, systemPrompt); } main().catch(console.error);Corra com npx tsc && node dist/demo.js openai. Troque o argumento por anthropic ou gemini para testar outros provedores. A primeira solicitação preenche o cache; o segundo lê dele.
Impacto no custo e na latência: o que esperar
Economias realistas dependem da frequência de reutilização dentro da janela TTL. OpenAI oferece um desconto de 50% em tokens de entrada em cache. A Anthropic fornece 90% em tokens de leitura em cache, embora a Anthropic cobre chamadas de criação de cache 25% acima do preço de entrada padrão. Gemini fornece 75% para Gemini 1.5 Pro (a taxa de Flash não foi confirmada no momento da escrita). Os relatórios da Anthropic reduziram a latência nos acessos ao cache, mas nenhum dos três provedores publica benchmarks de latência fixa para armazenamento em cache; consulte a documentação oficial de cache rápido de cada provedor para obter os números atuais.
Essas economias só se materializam quando o mesmo prefixo é reutilizado frequentemente no TTL do cache. Um prefixo que muda a cada solicitação ou que fica abaixo do limite mínimo de token do provedor nunca acionará o cache e você pagará o preço padrão.
Essas economias só se materializam quando o mesmo prefixo é reutilizado frequentemente no TTL do cache. Um prefixo que muda a cada solicitação ou que fica abaixo do limite mínimo de token do provedor nunca acionará o cache e você pagará o preço padrão.
Próximas etapas
Comece adicionando suporte de resposta de streaming ao construtor de solicitações, já que os prefixos armazenados em cache funcionam com streaming em todos os três provedores. A partir daí, implemente o gerenciamento persistente de TTL para Gemini cachedContent recursos (que de outra forma exigiriam recriação manual após a expiração) e conectar o registrador de uso de cache a um pipeline de observabilidade como o OpenTelemetry spans para monitoramento de custos de produção.
Para obter detalhes completos sobre o comportamento de cache de cada provedor, consulte a documentação oficial: Cache de prompt OpenAI, Cache de prompt antrópicoe Cache de Contexto Gemini.
Source link
Compartilhe esta publicação
Ajude a levar esta informação adiante — publique no seu perfil.