UNPKG

agentfootprint

Version:

The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.

118 lines • 6.01 kB
/** * anthropicCacheWire — the cache↔wire translation both Anthropic adapters share. * * Pattern: extracted helper (was module-private inside BrowserAnthropicProvider). * Role: Outer ring. Three jobs, one file: * 1. `ANTHROPIC_PROMPT_CACHING` — what every adapter on this wire DECLARES * (`LLMProvider.promptCaching`): explicit breakpoints, four per request, * usage reported. The agent chooses its cache strategy from it. * 2. `applyCacheMarkers` — stamp `cache_control` onto an Anthropic request * body where the framework's `CacheMarker`s point. * 3. `readCacheUsage` — lift Anthropic's cache token counts off a response * usage payload into the port's `cacheRead` / `cacheWrite` fields. * * Why it exists: the server adapter (`AnthropicProvider`) used to drop * `LLMRequest.cacheMarkers` on the floor and report no cache tokens, so on * the server path a byte-identical prompt prefix could neither be cached nor * even OBSERVED as uncached — only the browser half kept its side of the * contract. One module serving every adapter on this wire removes the gap * and prevents byte-twin drift between them. The declaration lives here for * the same reason: the three adapters that build this body * (`anthropic()`, `browserAnthropic()`, `invokeModelGateway()`) make one * promise, written once. * * @example * const indexMap: number[] = []; * const body = { model, max_tokens, messages: toAnthropicMessages(req.messages, indexMap) }; * if (req.cacheMarkers?.length) applyCacheMarkers(body, req.cacheMarkers, indexMap); * // ...and on the way back: * return { usage: { input, output, ...readCacheUsage(message.usage) } }; */ // ─── The declaration ──────────────────────────────────────────────── /** * What an adapter on the Anthropic Messages wire promises about prompt * caching: it writes `cache_control` where the request's markers point * (`applyCacheMarkers`), the API honours at most four such breakpoints per * request, and the response's cache token counts reach the port * (`readCacheUsage`). * * Declared by the adapter, not looked up by its name — so `withRetry(anthropic())` * or an application's own wrapper keeps caching by forwarding this one field. */ export const ANTHROPIC_PROMPT_CACHING = Object.freeze({ mode: 'breakpoints', maxBreakpoints: 4, reportsUsage: true, }); /** * Apply `CacheMarker[]` to an Anthropic request body in-place. * * Per-field positional rules (Anthropic API): * - `system`: convert from `string` → array of text blocks; mark the block * with `cache_control`. The body carries the whole system prompt in ONE * block today, so any system marker caches the whole prompt. * - `tools`: mark the tool at `boundaryIndex` (clamped to last tool). * - `messages`: mark the LAST content block of the LAST message in the * cacheable prefix. Anthropic only honors `cache_control` there. * * `messageIndexMap` translates the marker's index — a position in * `LLMRequest.messages` — into a position in `body.messages`. A message that * did not survive the transform (`-1`, a system message) cannot be marked at * all; silently marking its neighbour would claim a boundary nobody asked for. * * Markers arrive already clamped to the declared `maxBreakpoints` (four, see * `ANTHROPIC_PROMPT_CACHING`) by `BreakpointCacheStrategy`, so no enforcement * here. */ export function applyCacheMarkers(body, markers, messageIndexMap) { for (const m of markers) { const cacheControl = m.ttl === 'long' ? { type: 'ephemeral', ttl: '1h' } : { type: 'ephemeral' }; if (m.field === 'system') { // Convert string system → array form so we can attach cache_control. if (typeof body.system === 'string') { body.system = [{ type: 'text', text: body.system, cache_control: cacheControl }]; } } else if (m.field === 'tools' && body.tools && body.tools.length > 0) { const idx = Math.min(m.boundaryIndex, body.tools.length - 1); const tool = body.tools[idx]; tool.cache_control = cacheControl; } else if (m.field === 'messages' && body.messages.length > 0) { // Translate first: the marker names a request-message position, and a // message that did not survive the transform cannot be marked at all. const mapped = messageIndexMap?.[m.boundaryIndex]; if (messageIndexMap !== undefined && (mapped === undefined || mapped < 0)) continue; const msgIdx = Math.min(mapped ?? m.boundaryIndex, body.messages.length - 1); const msg = body.messages[msgIdx]; if (typeof msg.content === 'string') { // String content → wrap in array so we can attach cache_control. msg.content = [{ type: 'text', text: msg.content, cache_control: cacheControl }]; } else if (Array.isArray(msg.content) && msg.content.length > 0) { const last = msg.content[msg.content.length - 1]; last.cache_control = cacheControl; } } } } /** * Lift Anthropic's cache counts into the port's optional `cacheRead` / * `cacheWrite` fields. Absent stays absent — an adapter must never invent a * zero for a number the provider did not report (that is the difference * between "no cache traffic" and "nobody measured"). */ export function readCacheUsage(usage) { if (!usage) return {}; return { ...(typeof usage.cache_read_input_tokens === 'number' && { cacheRead: usage.cache_read_input_tokens, }), ...(typeof usage.cache_creation_input_tokens === 'number' && { cacheWrite: usage.cache_creation_input_tokens, }), }; } //# sourceMappingURL=anthropicCacheWire.js.map