@juspay/neurolink 9.86.1 → 9.86.3

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.
@@ -13,6 +13,7 @@ import { createProxyFetch } from "../proxy/proxyFetch.js";
13
13
  import { AuthenticationError, NetworkError, ProviderError, RateLimitError, } from "../types/index.js";
14
14
  import { logger } from "../utils/logger.js";
15
15
  import { redactUrlCredentials } from "../utils/logSanitize.js";
16
+ import { ANTHROPIC_MAX_CACHE_BREAKPOINTS, applyAnthropicHistoryCacheBreakpoints, countAnthropicCacheMarkers, } from "../utils/anthropicCacheBreakpoints.js";
16
17
  import { calculateCost } from "../utils/pricing.js";
17
18
  import { createAnthropicConfig, getProviderModel, validateApiKey, } from "../utils/providerConfig.js";
18
19
  import { composeAbortSignals, createTimeoutController, mergeAbortSignals, TimeoutError, } from "../utils/timeout.js";
@@ -1123,9 +1124,23 @@ export class AnthropicProvider extends BaseProvider {
1123
1124
  }
1124
1125
  // Extended thinking passthrough (providerOptions.anthropic.thinking).
1125
1126
  const thinking = options.providerOptions?.anthropic?.thinking;
1127
+ // Prompt-cache parity with the native Vertex+Claude path: upstream
1128
+ // layers mark only the stable prefix (system via MessageBuilder,
1129
+ // last tool via GenerationHandler) — the growing conversation
1130
+ // history has no breakpoint, so on every turn it falls after the
1131
+ // last marker and is re-billed as fresh input. Add rolling history
1132
+ // breakpoints in whatever budget remains under Anthropic's
1133
+ // four-marker ceiling; pre-existing markers are counted so the
1134
+ // request can never exceed the cap.
1135
+ const cacheMarkersUsed = countAnthropicCacheMarkers({
1136
+ system,
1137
+ tools,
1138
+ messages: messages,
1139
+ });
1140
+ const cachedMessages = applyAnthropicHistoryCacheBreakpoints(messages, ANTHROPIC_MAX_CACHE_BREAKPOINTS - cacheMarkersUsed);
1126
1141
  const params = {
1127
1142
  model: modelId,
1128
- messages,
1143
+ messages: cachedMessages,
1129
1144
  max_tokens: resolveClaudeMaxTokens(modelId, options.maxOutputTokens),
1130
1145
  ...(system ? { system } : {}),
1131
1146
  ...(options.temperature !== undefined &&
@@ -1345,10 +1360,19 @@ export class AnthropicProvider extends BaseProvider {
1345
1360
  .then((usage) => {
1346
1361
  streamSpan.setAttribute("gen_ai.usage.input_tokens", usage.promptTokens || 0);
1347
1362
  streamSpan.setAttribute("gen_ai.usage.output_tokens", usage.completionTokens || 0);
1363
+ if (usage.cacheReadTokens) {
1364
+ streamSpan.setAttribute("gen_ai.usage.cached_input_tokens", usage.cacheReadTokens);
1365
+ }
1348
1366
  const cost = calculateCost(this.providerName, this.modelName, {
1349
1367
  input: usage.promptTokens || 0,
1350
1368
  output: usage.completionTokens || 0,
1351
1369
  total: usage.totalTokens || 0,
1370
+ ...(usage.cacheReadTokens
1371
+ ? { cacheReadTokens: usage.cacheReadTokens }
1372
+ : {}),
1373
+ ...(usage.cacheCreationTokens
1374
+ ? { cacheCreationTokens: usage.cacheCreationTokens }
1375
+ : {}),
1352
1376
  });
1353
1377
  if (cost && cost > 0) {
1354
1378
  streamSpan.setAttribute("neurolink.cost", cost);
@@ -1372,11 +1396,26 @@ export class AnthropicProvider extends BaseProvider {
1372
1396
  const conversation = payload.messages.slice();
1373
1397
  let totalInput = 0;
1374
1398
  let totalOutput = 0;
1399
+ let totalCacheRead = 0;
1400
+ let totalCacheWrite = 0;
1375
1401
  let lastStop = null;
1376
1402
  for (let step = 0; step < maxSteps; step++) {
1403
+ // Prompt-cache parity with the native Vertex+Claude path — rolling
1404
+ // history breakpoints, re-applied per step so the stable prefix
1405
+ // stays byte-identical while the breakpoint follows the growing
1406
+ // tail. Budget respects markers upstream layers already placed
1407
+ // (system / last tool / message blocks) so the request never
1408
+ // exceeds Anthropic's four-marker cap. Pure: `conversation` itself
1409
+ // is never mutated, so re-counting per step stays stable.
1410
+ const cacheMarkersUsed = countAnthropicCacheMarkers({
1411
+ system: payload.system,
1412
+ tools: anthropicTools,
1413
+ messages: conversation,
1414
+ });
1415
+ const cachedConversation = applyAnthropicHistoryCacheBreakpoints(conversation, ANTHROPIC_MAX_CACHE_BREAKPOINTS - cacheMarkersUsed);
1377
1416
  const params = {
1378
1417
  model: modelId,
1379
- messages: conversation,
1418
+ messages: cachedConversation,
1380
1419
  max_tokens: resolveClaudeMaxTokens(modelId, options.maxTokens),
1381
1420
  stream: true,
1382
1421
  ...(payload.system ? { system: payload.system } : {}),
@@ -1406,6 +1445,12 @@ export class AnthropicProvider extends BaseProvider {
1406
1445
  if (event.type === "message_start") {
1407
1446
  totalInput += event.message.usage.input_tokens ?? 0;
1408
1447
  totalOutput += event.message.usage.output_tokens ?? 0;
1448
+ // Anthropic reports cache reads/writes SEPARATELY from
1449
+ // input_tokens on the same message_start event — without these
1450
+ // the streaming path silently drops all cache accounting.
1451
+ totalCacheRead += event.message.usage.cache_read_input_tokens ?? 0;
1452
+ totalCacheWrite +=
1453
+ event.message.usage.cache_creation_input_tokens ?? 0;
1409
1454
  }
1410
1455
  else if (event.type === "content_block_start") {
1411
1456
  blockTypes.set(event.index, event.content_block.type);
@@ -1578,7 +1623,11 @@ export class AnthropicProvider extends BaseProvider {
1578
1623
  resolveUsage({
1579
1624
  promptTokens: totalInput,
1580
1625
  completionTokens: totalOutput,
1581
- totalTokens: totalInput + totalOutput,
1626
+ totalTokens: totalInput + totalCacheRead + totalCacheWrite + totalOutput,
1627
+ ...(totalCacheRead > 0 ? { cacheReadTokens: totalCacheRead } : {}),
1628
+ ...(totalCacheWrite > 0
1629
+ ? { cacheCreationTokens: totalCacheWrite }
1630
+ : {}),
1582
1631
  });
1583
1632
  resolveFinish(lastStop ?? "stop");
1584
1633
  };
@@ -13,7 +13,7 @@
13
13
  * Nothing here imports from "ai" or "@ai-sdk/*". The whole point of this
14
14
  * module is to be the native replacement for the AI SDK's OpenAI wrapper.
15
15
  */
16
- import type { OpenAICompatBuildBodyArgs, OpenAICompatChatMessage, OpenAICompatChatRequest, OpenAICompatChatTool, OpenAICompatMessage, OpenAICompatMessageContent, OpenAICompatResponseFormat, OpenAICompatSSEResult, OpenAICompatStreamChunk, OpenAICompatToolChoiceWire, OpenAICompatV3CallToolChoice, OpenAICompatV3CallTools, Tool } from "../types/index.js";
16
+ import type { OpenAICompatBuildBodyArgs, OpenAICompatChatMessage, OpenAICompatChatRequest, OpenAICompatChatTool, OpenAICompatMessage, OpenAICompatMessageContent, OpenAICompatResponseFormat, OpenAICompatSSEResult, OpenAICompatStreamChunk, OpenAICompatToolChoiceWire, OpenAICompatV3CallToolChoice, OpenAICompatV3CallTools, DeferredUsage, Tool } from "../types/index.js";
17
17
  export declare const stripTrailingSlash: (s: string) => string;
18
18
  export declare const safeStringify: (value: unknown) => string;
19
19
  export declare const stringifyToolInput: (input: unknown) => string;
@@ -38,17 +38,9 @@ export declare const buildBody: (args: OpenAICompatBuildBodyArgs) => OpenAICompa
38
38
  export declare const parseSSEStream: (body: ReadableStream<Uint8Array>, onTextDelta: (delta: string) => void, onReasoningDelta?: (delta: string) => void) => Promise<OpenAICompatSSEResult>;
39
39
  export declare const buildAPIError: (url: string, body: OpenAICompatChatRequest, res: Response) => Promise<Error>;
40
40
  export declare const createDeferredAnalytics: () => {
41
- usagePromise: Promise<{
42
- promptTokens: number;
43
- completionTokens: number;
44
- totalTokens: number;
45
- }>;
41
+ usagePromise: Promise<DeferredUsage>;
46
42
  finishPromise: Promise<string>;
47
- resolveUsage: (u: {
48
- promptTokens: number;
49
- completionTokens: number;
50
- totalTokens: number;
51
- }) => void;
43
+ resolveUsage: (u: DeferredUsage) => void;
52
44
  resolveFinish: (reason: string) => void;
53
45
  };
54
46
  export declare const createChunkQueue: () => {
@@ -223,6 +223,11 @@ export type RawUsageObject = {
223
223
  cacheReadInputTokens?: number;
224
224
  cacheCreationTokens?: number;
225
225
  cacheReadTokens?: number;
226
+ cachedInputTokens?: number;
227
+ inputTokenDetails?: {
228
+ cacheReadTokens?: number;
229
+ cacheWriteTokens?: number;
230
+ };
226
231
  prompt_tokens_details?: {
227
232
  cached_tokens?: number;
228
233
  };
@@ -232,6 +237,18 @@ export type RawUsageObject = {
232
237
  thinkingTokens?: number;
233
238
  usage?: RawUsageObject;
234
239
  };
240
+ /**
241
+ * Aggregated usage resolved by a provider's deferred-analytics pair after a
242
+ * multi-step stream loop ends. The cache fields are optional — only providers
243
+ * with prompt caching (Anthropic) populate them.
244
+ */
245
+ export type DeferredUsage = {
246
+ promptTokens: number;
247
+ completionTokens: number;
248
+ totalTokens: number;
249
+ cacheReadTokens?: number;
250
+ cacheCreationTokens?: number;
251
+ };
235
252
  /**
236
253
  * Options for token extraction from raw usage objects.
237
254
  */
@@ -1,4 +1,6 @@
1
- import type { VertexAnthropicCacheInput, VertexAnthropicCacheOutput } from "../types/index.js";
1
+ import type { VertexAnthropicCacheInput, VertexAnthropicCacheOutput, VertexAnthropicMessage } from "../types/index.js";
2
+ /** Anthropic allows at most four `cache_control` breakpoints per request. */
3
+ export declare const ANTHROPIC_MAX_CACHE_BREAKPOINTS = 4;
2
4
  /**
3
5
  * Annotate a native Vertex+Claude request with prompt-cache breakpoints.
4
6
  *
@@ -13,3 +15,29 @@ import type { VertexAnthropicCacheInput, VertexAnthropicCacheOutput } from "../t
13
15
  * Pure: the inputs are cloned, never mutated.
14
16
  */
15
17
  export declare function applyVertexAnthropicCacheBreakpoints(input: VertexAnthropicCacheInput): VertexAnthropicCacheOutput;
18
+ /**
19
+ * Count the `cache_control` markers already present on a request. The direct
20
+ * Anthropic path (AI-SDK pipeline) arrives with markers the upstream layers
21
+ * placed — MessageBuilder tags the system prompt, GenerationHandler tags the
22
+ * last tool definition, and message content blocks may carry translated
23
+ * AI-SDK markers. Anthropic rejects requests with more than four markers, so
24
+ * any additional history breakpoints must fit in the remaining budget.
25
+ */
26
+ export declare function countAnthropicCacheMarkers(input: {
27
+ system?: string | ReadonlyArray<{
28
+ cache_control?: unknown;
29
+ }> | undefined;
30
+ tools?: ReadonlyArray<{
31
+ cache_control?: unknown;
32
+ }> | undefined;
33
+ messages: ReadonlyArray<VertexAnthropicMessage>;
34
+ }): number;
35
+ /**
36
+ * Rolling history breakpoints for request paths whose stable-prefix markers
37
+ * are managed upstream (direct Anthropic: system via MessageBuilder, last
38
+ * tool via GenerationHandler). Marks the last content block of up to `budget`
39
+ * tail messages; a tail block that already carries a marker is left as-is
40
+ * without consuming budget (it already serves as that breakpoint). Pure —
41
+ * the input array is cloned, never mutated.
42
+ */
43
+ export declare function applyAnthropicHistoryCacheBreakpoints(input: ReadonlyArray<VertexAnthropicMessage>, budget: number): VertexAnthropicMessage[];
@@ -16,7 +16,8 @@
16
16
  */
17
17
  const EPHEMERAL = { type: "ephemeral" };
18
18
  /** Anthropic allows at most four `cache_control` breakpoints per request. */
19
- const MAX_BREAKPOINTS = 4;
19
+ export const ANTHROPIC_MAX_CACHE_BREAKPOINTS = 4;
20
+ const MAX_BREAKPOINTS = ANTHROPIC_MAX_CACHE_BREAKPOINTS;
20
21
  /**
21
22
  * Annotate a native Vertex+Claude request with prompt-cache breakpoints.
22
23
  *
@@ -60,6 +61,58 @@ export function applyVertexAnthropicCacheBreakpoints(input) {
60
61
  }
61
62
  return { system, tools, messages };
62
63
  }
64
+ /**
65
+ * Count the `cache_control` markers already present on a request. The direct
66
+ * Anthropic path (AI-SDK pipeline) arrives with markers the upstream layers
67
+ * placed — MessageBuilder tags the system prompt, GenerationHandler tags the
68
+ * last tool definition, and message content blocks may carry translated
69
+ * AI-SDK markers. Anthropic rejects requests with more than four markers, so
70
+ * any additional history breakpoints must fit in the remaining budget.
71
+ */
72
+ export function countAnthropicCacheMarkers(input) {
73
+ let count = 0;
74
+ if (Array.isArray(input.system)) {
75
+ count += input.system.filter((b) => b.cache_control).length;
76
+ }
77
+ if (input.tools) {
78
+ count += input.tools.filter((t) => t.cache_control).length;
79
+ }
80
+ for (const message of input.messages) {
81
+ if (Array.isArray(message.content)) {
82
+ count += message.content.filter((b) => b.cache_control).length;
83
+ }
84
+ }
85
+ return count;
86
+ }
87
+ /**
88
+ * Rolling history breakpoints for request paths whose stable-prefix markers
89
+ * are managed upstream (direct Anthropic: system via MessageBuilder, last
90
+ * tool via GenerationHandler). Marks the last content block of up to `budget`
91
+ * tail messages; a tail block that already carries a marker is left as-is
92
+ * without consuming budget (it already serves as that breakpoint). Pure —
93
+ * the input array is cloned, never mutated.
94
+ */
95
+ export function applyAnthropicHistoryCacheBreakpoints(input, budget) {
96
+ const messages = input.map((m) => ({ ...m }));
97
+ let remaining = Math.max(0, Math.min(budget, MAX_BREAKPOINTS));
98
+ for (let i = messages.length - 1; i >= 0 && remaining > 0; i--) {
99
+ if (lastContentBlockHasMarker(messages[i])) {
100
+ continue;
101
+ }
102
+ if (markLastContentBlock(messages, i)) {
103
+ remaining--;
104
+ }
105
+ }
106
+ return messages;
107
+ }
108
+ /** True when the last content block of a message already carries `cache_control`. */
109
+ function lastContentBlockHasMarker(message) {
110
+ if (!Array.isArray(message.content) || message.content.length === 0) {
111
+ return false;
112
+ }
113
+ const last = message.content[message.content.length - 1];
114
+ return !!last.cache_control;
115
+ }
63
116
  /**
64
117
  * Place a cache breakpoint on the last content block of `messages[i]`.
65
118
  * Anthropic attaches `cache_control` to a content block, not the message
@@ -30,12 +30,17 @@ export declare function extractTotalTokens(usage: RawUsageObject, input: number,
30
30
  export declare function extractReasoningTokens(usage: RawUsageObject): number | undefined;
31
31
  /**
32
32
  * Extract cache creation token count from various provider formats
33
- * Supports: cacheCreationInputTokens, cacheCreationTokens
33
+ * Supports: cacheCreationInputTokens, cacheCreationTokens, and the ai@6
34
+ * normalized `inputTokenDetails.cacheWriteTokens` (the only shape the native
35
+ * direct-Anthropic path reports through generateText).
34
36
  */
35
37
  export declare function extractCacheCreationTokens(usage: RawUsageObject): number | undefined;
36
38
  /**
37
39
  * Extract cache read token count from various provider formats
38
- * Supports: cacheReadInputTokens, cacheReadTokens
40
+ * Supports: cacheReadInputTokens, cacheReadTokens, and the ai@6 normalized
41
+ * shapes — flat `cachedInputTokens` and nested
42
+ * `inputTokenDetails.cacheReadTokens` (the only shapes the native
43
+ * direct-Anthropic path reports through generateText).
39
44
  */
40
45
  export declare function extractCacheReadTokens(usage: RawUsageObject): number | undefined;
41
46
  /**
@@ -74,7 +74,9 @@ export function extractReasoningTokens(usage) {
74
74
  }
75
75
  /**
76
76
  * Extract cache creation token count from various provider formats
77
- * Supports: cacheCreationInputTokens, cacheCreationTokens
77
+ * Supports: cacheCreationInputTokens, cacheCreationTokens, and the ai@6
78
+ * normalized `inputTokenDetails.cacheWriteTokens` (the only shape the native
79
+ * direct-Anthropic path reports through generateText).
78
80
  */
79
81
  export function extractCacheCreationTokens(usage) {
80
82
  if (typeof usage.cacheCreationInputTokens === "number" &&
@@ -85,11 +87,18 @@ export function extractCacheCreationTokens(usage) {
85
87
  usage.cacheCreationTokens > 0) {
86
88
  return usage.cacheCreationTokens;
87
89
  }
90
+ if (typeof usage.inputTokenDetails?.cacheWriteTokens === "number" &&
91
+ usage.inputTokenDetails.cacheWriteTokens > 0) {
92
+ return usage.inputTokenDetails.cacheWriteTokens;
93
+ }
88
94
  return undefined;
89
95
  }
90
96
  /**
91
97
  * Extract cache read token count from various provider formats
92
- * Supports: cacheReadInputTokens, cacheReadTokens
98
+ * Supports: cacheReadInputTokens, cacheReadTokens, and the ai@6 normalized
99
+ * shapes — flat `cachedInputTokens` and nested
100
+ * `inputTokenDetails.cacheReadTokens` (the only shapes the native
101
+ * direct-Anthropic path reports through generateText).
93
102
  */
94
103
  export function extractCacheReadTokens(usage) {
95
104
  if (typeof usage.cacheReadInputTokens === "number" &&
@@ -99,6 +108,14 @@ export function extractCacheReadTokens(usage) {
99
108
  if (typeof usage.cacheReadTokens === "number" && usage.cacheReadTokens > 0) {
100
109
  return usage.cacheReadTokens;
101
110
  }
111
+ if (typeof usage.cachedInputTokens === "number" &&
112
+ usage.cachedInputTokens > 0) {
113
+ return usage.cachedInputTokens;
114
+ }
115
+ if (typeof usage.inputTokenDetails?.cacheReadTokens === "number" &&
116
+ usage.inputTokenDetails.cacheReadTokens > 0) {
117
+ return usage.inputTokenDetails.cacheReadTokens;
118
+ }
102
119
  return undefined;
103
120
  }
104
121
  /**
@@ -13,6 +13,7 @@ import { createProxyFetch } from "../proxy/proxyFetch.js";
13
13
  import { AuthenticationError, NetworkError, ProviderError, RateLimitError, } from "../types/index.js";
14
14
  import { logger } from "../utils/logger.js";
15
15
  import { redactUrlCredentials } from "../utils/logSanitize.js";
16
+ import { ANTHROPIC_MAX_CACHE_BREAKPOINTS, applyAnthropicHistoryCacheBreakpoints, countAnthropicCacheMarkers, } from "../utils/anthropicCacheBreakpoints.js";
16
17
  import { calculateCost } from "../utils/pricing.js";
17
18
  import { createAnthropicConfig, getProviderModel, validateApiKey, } from "../utils/providerConfig.js";
18
19
  import { composeAbortSignals, createTimeoutController, mergeAbortSignals, TimeoutError, } from "../utils/timeout.js";
@@ -1123,9 +1124,23 @@ export class AnthropicProvider extends BaseProvider {
1123
1124
  }
1124
1125
  // Extended thinking passthrough (providerOptions.anthropic.thinking).
1125
1126
  const thinking = options.providerOptions?.anthropic?.thinking;
1127
+ // Prompt-cache parity with the native Vertex+Claude path: upstream
1128
+ // layers mark only the stable prefix (system via MessageBuilder,
1129
+ // last tool via GenerationHandler) — the growing conversation
1130
+ // history has no breakpoint, so on every turn it falls after the
1131
+ // last marker and is re-billed as fresh input. Add rolling history
1132
+ // breakpoints in whatever budget remains under Anthropic's
1133
+ // four-marker ceiling; pre-existing markers are counted so the
1134
+ // request can never exceed the cap.
1135
+ const cacheMarkersUsed = countAnthropicCacheMarkers({
1136
+ system,
1137
+ tools,
1138
+ messages: messages,
1139
+ });
1140
+ const cachedMessages = applyAnthropicHistoryCacheBreakpoints(messages, ANTHROPIC_MAX_CACHE_BREAKPOINTS - cacheMarkersUsed);
1126
1141
  const params = {
1127
1142
  model: modelId,
1128
- messages,
1143
+ messages: cachedMessages,
1129
1144
  max_tokens: resolveClaudeMaxTokens(modelId, options.maxOutputTokens),
1130
1145
  ...(system ? { system } : {}),
1131
1146
  ...(options.temperature !== undefined &&
@@ -1345,10 +1360,19 @@ export class AnthropicProvider extends BaseProvider {
1345
1360
  .then((usage) => {
1346
1361
  streamSpan.setAttribute("gen_ai.usage.input_tokens", usage.promptTokens || 0);
1347
1362
  streamSpan.setAttribute("gen_ai.usage.output_tokens", usage.completionTokens || 0);
1363
+ if (usage.cacheReadTokens) {
1364
+ streamSpan.setAttribute("gen_ai.usage.cached_input_tokens", usage.cacheReadTokens);
1365
+ }
1348
1366
  const cost = calculateCost(this.providerName, this.modelName, {
1349
1367
  input: usage.promptTokens || 0,
1350
1368
  output: usage.completionTokens || 0,
1351
1369
  total: usage.totalTokens || 0,
1370
+ ...(usage.cacheReadTokens
1371
+ ? { cacheReadTokens: usage.cacheReadTokens }
1372
+ : {}),
1373
+ ...(usage.cacheCreationTokens
1374
+ ? { cacheCreationTokens: usage.cacheCreationTokens }
1375
+ : {}),
1352
1376
  });
1353
1377
  if (cost && cost > 0) {
1354
1378
  streamSpan.setAttribute("neurolink.cost", cost);
@@ -1372,11 +1396,26 @@ export class AnthropicProvider extends BaseProvider {
1372
1396
  const conversation = payload.messages.slice();
1373
1397
  let totalInput = 0;
1374
1398
  let totalOutput = 0;
1399
+ let totalCacheRead = 0;
1400
+ let totalCacheWrite = 0;
1375
1401
  let lastStop = null;
1376
1402
  for (let step = 0; step < maxSteps; step++) {
1403
+ // Prompt-cache parity with the native Vertex+Claude path — rolling
1404
+ // history breakpoints, re-applied per step so the stable prefix
1405
+ // stays byte-identical while the breakpoint follows the growing
1406
+ // tail. Budget respects markers upstream layers already placed
1407
+ // (system / last tool / message blocks) so the request never
1408
+ // exceeds Anthropic's four-marker cap. Pure: `conversation` itself
1409
+ // is never mutated, so re-counting per step stays stable.
1410
+ const cacheMarkersUsed = countAnthropicCacheMarkers({
1411
+ system: payload.system,
1412
+ tools: anthropicTools,
1413
+ messages: conversation,
1414
+ });
1415
+ const cachedConversation = applyAnthropicHistoryCacheBreakpoints(conversation, ANTHROPIC_MAX_CACHE_BREAKPOINTS - cacheMarkersUsed);
1377
1416
  const params = {
1378
1417
  model: modelId,
1379
- messages: conversation,
1418
+ messages: cachedConversation,
1380
1419
  max_tokens: resolveClaudeMaxTokens(modelId, options.maxTokens),
1381
1420
  stream: true,
1382
1421
  ...(payload.system ? { system: payload.system } : {}),
@@ -1406,6 +1445,12 @@ export class AnthropicProvider extends BaseProvider {
1406
1445
  if (event.type === "message_start") {
1407
1446
  totalInput += event.message.usage.input_tokens ?? 0;
1408
1447
  totalOutput += event.message.usage.output_tokens ?? 0;
1448
+ // Anthropic reports cache reads/writes SEPARATELY from
1449
+ // input_tokens on the same message_start event — without these
1450
+ // the streaming path silently drops all cache accounting.
1451
+ totalCacheRead += event.message.usage.cache_read_input_tokens ?? 0;
1452
+ totalCacheWrite +=
1453
+ event.message.usage.cache_creation_input_tokens ?? 0;
1409
1454
  }
1410
1455
  else if (event.type === "content_block_start") {
1411
1456
  blockTypes.set(event.index, event.content_block.type);
@@ -1578,7 +1623,11 @@ export class AnthropicProvider extends BaseProvider {
1578
1623
  resolveUsage({
1579
1624
  promptTokens: totalInput,
1580
1625
  completionTokens: totalOutput,
1581
- totalTokens: totalInput + totalOutput,
1626
+ totalTokens: totalInput + totalCacheRead + totalCacheWrite + totalOutput,
1627
+ ...(totalCacheRead > 0 ? { cacheReadTokens: totalCacheRead } : {}),
1628
+ ...(totalCacheWrite > 0
1629
+ ? { cacheCreationTokens: totalCacheWrite }
1630
+ : {}),
1582
1631
  });
1583
1632
  resolveFinish(lastStop ?? "stop");
1584
1633
  };
@@ -13,7 +13,7 @@
13
13
  * Nothing here imports from "ai" or "@ai-sdk/*". The whole point of this
14
14
  * module is to be the native replacement for the AI SDK's OpenAI wrapper.
15
15
  */
16
- import type { OpenAICompatBuildBodyArgs, OpenAICompatChatMessage, OpenAICompatChatRequest, OpenAICompatChatTool, OpenAICompatMessage, OpenAICompatMessageContent, OpenAICompatResponseFormat, OpenAICompatSSEResult, OpenAICompatStreamChunk, OpenAICompatToolChoiceWire, OpenAICompatV3CallToolChoice, OpenAICompatV3CallTools, Tool } from "../types/index.js";
16
+ import type { OpenAICompatBuildBodyArgs, OpenAICompatChatMessage, OpenAICompatChatRequest, OpenAICompatChatTool, OpenAICompatMessage, OpenAICompatMessageContent, OpenAICompatResponseFormat, OpenAICompatSSEResult, OpenAICompatStreamChunk, OpenAICompatToolChoiceWire, OpenAICompatV3CallToolChoice, OpenAICompatV3CallTools, DeferredUsage, Tool } from "../types/index.js";
17
17
  export declare const stripTrailingSlash: (s: string) => string;
18
18
  export declare const safeStringify: (value: unknown) => string;
19
19
  export declare const stringifyToolInput: (input: unknown) => string;
@@ -38,17 +38,9 @@ export declare const buildBody: (args: OpenAICompatBuildBodyArgs) => OpenAICompa
38
38
  export declare const parseSSEStream: (body: ReadableStream<Uint8Array>, onTextDelta: (delta: string) => void, onReasoningDelta?: (delta: string) => void) => Promise<OpenAICompatSSEResult>;
39
39
  export declare const buildAPIError: (url: string, body: OpenAICompatChatRequest, res: Response) => Promise<Error>;
40
40
  export declare const createDeferredAnalytics: () => {
41
- usagePromise: Promise<{
42
- promptTokens: number;
43
- completionTokens: number;
44
- totalTokens: number;
45
- }>;
41
+ usagePromise: Promise<DeferredUsage>;
46
42
  finishPromise: Promise<string>;
47
- resolveUsage: (u: {
48
- promptTokens: number;
49
- completionTokens: number;
50
- totalTokens: number;
51
- }) => void;
43
+ resolveUsage: (u: DeferredUsage) => void;
52
44
  resolveFinish: (reason: string) => void;
53
45
  };
54
46
  export declare const createChunkQueue: () => {
@@ -223,6 +223,11 @@ export type RawUsageObject = {
223
223
  cacheReadInputTokens?: number;
224
224
  cacheCreationTokens?: number;
225
225
  cacheReadTokens?: number;
226
+ cachedInputTokens?: number;
227
+ inputTokenDetails?: {
228
+ cacheReadTokens?: number;
229
+ cacheWriteTokens?: number;
230
+ };
226
231
  prompt_tokens_details?: {
227
232
  cached_tokens?: number;
228
233
  };
@@ -232,6 +237,18 @@ export type RawUsageObject = {
232
237
  thinkingTokens?: number;
233
238
  usage?: RawUsageObject;
234
239
  };
240
+ /**
241
+ * Aggregated usage resolved by a provider's deferred-analytics pair after a
242
+ * multi-step stream loop ends. The cache fields are optional — only providers
243
+ * with prompt caching (Anthropic) populate them.
244
+ */
245
+ export type DeferredUsage = {
246
+ promptTokens: number;
247
+ completionTokens: number;
248
+ totalTokens: number;
249
+ cacheReadTokens?: number;
250
+ cacheCreationTokens?: number;
251
+ };
235
252
  /**
236
253
  * Options for token extraction from raw usage objects.
237
254
  */
@@ -1,4 +1,6 @@
1
- import type { VertexAnthropicCacheInput, VertexAnthropicCacheOutput } from "../types/index.js";
1
+ import type { VertexAnthropicCacheInput, VertexAnthropicCacheOutput, VertexAnthropicMessage } from "../types/index.js";
2
+ /** Anthropic allows at most four `cache_control` breakpoints per request. */
3
+ export declare const ANTHROPIC_MAX_CACHE_BREAKPOINTS = 4;
2
4
  /**
3
5
  * Annotate a native Vertex+Claude request with prompt-cache breakpoints.
4
6
  *
@@ -13,3 +15,29 @@ import type { VertexAnthropicCacheInput, VertexAnthropicCacheOutput } from "../t
13
15
  * Pure: the inputs are cloned, never mutated.
14
16
  */
15
17
  export declare function applyVertexAnthropicCacheBreakpoints(input: VertexAnthropicCacheInput): VertexAnthropicCacheOutput;
18
+ /**
19
+ * Count the `cache_control` markers already present on a request. The direct
20
+ * Anthropic path (AI-SDK pipeline) arrives with markers the upstream layers
21
+ * placed — MessageBuilder tags the system prompt, GenerationHandler tags the
22
+ * last tool definition, and message content blocks may carry translated
23
+ * AI-SDK markers. Anthropic rejects requests with more than four markers, so
24
+ * any additional history breakpoints must fit in the remaining budget.
25
+ */
26
+ export declare function countAnthropicCacheMarkers(input: {
27
+ system?: string | ReadonlyArray<{
28
+ cache_control?: unknown;
29
+ }> | undefined;
30
+ tools?: ReadonlyArray<{
31
+ cache_control?: unknown;
32
+ }> | undefined;
33
+ messages: ReadonlyArray<VertexAnthropicMessage>;
34
+ }): number;
35
+ /**
36
+ * Rolling history breakpoints for request paths whose stable-prefix markers
37
+ * are managed upstream (direct Anthropic: system via MessageBuilder, last
38
+ * tool via GenerationHandler). Marks the last content block of up to `budget`
39
+ * tail messages; a tail block that already carries a marker is left as-is
40
+ * without consuming budget (it already serves as that breakpoint). Pure —
41
+ * the input array is cloned, never mutated.
42
+ */
43
+ export declare function applyAnthropicHistoryCacheBreakpoints(input: ReadonlyArray<VertexAnthropicMessage>, budget: number): VertexAnthropicMessage[];
@@ -16,7 +16,8 @@
16
16
  */
17
17
  const EPHEMERAL = { type: "ephemeral" };
18
18
  /** Anthropic allows at most four `cache_control` breakpoints per request. */
19
- const MAX_BREAKPOINTS = 4;
19
+ export const ANTHROPIC_MAX_CACHE_BREAKPOINTS = 4;
20
+ const MAX_BREAKPOINTS = ANTHROPIC_MAX_CACHE_BREAKPOINTS;
20
21
  /**
21
22
  * Annotate a native Vertex+Claude request with prompt-cache breakpoints.
22
23
  *
@@ -60,6 +61,58 @@ export function applyVertexAnthropicCacheBreakpoints(input) {
60
61
  }
61
62
  return { system, tools, messages };
62
63
  }
64
+ /**
65
+ * Count the `cache_control` markers already present on a request. The direct
66
+ * Anthropic path (AI-SDK pipeline) arrives with markers the upstream layers
67
+ * placed — MessageBuilder tags the system prompt, GenerationHandler tags the
68
+ * last tool definition, and message content blocks may carry translated
69
+ * AI-SDK markers. Anthropic rejects requests with more than four markers, so
70
+ * any additional history breakpoints must fit in the remaining budget.
71
+ */
72
+ export function countAnthropicCacheMarkers(input) {
73
+ let count = 0;
74
+ if (Array.isArray(input.system)) {
75
+ count += input.system.filter((b) => b.cache_control).length;
76
+ }
77
+ if (input.tools) {
78
+ count += input.tools.filter((t) => t.cache_control).length;
79
+ }
80
+ for (const message of input.messages) {
81
+ if (Array.isArray(message.content)) {
82
+ count += message.content.filter((b) => b.cache_control).length;
83
+ }
84
+ }
85
+ return count;
86
+ }
87
+ /**
88
+ * Rolling history breakpoints for request paths whose stable-prefix markers
89
+ * are managed upstream (direct Anthropic: system via MessageBuilder, last
90
+ * tool via GenerationHandler). Marks the last content block of up to `budget`
91
+ * tail messages; a tail block that already carries a marker is left as-is
92
+ * without consuming budget (it already serves as that breakpoint). Pure —
93
+ * the input array is cloned, never mutated.
94
+ */
95
+ export function applyAnthropicHistoryCacheBreakpoints(input, budget) {
96
+ const messages = input.map((m) => ({ ...m }));
97
+ let remaining = Math.max(0, Math.min(budget, MAX_BREAKPOINTS));
98
+ for (let i = messages.length - 1; i >= 0 && remaining > 0; i--) {
99
+ if (lastContentBlockHasMarker(messages[i])) {
100
+ continue;
101
+ }
102
+ if (markLastContentBlock(messages, i)) {
103
+ remaining--;
104
+ }
105
+ }
106
+ return messages;
107
+ }
108
+ /** True when the last content block of a message already carries `cache_control`. */
109
+ function lastContentBlockHasMarker(message) {
110
+ if (!Array.isArray(message.content) || message.content.length === 0) {
111
+ return false;
112
+ }
113
+ const last = message.content[message.content.length - 1];
114
+ return !!last.cache_control;
115
+ }
63
116
  /**
64
117
  * Place a cache breakpoint on the last content block of `messages[i]`.
65
118
  * Anthropic attaches `cache_control` to a content block, not the message
@@ -30,12 +30,17 @@ export declare function extractTotalTokens(usage: RawUsageObject, input: number,
30
30
  export declare function extractReasoningTokens(usage: RawUsageObject): number | undefined;
31
31
  /**
32
32
  * Extract cache creation token count from various provider formats
33
- * Supports: cacheCreationInputTokens, cacheCreationTokens
33
+ * Supports: cacheCreationInputTokens, cacheCreationTokens, and the ai@6
34
+ * normalized `inputTokenDetails.cacheWriteTokens` (the only shape the native
35
+ * direct-Anthropic path reports through generateText).
34
36
  */
35
37
  export declare function extractCacheCreationTokens(usage: RawUsageObject): number | undefined;
36
38
  /**
37
39
  * Extract cache read token count from various provider formats
38
- * Supports: cacheReadInputTokens, cacheReadTokens
40
+ * Supports: cacheReadInputTokens, cacheReadTokens, and the ai@6 normalized
41
+ * shapes — flat `cachedInputTokens` and nested
42
+ * `inputTokenDetails.cacheReadTokens` (the only shapes the native
43
+ * direct-Anthropic path reports through generateText).
39
44
  */
40
45
  export declare function extractCacheReadTokens(usage: RawUsageObject): number | undefined;
41
46
  /**