@juspay/neurolink 9.79.0 → 9.79.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -10,6 +10,7 @@ import { ModelConfigurationManager } from "../core/modelConfiguration.js";
10
10
  import { createProxyFetch } from "../proxy/proxyFetch.js";
11
11
  import { AuthenticationError, InvalidModelError, NetworkError, ProviderError, RateLimitError, } from "../types/index.js";
12
12
  import { ERROR_CODES, NeuroLinkError } from "../utils/errorHandling.js";
13
+ import { applyVertexAnthropicCacheBreakpoints } from "../utils/anthropicCacheBreakpoints.js";
13
14
  import { FileDetector } from "../utils/fileDetector.js";
14
15
  import { processUnifiedFilesArray } from "../utils/messageBuilder.js";
15
16
  import { logger } from "../utils/logger.js";
@@ -2491,9 +2492,27 @@ export class GoogleVertexProvider extends BaseProvider {
2491
2492
  }, turnContext);
2492
2493
  let response;
2493
2494
  try {
2495
+ // Vertex has no automatic prompt caching — place explicit
2496
+ // cache_control breakpoints (system, tools, rolling history) so the
2497
+ // conversation prefix is cached across turns instead of re-billed as
2498
+ // fresh input every call. Re-applied per step: the stable prefix
2499
+ // stays byte-identical (consistent cache key) while the rolling
2500
+ // breakpoint follows the growing tail.
2501
+ const cachedStream = applyVertexAnthropicCacheBreakpoints({
2502
+ system: systemPromptWithSchema,
2503
+ tools,
2504
+ messages: currentMessages,
2505
+ });
2494
2506
  const stream = await client.messages.stream({
2495
2507
  ...requestParams,
2496
- messages: currentMessages,
2508
+ ...(cachedStream.system !== undefined && {
2509
+ system: cachedStream.system,
2510
+ }),
2511
+ ...(cachedStream.tools &&
2512
+ cachedStream.tools.length > 0 && {
2513
+ tools: cachedStream.tools,
2514
+ }),
2515
+ messages: cachedStream.messages,
2497
2516
  });
2498
2517
  activeStream = stream;
2499
2518
  // Forward each text delta as it arrives — the Anthropic SDK fires
@@ -2750,6 +2769,17 @@ export class GoogleVertexProvider extends BaseProvider {
2750
2769
  }
2751
2770
  metadata.responseTime = Date.now() - startTime;
2752
2771
  metadata.totalToolExecutions = allToolCalls.filter((tc) => tc.toolName !== "final_result").length;
2772
+ // Surface cache metrics once, after the agentic loop, from the
2773
+ // cumulative turnCacheUsage running totals (accumulated via += on every
2774
+ // step above). Assigned here — not per-iteration — so the final values
2775
+ // are unambiguous: analytics sees caching is working and calculateCost
2776
+ // can price the cache-read/write tiers instead of full input rate.
2777
+ if (turnCacheUsage.read > 0) {
2778
+ usage.cacheReadTokens = turnCacheUsage.read;
2779
+ }
2780
+ if (turnCacheUsage.creation > 0) {
2781
+ usage.cacheCreationTokens = turnCacheUsage.creation;
2782
+ }
2753
2783
  const turnOutputAttribute = spanJsonAttribute({
2754
2784
  text: aggregatedTurnText,
2755
2785
  ...(structuredOutputRef.value
@@ -3110,6 +3140,12 @@ export class GoogleVertexProvider extends BaseProvider {
3110
3140
  const allToolCalls = [];
3111
3141
  let totalInputTokens = 0;
3112
3142
  let totalOutputTokens = 0;
3143
+ // Cache metrics (Anthropic reports these separately from input_tokens, which
3144
+ // is the uncached remainder). Surfaced on the result so analytics can see
3145
+ // caching is working and calculateCost can price the ~0.1x cache-read /
3146
+ // ~1.25x cache-write tiers instead of billing everything at full input rate.
3147
+ let totalCacheReadTokens = 0;
3148
+ let totalCacheCreationTokens = 0;
3113
3149
  // Track the final Anthropic stop_reason so we can surface finishReason
3114
3150
  // (notably "length" on token truncation) — the legacy native path always
3115
3151
  // reported "stop", hiding truncation from callers.
@@ -3121,13 +3157,36 @@ export class GoogleVertexProvider extends BaseProvider {
3121
3157
  // Bound the SDK wait so a stalled Vertex/Anthropic call can't hang
3122
3158
  // generate forever. options.timeout wins if set, otherwise default
3123
3159
  // to 5 min — generous for tool-heavy turns.
3160
+ // Vertex has no automatic prompt caching — place explicit cache_control
3161
+ // breakpoints (system, tools, rolling history) so the conversation
3162
+ // prefix is cached across turns instead of re-billed as fresh input
3163
+ // every call. Re-applied per step: the stable prefix stays
3164
+ // byte-identical (consistent cache key) while the rolling breakpoint
3165
+ // follows the growing tail.
3166
+ const cachedGenerate = applyVertexAnthropicCacheBreakpoints({
3167
+ system: systemPromptWithSchema,
3168
+ tools,
3169
+ messages: currentMessages,
3170
+ });
3124
3171
  const response = await withTimeout(client.messages.create({
3125
3172
  ...requestParams,
3126
- messages: currentMessages,
3173
+ ...(cachedGenerate.system !== undefined && {
3174
+ system: cachedGenerate.system,
3175
+ }),
3176
+ ...(cachedGenerate.tools &&
3177
+ cachedGenerate.tools.length > 0 && {
3178
+ tools: cachedGenerate.tools,
3179
+ }),
3180
+ messages: cachedGenerate.messages,
3127
3181
  }), generateTimeoutMs, "Anthropic generate timed out");
3128
- // Update token counts
3182
+ // Update token counts. input_tokens is the uncached remainder; cache
3183
+ // reads/writes are reported separately and accumulated here so the
3184
+ // result reflects the full picture.
3129
3185
  totalInputTokens += response.usage?.input_tokens || 0;
3130
3186
  totalOutputTokens += response.usage?.output_tokens || 0;
3187
+ totalCacheReadTokens += response.usage?.cache_read_input_tokens || 0;
3188
+ totalCacheCreationTokens +=
3189
+ response.usage?.cache_creation_input_tokens || 0;
3131
3190
  lastStopReason = response.stop_reason;
3132
3191
  // Check if we need to handle tool use
3133
3192
  const toolUseBlocks = response.content.filter((block) => block.type === "tool_use");
@@ -3287,6 +3346,12 @@ export class GoogleVertexProvider extends BaseProvider {
3287
3346
  input: totalInputTokens,
3288
3347
  output: totalOutputTokens,
3289
3348
  total: totalInputTokens + totalOutputTokens,
3349
+ ...(totalCacheReadTokens > 0 && {
3350
+ cacheReadTokens: totalCacheReadTokens,
3351
+ }),
3352
+ ...(totalCacheCreationTokens > 0 && {
3353
+ cacheCreationTokens: totalCacheCreationTokens,
3354
+ }),
3290
3355
  },
3291
3356
  responseTime,
3292
3357
  toolsUsed: externalToolCalls.map((tc) => tc.toolName),
@@ -3761,6 +3826,8 @@ export class GoogleVertexProvider extends BaseProvider {
3761
3826
  const inputTokens = usage.input ?? usage.inputTokens ?? 0;
3762
3827
  const outputTokens = usage.output ?? usage.outputTokens ?? 0;
3763
3828
  const totalTokens = usage.total ?? usage.totalTokens ?? inputTokens + outputTokens;
3829
+ const cacheReadTokens = usage.cacheReadTokens ?? 0;
3830
+ const cacheCreationTokens = usage.cacheCreationTokens ?? 0;
3764
3831
  if (inputTokens > 0) {
3765
3832
  span.setAttribute("gen_ai.usage.input_tokens", inputTokens);
3766
3833
  }
@@ -3770,11 +3837,22 @@ export class GoogleVertexProvider extends BaseProvider {
3770
3837
  if (totalTokens > 0) {
3771
3838
  span.setAttribute("gen_ai.usage.total_tokens", totalTokens);
3772
3839
  }
3840
+ if (cacheReadTokens > 0) {
3841
+ span.setAttribute("gen_ai.usage.cache_read_input_tokens", cacheReadTokens);
3842
+ }
3843
+ if (cacheCreationTokens > 0) {
3844
+ span.setAttribute("gen_ai.usage.cache_creation_input_tokens", cacheCreationTokens);
3845
+ }
3773
3846
  try {
3847
+ // Pass cache tokens through so calculateCost prices the ~0.1x cache-read
3848
+ // and ~1.25x cache-write tiers instead of billing cached content at the
3849
+ // full input rate.
3774
3850
  const cost = calculateCost(this.providerName, modelName, {
3775
3851
  input: inputTokens,
3776
3852
  output: outputTokens,
3777
3853
  total: totalTokens,
3854
+ cacheReadTokens,
3855
+ cacheCreationTokens,
3778
3856
  });
3779
3857
  if (typeof cost === "number" && cost > 0) {
3780
3858
  span.setAttribute("neurolink.cost", cost);
@@ -1797,11 +1797,21 @@ export type VertexGenaiFunctionDeclaration = {
1797
1797
  * Message payload passed to the Anthropic Vertex SDK — mirrors the Anthropic
1798
1798
  * Messages API shape (role + structured content blocks).
1799
1799
  */
1800
+ /**
1801
+ * Anthropic ephemeral prompt-cache breakpoint marker. Placed on a content
1802
+ * block / tool / system block to make the rendered prefix up to that point a
1803
+ * cache breakpoint. Vertex has NO automatic caching, so these explicit markers
1804
+ * are the only way the conversation prefix is cached across turns.
1805
+ */
1806
+ export type VertexAnthropicCacheControl = {
1807
+ type: "ephemeral";
1808
+ };
1800
1809
  export type VertexAnthropicMessage = {
1801
1810
  role: "user" | "assistant";
1802
1811
  content: string | Array<{
1803
1812
  type: "text";
1804
1813
  text: string;
1814
+ cache_control?: VertexAnthropicCacheControl;
1805
1815
  } | {
1806
1816
  type: "image";
1807
1817
  source: {
@@ -1809,6 +1819,7 @@ export type VertexAnthropicMessage = {
1809
1819
  media_type: string;
1810
1820
  data: string;
1811
1821
  };
1822
+ cache_control?: VertexAnthropicCacheControl;
1812
1823
  } | {
1813
1824
  type: "document";
1814
1825
  source: {
@@ -1816,23 +1827,38 @@ export type VertexAnthropicMessage = {
1816
1827
  media_type: string;
1817
1828
  data: string;
1818
1829
  };
1830
+ cache_control?: VertexAnthropicCacheControl;
1819
1831
  } | {
1820
1832
  type: "tool_use";
1821
1833
  id: string;
1822
1834
  name: string;
1823
1835
  input: unknown;
1836
+ cache_control?: VertexAnthropicCacheControl;
1824
1837
  } | {
1825
1838
  type: "tool_result";
1826
1839
  tool_use_id: string;
1827
1840
  content: string;
1841
+ cache_control?: VertexAnthropicCacheControl;
1828
1842
  } | {
1829
1843
  type: "thinking";
1830
1844
  thinking: string;
1845
+ cache_control?: VertexAnthropicCacheControl;
1831
1846
  } | {
1832
1847
  type: "redacted_thinking";
1833
1848
  data: string;
1849
+ cache_control?: VertexAnthropicCacheControl;
1834
1850
  }>;
1835
1851
  };
1852
+ /**
1853
+ * System prompt block form accepted by the Anthropic Vertex SDK. Used instead
1854
+ * of a bare string when a `cache_control` breakpoint must ride on the system
1855
+ * prompt (a string `system` cannot carry one).
1856
+ */
1857
+ export type VertexAnthropicSystemBlock = {
1858
+ type: "text";
1859
+ text: string;
1860
+ cache_control?: VertexAnthropicCacheControl;
1861
+ };
1836
1862
  /** Tool definition accepted by the Anthropic Vertex SDK. */
1837
1863
  export type VertexAnthropicTool = {
1838
1864
  name: string;
@@ -1842,6 +1868,26 @@ export type VertexAnthropicTool = {
1842
1868
  properties?: Record<string, unknown>;
1843
1869
  required?: string[];
1844
1870
  };
1871
+ cache_control?: VertexAnthropicCacheControl;
1872
+ };
1873
+ /** Input to `applyVertexAnthropicCacheBreakpoints`. */
1874
+ export type VertexAnthropicCacheInput = {
1875
+ system?: string;
1876
+ tools?: VertexAnthropicTool[];
1877
+ messages: VertexAnthropicMessage[];
1878
+ /**
1879
+ * Cap on how many of the most-recent messages receive a rolling history
1880
+ * breakpoint. Defaults to "use the remaining budget". Two or more gives
1881
+ * cross-turn continuity and resilience against Anthropic's 20-block cache
1882
+ * lookback window on tool-heavy turns.
1883
+ */
1884
+ maxHistoryBreakpoints?: number;
1885
+ };
1886
+ /** Output of `applyVertexAnthropicCacheBreakpoints` — a cache-annotated request. */
1887
+ export type VertexAnthropicCacheOutput = {
1888
+ system?: string | VertexAnthropicSystemBlock[];
1889
+ tools?: VertexAnthropicTool[];
1890
+ messages: VertexAnthropicMessage[];
1845
1891
  };
1846
1892
  /**
1847
1893
  * Content block variants returned by the Anthropic Vertex SDK during streaming
@@ -0,0 +1,15 @@
1
+ import type { VertexAnthropicCacheInput, VertexAnthropicCacheOutput } from "../types/index.js";
2
+ /**
3
+ * Annotate a native Vertex+Claude request with prompt-cache breakpoints.
4
+ *
5
+ * Budget allocation (max 4 markers):
6
+ * 1. The stable prefix — the last system block when a system prompt is
7
+ * present (this single marker caches `tools + system`, since system
8
+ * renders after tools); otherwise the last tool definition.
9
+ * 2-4. A rolling breakpoint on the last few messages, so the
10
+ * growing-but-now-stable conversation history is cached and only the
11
+ * newest turn is billed as fresh input.
12
+ *
13
+ * Pure: the inputs are cloned, never mutated.
14
+ */
15
+ export declare function applyVertexAnthropicCacheBreakpoints(input: VertexAnthropicCacheInput): VertexAnthropicCacheOutput;
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Anthropic prompt-cache breakpoint placement for the native Vertex+Claude
3
+ * request path.
4
+ *
5
+ * Vertex does NOT support automatic prompt caching — the only way the
6
+ * conversation prefix gets cached across turns is explicit `cache_control`
7
+ * markers in the request. Anthropic renders a request as `tools → system →
8
+ * messages` and caches the prefix up to each marker, with a hard ceiling of
9
+ * four markers.
10
+ *
11
+ * Without a marker on the message history, the entire (growing, often
12
+ * tool-result-heavy) conversation falls *after* the last breakpoint and is
13
+ * re-billed at full input price on every turn. That is the regression this
14
+ * fixes: it gives the history a rolling breakpoint so the stable prefix is
15
+ * cached at ~0.1x and only the newest turn is fresh.
16
+ */
17
+ const EPHEMERAL = { type: "ephemeral" };
18
+ /** Anthropic allows at most four `cache_control` breakpoints per request. */
19
+ const MAX_BREAKPOINTS = 4;
20
+ /**
21
+ * Annotate a native Vertex+Claude request with prompt-cache breakpoints.
22
+ *
23
+ * Budget allocation (max 4 markers):
24
+ * 1. The stable prefix — the last system block when a system prompt is
25
+ * present (this single marker caches `tools + system`, since system
26
+ * renders after tools); otherwise the last tool definition.
27
+ * 2-4. A rolling breakpoint on the last few messages, so the
28
+ * growing-but-now-stable conversation history is cached and only the
29
+ * newest turn is billed as fresh input.
30
+ *
31
+ * Pure: the inputs are cloned, never mutated.
32
+ */
33
+ export function applyVertexAnthropicCacheBreakpoints(input) {
34
+ let budget = MAX_BREAKPOINTS;
35
+ // 1. Stable prefix. A bare string `system` cannot carry cache_control, so
36
+ // convert it to block form. Marking the last system block caches the whole
37
+ // tools + system prefix; only fall back to marking the last tool when there
38
+ // is no system prompt to mark.
39
+ let system = input.system;
40
+ let tools = input.tools;
41
+ const hasSystem = !!input.system && input.system.trim().length > 0;
42
+ if (hasSystem) {
43
+ system = [
44
+ { type: "text", text: input.system, cache_control: EPHEMERAL },
45
+ ];
46
+ budget--;
47
+ }
48
+ else if (input.tools && input.tools.length > 0) {
49
+ const lastIndex = input.tools.length - 1;
50
+ tools = input.tools.map((tool, i) => i === lastIndex ? { ...tool, cache_control: EPHEMERAL } : tool);
51
+ budget--;
52
+ }
53
+ // 2. Rolling history breakpoints over the tail of the conversation.
54
+ const messages = input.messages.map((m) => ({ ...m }));
55
+ let remaining = Math.min(budget, input.maxHistoryBreakpoints ?? MAX_BREAKPOINTS);
56
+ for (let i = messages.length - 1; i >= 0 && remaining > 0; i--) {
57
+ if (markLastContentBlock(messages, i)) {
58
+ remaining--;
59
+ }
60
+ }
61
+ return { system, tools, messages };
62
+ }
63
+ /**
64
+ * Place a cache breakpoint on the last content block of `messages[i]`.
65
+ * Anthropic attaches `cache_control` to a content block, not the message
66
+ * envelope, so a string content body is first converted to block form.
67
+ * Returns false when the message has no markable block (caller then walks to
68
+ * an earlier message), so an empty turn never silently consumes a breakpoint.
69
+ */
70
+ function markLastContentBlock(messages, i) {
71
+ const message = messages[i];
72
+ if (typeof message.content === "string") {
73
+ if (message.content.length === 0) {
74
+ return false;
75
+ }
76
+ messages[i] = {
77
+ ...message,
78
+ content: [
79
+ { type: "text", text: message.content, cache_control: EPHEMERAL },
80
+ ],
81
+ };
82
+ return true;
83
+ }
84
+ if (!Array.isArray(message.content) || message.content.length === 0) {
85
+ return false;
86
+ }
87
+ // Shallow clone is sufficient: we only ever add a top-level `cache_control`
88
+ // field to the last block and never mutate nested members (e.g. an image's
89
+ // `source`). Those nested objects stay shared with the input by reference,
90
+ // which is safe because they are never written to. If a future block shape
91
+ // requires mutating nested members, deep-clone that block instead.
92
+ const content = message.content.map((block) => ({ ...block }));
93
+ const lastIndex = content.length - 1;
94
+ content[lastIndex] = { ...content[lastIndex], cache_control: EPHEMERAL };
95
+ messages[i] = { ...message, content };
96
+ return true;
97
+ }
98
+ //# sourceMappingURL=anthropicCacheBreakpoints.js.map
@@ -10,6 +10,7 @@ import { ModelConfigurationManager } from "../core/modelConfiguration.js";
10
10
  import { createProxyFetch } from "../proxy/proxyFetch.js";
11
11
  import { AuthenticationError, InvalidModelError, NetworkError, ProviderError, RateLimitError, } from "../types/index.js";
12
12
  import { ERROR_CODES, NeuroLinkError } from "../utils/errorHandling.js";
13
+ import { applyVertexAnthropicCacheBreakpoints } from "../utils/anthropicCacheBreakpoints.js";
13
14
  import { FileDetector } from "../utils/fileDetector.js";
14
15
  import { processUnifiedFilesArray } from "../utils/messageBuilder.js";
15
16
  import { logger } from "../utils/logger.js";
@@ -2491,9 +2492,27 @@ export class GoogleVertexProvider extends BaseProvider {
2491
2492
  }, turnContext);
2492
2493
  let response;
2493
2494
  try {
2495
+ // Vertex has no automatic prompt caching — place explicit
2496
+ // cache_control breakpoints (system, tools, rolling history) so the
2497
+ // conversation prefix is cached across turns instead of re-billed as
2498
+ // fresh input every call. Re-applied per step: the stable prefix
2499
+ // stays byte-identical (consistent cache key) while the rolling
2500
+ // breakpoint follows the growing tail.
2501
+ const cachedStream = applyVertexAnthropicCacheBreakpoints({
2502
+ system: systemPromptWithSchema,
2503
+ tools,
2504
+ messages: currentMessages,
2505
+ });
2494
2506
  const stream = await client.messages.stream({
2495
2507
  ...requestParams,
2496
- messages: currentMessages,
2508
+ ...(cachedStream.system !== undefined && {
2509
+ system: cachedStream.system,
2510
+ }),
2511
+ ...(cachedStream.tools &&
2512
+ cachedStream.tools.length > 0 && {
2513
+ tools: cachedStream.tools,
2514
+ }),
2515
+ messages: cachedStream.messages,
2497
2516
  });
2498
2517
  activeStream = stream;
2499
2518
  // Forward each text delta as it arrives — the Anthropic SDK fires
@@ -2750,6 +2769,17 @@ export class GoogleVertexProvider extends BaseProvider {
2750
2769
  }
2751
2770
  metadata.responseTime = Date.now() - startTime;
2752
2771
  metadata.totalToolExecutions = allToolCalls.filter((tc) => tc.toolName !== "final_result").length;
2772
+ // Surface cache metrics once, after the agentic loop, from the
2773
+ // cumulative turnCacheUsage running totals (accumulated via += on every
2774
+ // step above). Assigned here — not per-iteration — so the final values
2775
+ // are unambiguous: analytics sees caching is working and calculateCost
2776
+ // can price the cache-read/write tiers instead of full input rate.
2777
+ if (turnCacheUsage.read > 0) {
2778
+ usage.cacheReadTokens = turnCacheUsage.read;
2779
+ }
2780
+ if (turnCacheUsage.creation > 0) {
2781
+ usage.cacheCreationTokens = turnCacheUsage.creation;
2782
+ }
2753
2783
  const turnOutputAttribute = spanJsonAttribute({
2754
2784
  text: aggregatedTurnText,
2755
2785
  ...(structuredOutputRef.value
@@ -3110,6 +3140,12 @@ export class GoogleVertexProvider extends BaseProvider {
3110
3140
  const allToolCalls = [];
3111
3141
  let totalInputTokens = 0;
3112
3142
  let totalOutputTokens = 0;
3143
+ // Cache metrics (Anthropic reports these separately from input_tokens, which
3144
+ // is the uncached remainder). Surfaced on the result so analytics can see
3145
+ // caching is working and calculateCost can price the ~0.1x cache-read /
3146
+ // ~1.25x cache-write tiers instead of billing everything at full input rate.
3147
+ let totalCacheReadTokens = 0;
3148
+ let totalCacheCreationTokens = 0;
3113
3149
  // Track the final Anthropic stop_reason so we can surface finishReason
3114
3150
  // (notably "length" on token truncation) — the legacy native path always
3115
3151
  // reported "stop", hiding truncation from callers.
@@ -3121,13 +3157,36 @@ export class GoogleVertexProvider extends BaseProvider {
3121
3157
  // Bound the SDK wait so a stalled Vertex/Anthropic call can't hang
3122
3158
  // generate forever. options.timeout wins if set, otherwise default
3123
3159
  // to 5 min — generous for tool-heavy turns.
3160
+ // Vertex has no automatic prompt caching — place explicit cache_control
3161
+ // breakpoints (system, tools, rolling history) so the conversation
3162
+ // prefix is cached across turns instead of re-billed as fresh input
3163
+ // every call. Re-applied per step: the stable prefix stays
3164
+ // byte-identical (consistent cache key) while the rolling breakpoint
3165
+ // follows the growing tail.
3166
+ const cachedGenerate = applyVertexAnthropicCacheBreakpoints({
3167
+ system: systemPromptWithSchema,
3168
+ tools,
3169
+ messages: currentMessages,
3170
+ });
3124
3171
  const response = await withTimeout(client.messages.create({
3125
3172
  ...requestParams,
3126
- messages: currentMessages,
3173
+ ...(cachedGenerate.system !== undefined && {
3174
+ system: cachedGenerate.system,
3175
+ }),
3176
+ ...(cachedGenerate.tools &&
3177
+ cachedGenerate.tools.length > 0 && {
3178
+ tools: cachedGenerate.tools,
3179
+ }),
3180
+ messages: cachedGenerate.messages,
3127
3181
  }), generateTimeoutMs, "Anthropic generate timed out");
3128
- // Update token counts
3182
+ // Update token counts. input_tokens is the uncached remainder; cache
3183
+ // reads/writes are reported separately and accumulated here so the
3184
+ // result reflects the full picture.
3129
3185
  totalInputTokens += response.usage?.input_tokens || 0;
3130
3186
  totalOutputTokens += response.usage?.output_tokens || 0;
3187
+ totalCacheReadTokens += response.usage?.cache_read_input_tokens || 0;
3188
+ totalCacheCreationTokens +=
3189
+ response.usage?.cache_creation_input_tokens || 0;
3131
3190
  lastStopReason = response.stop_reason;
3132
3191
  // Check if we need to handle tool use
3133
3192
  const toolUseBlocks = response.content.filter((block) => block.type === "tool_use");
@@ -3287,6 +3346,12 @@ export class GoogleVertexProvider extends BaseProvider {
3287
3346
  input: totalInputTokens,
3288
3347
  output: totalOutputTokens,
3289
3348
  total: totalInputTokens + totalOutputTokens,
3349
+ ...(totalCacheReadTokens > 0 && {
3350
+ cacheReadTokens: totalCacheReadTokens,
3351
+ }),
3352
+ ...(totalCacheCreationTokens > 0 && {
3353
+ cacheCreationTokens: totalCacheCreationTokens,
3354
+ }),
3290
3355
  },
3291
3356
  responseTime,
3292
3357
  toolsUsed: externalToolCalls.map((tc) => tc.toolName),
@@ -3761,6 +3826,8 @@ export class GoogleVertexProvider extends BaseProvider {
3761
3826
  const inputTokens = usage.input ?? usage.inputTokens ?? 0;
3762
3827
  const outputTokens = usage.output ?? usage.outputTokens ?? 0;
3763
3828
  const totalTokens = usage.total ?? usage.totalTokens ?? inputTokens + outputTokens;
3829
+ const cacheReadTokens = usage.cacheReadTokens ?? 0;
3830
+ const cacheCreationTokens = usage.cacheCreationTokens ?? 0;
3764
3831
  if (inputTokens > 0) {
3765
3832
  span.setAttribute("gen_ai.usage.input_tokens", inputTokens);
3766
3833
  }
@@ -3770,11 +3837,22 @@ export class GoogleVertexProvider extends BaseProvider {
3770
3837
  if (totalTokens > 0) {
3771
3838
  span.setAttribute("gen_ai.usage.total_tokens", totalTokens);
3772
3839
  }
3840
+ if (cacheReadTokens > 0) {
3841
+ span.setAttribute("gen_ai.usage.cache_read_input_tokens", cacheReadTokens);
3842
+ }
3843
+ if (cacheCreationTokens > 0) {
3844
+ span.setAttribute("gen_ai.usage.cache_creation_input_tokens", cacheCreationTokens);
3845
+ }
3773
3846
  try {
3847
+ // Pass cache tokens through so calculateCost prices the ~0.1x cache-read
3848
+ // and ~1.25x cache-write tiers instead of billing cached content at the
3849
+ // full input rate.
3774
3850
  const cost = calculateCost(this.providerName, modelName, {
3775
3851
  input: inputTokens,
3776
3852
  output: outputTokens,
3777
3853
  total: totalTokens,
3854
+ cacheReadTokens,
3855
+ cacheCreationTokens,
3778
3856
  });
3779
3857
  if (typeof cost === "number" && cost > 0) {
3780
3858
  span.setAttribute("neurolink.cost", cost);
@@ -1797,11 +1797,21 @@ export type VertexGenaiFunctionDeclaration = {
1797
1797
  * Message payload passed to the Anthropic Vertex SDK — mirrors the Anthropic
1798
1798
  * Messages API shape (role + structured content blocks).
1799
1799
  */
1800
+ /**
1801
+ * Anthropic ephemeral prompt-cache breakpoint marker. Placed on a content
1802
+ * block / tool / system block to make the rendered prefix up to that point a
1803
+ * cache breakpoint. Vertex has NO automatic caching, so these explicit markers
1804
+ * are the only way the conversation prefix is cached across turns.
1805
+ */
1806
+ export type VertexAnthropicCacheControl = {
1807
+ type: "ephemeral";
1808
+ };
1800
1809
  export type VertexAnthropicMessage = {
1801
1810
  role: "user" | "assistant";
1802
1811
  content: string | Array<{
1803
1812
  type: "text";
1804
1813
  text: string;
1814
+ cache_control?: VertexAnthropicCacheControl;
1805
1815
  } | {
1806
1816
  type: "image";
1807
1817
  source: {
@@ -1809,6 +1819,7 @@ export type VertexAnthropicMessage = {
1809
1819
  media_type: string;
1810
1820
  data: string;
1811
1821
  };
1822
+ cache_control?: VertexAnthropicCacheControl;
1812
1823
  } | {
1813
1824
  type: "document";
1814
1825
  source: {
@@ -1816,23 +1827,38 @@ export type VertexAnthropicMessage = {
1816
1827
  media_type: string;
1817
1828
  data: string;
1818
1829
  };
1830
+ cache_control?: VertexAnthropicCacheControl;
1819
1831
  } | {
1820
1832
  type: "tool_use";
1821
1833
  id: string;
1822
1834
  name: string;
1823
1835
  input: unknown;
1836
+ cache_control?: VertexAnthropicCacheControl;
1824
1837
  } | {
1825
1838
  type: "tool_result";
1826
1839
  tool_use_id: string;
1827
1840
  content: string;
1841
+ cache_control?: VertexAnthropicCacheControl;
1828
1842
  } | {
1829
1843
  type: "thinking";
1830
1844
  thinking: string;
1845
+ cache_control?: VertexAnthropicCacheControl;
1831
1846
  } | {
1832
1847
  type: "redacted_thinking";
1833
1848
  data: string;
1849
+ cache_control?: VertexAnthropicCacheControl;
1834
1850
  }>;
1835
1851
  };
1852
+ /**
1853
+ * System prompt block form accepted by the Anthropic Vertex SDK. Used instead
1854
+ * of a bare string when a `cache_control` breakpoint must ride on the system
1855
+ * prompt (a string `system` cannot carry one).
1856
+ */
1857
+ export type VertexAnthropicSystemBlock = {
1858
+ type: "text";
1859
+ text: string;
1860
+ cache_control?: VertexAnthropicCacheControl;
1861
+ };
1836
1862
  /** Tool definition accepted by the Anthropic Vertex SDK. */
1837
1863
  export type VertexAnthropicTool = {
1838
1864
  name: string;
@@ -1842,6 +1868,26 @@ export type VertexAnthropicTool = {
1842
1868
  properties?: Record<string, unknown>;
1843
1869
  required?: string[];
1844
1870
  };
1871
+ cache_control?: VertexAnthropicCacheControl;
1872
+ };
1873
+ /** Input to `applyVertexAnthropicCacheBreakpoints`. */
1874
+ export type VertexAnthropicCacheInput = {
1875
+ system?: string;
1876
+ tools?: VertexAnthropicTool[];
1877
+ messages: VertexAnthropicMessage[];
1878
+ /**
1879
+ * Cap on how many of the most-recent messages receive a rolling history
1880
+ * breakpoint. Defaults to "use the remaining budget". Two or more gives
1881
+ * cross-turn continuity and resilience against Anthropic's 20-block cache
1882
+ * lookback window on tool-heavy turns.
1883
+ */
1884
+ maxHistoryBreakpoints?: number;
1885
+ };
1886
+ /** Output of `applyVertexAnthropicCacheBreakpoints` — a cache-annotated request. */
1887
+ export type VertexAnthropicCacheOutput = {
1888
+ system?: string | VertexAnthropicSystemBlock[];
1889
+ tools?: VertexAnthropicTool[];
1890
+ messages: VertexAnthropicMessage[];
1845
1891
  };
1846
1892
  /**
1847
1893
  * Content block variants returned by the Anthropic Vertex SDK during streaming
@@ -0,0 +1,15 @@
1
+ import type { VertexAnthropicCacheInput, VertexAnthropicCacheOutput } from "../types/index.js";
2
+ /**
3
+ * Annotate a native Vertex+Claude request with prompt-cache breakpoints.
4
+ *
5
+ * Budget allocation (max 4 markers):
6
+ * 1. The stable prefix — the last system block when a system prompt is
7
+ * present (this single marker caches `tools + system`, since system
8
+ * renders after tools); otherwise the last tool definition.
9
+ * 2-4. A rolling breakpoint on the last few messages, so the
10
+ * growing-but-now-stable conversation history is cached and only the
11
+ * newest turn is billed as fresh input.
12
+ *
13
+ * Pure: the inputs are cloned, never mutated.
14
+ */
15
+ export declare function applyVertexAnthropicCacheBreakpoints(input: VertexAnthropicCacheInput): VertexAnthropicCacheOutput;