@juspay/neurolink
Version:
The pipe layer of an AI nervous system: one interface connecting provider neurons to your application, across three inference types — generate, stream and decide. `decide` returns typed, calibrated judgements from a non-generative model (~400ms, ~$0.00002
275 lines (274 loc) • 11.6 kB
JavaScript
/**
* Anthropic prompt-cache breakpoint placement for the native Vertex+Claude
* request path.
*
* Vertex does NOT support automatic prompt caching — the only way the
* conversation prefix gets cached across turns is explicit `cache_control`
* markers in the request. Anthropic renders a request as `tools → system →
* messages` and caches the prefix up to each marker, with a hard ceiling of
* four markers.
*
* Without a marker on the message history, the entire (growing, often
* tool-result-heavy) conversation falls *after* the last breakpoint and is
* re-billed at full input price on every turn. That is the regression this
* fixes: it gives the history a rolling breakpoint so the stable prefix is
* cached at ~0.1x and only the newest turn is fresh.
*/
const EPHEMERAL = { type: "ephemeral" };
/** Anthropic allows at most four `cache_control` breakpoints per request. */
export const ANTHROPIC_MAX_CACHE_BREAKPOINTS = 4;
const MAX_BREAKPOINTS = ANTHROPIC_MAX_CACHE_BREAKPOINTS;
/**
* Annotate a native Vertex+Claude request with prompt-cache breakpoints.
*
* Budget allocation (max 4 markers):
* 1. The stable prefix — the last system block when a system prompt is
* present (this single marker caches `tools + system`, since system
* renders after tools); otherwise the last tool definition.
* 2-4. A rolling breakpoint on the last few messages, so the
* growing-but-now-stable conversation history is cached and only the
* newest turn is billed as fresh input.
*
* Pure: the inputs are cloned, never mutated.
*/
export function applyVertexAnthropicCacheBreakpoints(input) {
let budget = MAX_BREAKPOINTS;
// 1. Stable prefix. A bare string `system` cannot carry cache_control, so
// convert it to block form. Marking the last system block caches the whole
// tools + system prefix; only fall back to marking the last tool when there
// is no system prompt to mark.
let system = input.system;
let tools = input.tools;
const hasSystem = !!input.system && input.system.trim().length > 0;
if (hasSystem) {
system = [
{ type: "text", text: input.system, cache_control: EPHEMERAL },
];
budget--;
}
else if (input.tools && input.tools.length > 0) {
const lastIndex = input.tools.length - 1;
tools = input.tools.map((tool, i) => i === lastIndex ? { ...tool, cache_control: EPHEMERAL } : tool);
budget--;
}
// 2. Rolling history breakpoints over the tail of the conversation.
const messages = input.messages.map((m) => ({ ...m }));
let remaining = Math.min(budget, input.maxHistoryBreakpoints ?? MAX_BREAKPOINTS);
for (let i = messages.length - 1; i >= 0 && remaining > 0; i--) {
if (markLastContentBlock(messages, i)) {
remaining--;
}
}
return { system, tools, messages };
}
/**
* Count the `cache_control` markers already present on a request. The direct
* Anthropic path arrives with markers the upstream layers placed —
* MessageBuilder tags the system prompt, the client tags the last tool
* definition, and message content blocks may carry their own. Anthropic
* rejects requests with more than four markers, so any additional history
* breakpoints must fit in the remaining budget.
*/
export function countAnthropicCacheMarkers(input) {
let count = 0;
if (Array.isArray(input.system)) {
count += input.system.filter((b) => b.cache_control).length;
}
if (input.tools) {
count += input.tools.filter((t) => t.cache_control).length;
}
for (const message of input.messages) {
if (Array.isArray(message.content)) {
count += message.content.filter((b) => b.cache_control).length;
}
}
return count;
}
/**
* Rolling history breakpoints for request paths whose stable-prefix markers
* are managed upstream (direct Anthropic: system via MessageBuilder, last
* tool via the provider client). Marks the last content block of up to `budget`
* tail messages; a tail block that already carries a marker is left as-is
* without consuming budget (it already serves as that breakpoint). Pure —
* the input array is cloned, never mutated.
*/
export function applyAnthropicHistoryCacheBreakpoints(input, budget) {
const messages = input.map((m) => ({ ...m }));
let remaining = Math.max(0, Math.min(budget, MAX_BREAKPOINTS));
for (let i = messages.length - 1; i >= 0 && remaining > 0; i--) {
if (lastContentBlockHasMarker(messages[i])) {
continue;
}
if (markLastContentBlock(messages, i)) {
remaining--;
}
}
return messages;
}
/** True when the last content block of a message already carries `cache_control`. */
function lastContentBlockHasMarker(message) {
if (!Array.isArray(message.content) || message.content.length === 0) {
return false;
}
const last = message.content[message.content.length - 1];
return !!last.cache_control;
}
/**
* Place a cache breakpoint on the last content block of `messages[i]`.
* Anthropic attaches `cache_control` to a content block, not the message
* envelope, so a string content body is first converted to block form.
* Returns false when the message has no markable block (caller then walks to
* an earlier message), so an empty turn never silently consumes a breakpoint.
*/
function markLastContentBlock(messages, i) {
const message = messages[i];
if (typeof message.content === "string") {
if (message.content.length === 0) {
return false;
}
messages[i] = {
...message,
content: [
{ type: "text", text: message.content, cache_control: EPHEMERAL },
],
};
return true;
}
if (!Array.isArray(message.content) || message.content.length === 0) {
return false;
}
// Shallow clone is sufficient: we only ever add a top-level `cache_control`
// field to the last block and never mutate nested members (e.g. an image's
// `source`). Those nested objects stay shared with the input by reference,
// which is safe because they are never written to. If a future block shape
// requires mutating nested members, deep-clone that block instead.
const content = message.content.map((block) => ({ ...block }));
const lastIndex = content.length - 1;
content[lastIndex] = { ...content[lastIndex], cache_control: EPHEMERAL };
messages[i] = { ...message, content };
return true;
}
/**
* Breakpoint placement for a Claude Messages request the proxy built itself,
* rather than one a client authored.
*
* A request translated from another wire format arrives with no markers at
* all, because the format it came from has no way to express one: OpenAI-shaped
* callers rely on automatic prefix caching, which Anthropic does not do. A
* faithful translation is therefore cached at 0% on every turn, and a caller
* that fell back here from an exhausted pool burns the replacement quota faster
* than the one it was spared.
*
* Placement follows Anthropic's `tools → system → messages` rendering: one
* marker on the last system block covers tools *and* system in a single
* breakpoint, and the rest roll along the tail of the history so a growing
* conversation keeps its settled prefix cached. Markers the caller supplied are
* counted first and never displaced — this only spends what is left of the
* four.
*
* Pure: inputs are cloned, never mutated.
*/
export function applyClaudeRequestCacheBreakpoints(request, options) {
const existing = countAnthropicCacheMarkers({
system: request.system,
tools: request.tools,
messages: [],
});
const inMessages = request.messages.reduce((n, m) => n +
(Array.isArray(m.content)
? m.content.filter((b) => "cache_control" in b && b.cache_control)
.length
: 0), 0);
let budget = MAX_BREAKPOINTS - existing - inMessages;
if (budget <= 0) {
return request;
}
const out = { ...request };
const breakpointOneMarker = ephemeral(options?.ttl);
// One marker on the stable head. The last system block is the better anchor
// than the last tool, because system renders after tools and so a single
// marker there caches both.
if (Array.isArray(out.system) && out.system.length > 0 && existing === 0) {
const system = out.system.map((b) => ({ ...b }));
system[system.length - 1] = {
...system[system.length - 1],
cache_control: breakpointOneMarker,
};
out.system = system;
budget--;
}
else if ((!out.system || (Array.isArray(out.system) && out.system.length === 0)) &&
out.tools &&
out.tools.length > 0 &&
existing === 0) {
const tools = out.tools.map((t) => ({ ...t }));
tools[tools.length - 1] = {
...tools[tools.length - 1],
cache_control: breakpointOneMarker,
};
out.tools = tools;
budget--;
}
if (budget > 0) {
out.messages = applyClaudeHistoryBreakpoints(out.messages, budget);
}
return out;
}
const CLAUDE_EPHEMERAL = { type: "ephemeral" };
/**
* Builds the breakpoint-1 marker, honoring a caller-supplied TTL override.
* Breakpoints 2-4 (`applyClaudeHistoryBreakpoints`) always use the bare
* `CLAUDE_EPHEMERAL` constant instead of this — they roll forward every turn
* or two, so paying the 1-hour (2x) rate on a marker about to be superseded
* is pure waste; only the stable head benefits from the longer window.
*/
function ephemeral(ttl) {
return ttl ? { type: "ephemeral", ttl } : CLAUDE_EPHEMERAL;
}
/**
* Roll `budget` markers backwards along the history, marking the last content
* block of each message that can carry one. Only text and tool_result blocks
* accept `cache_control`; a message ending in anything else is skipped without
* spending budget.
*
* A message whose content is a plain string is promoted to the single text
* block the marker has to live on. Skipping strings instead would have left an
* ordinary conversation with no history breakpoint at all, since the OpenAI
* bridge emits every plain-text turn as a string — which is precisely the
* growing history this is here to keep cached.
*/
function applyClaudeHistoryBreakpoints(input, budget) {
const messages = input.map((m) => ({ ...m }));
let remaining = Math.max(0, Math.min(budget, MAX_BREAKPOINTS));
for (let i = messages.length - 1; i >= 0 && remaining > 0; i--) {
const content = messages[i].content;
if (typeof content === "string") {
if (content.length === 0) {
continue;
}
messages[i] = {
...messages[i],
content: [
{ type: "text", text: content, cache_control: CLAUDE_EPHEMERAL },
],
};
remaining--;
continue;
}
if (!Array.isArray(content) || content.length === 0) {
continue;
}
const last = content[content.length - 1];
if (last.type !== "text" && last.type !== "tool_result") {
continue;
}
if (last.cache_control) {
continue;
}
const blocks = content.map((b) => ({ ...b }));
blocks[blocks.length - 1] = { ...last, cache_control: CLAUDE_EPHEMERAL };
messages[i] = { ...messages[i], content: blocks };
remaining--;
}
return messages;
}