@jaypie/llm 1.3.14 → 1.3.15

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.
@@ -501,6 +501,12 @@ function sumUsageByProviderModel(usage) {
501
501
  totals[key].output += item.output;
502
502
  totals[key].reasoning += item.reasoning;
503
503
  totals[key].total += item.total;
504
+ if (item.cacheRead !== undefined) {
505
+ totals[key].cacheRead = (totals[key].cacheRead ?? 0) + item.cacheRead;
506
+ }
507
+ if (item.cacheWrite !== undefined) {
508
+ totals[key].cacheWrite = (totals[key].cacheWrite ?? 0) + item.cacheWrite;
509
+ }
504
510
  }
505
511
  return totals;
506
512
  }
@@ -526,6 +532,7 @@ function buildExchangeEnvelope({ duration, initialHistoryLength, input, options,
526
532
  return {
527
533
  ids: extractResponseIds(response.responses),
528
534
  request: {
535
+ cache: options.cache,
529
536
  data: options.data,
530
537
  effort: options.effort,
531
538
  explain: options.explain,
@@ -563,6 +570,38 @@ function buildExchangeEnvelope({ duration, initialHistoryLength, input, options,
563
570
  };
564
571
  }
565
572
 
573
+ const CACHE_TTL_DEFAULT = "5m";
574
+ /**
575
+ * Normalize the caller-facing `cache` option into a concrete decision.
576
+ * - `undefined` / `true` → enabled at the default TTL
577
+ * - `false` / `0` → disabled
578
+ * - `"5m"` / `"1h"` → enabled at that TTL
579
+ */
580
+ function resolveCache(cache) {
581
+ if (cache === false || cache === 0) {
582
+ return { enabled: false, ttl: CACHE_TTL_DEFAULT };
583
+ }
584
+ if (cache === "5m" || cache === "1h") {
585
+ return { enabled: true, ttl: cache };
586
+ }
587
+ // undefined or true
588
+ return { enabled: true, ttl: CACHE_TTL_DEFAULT };
589
+ }
590
+ /**
591
+ * Deterministic short key for providers with automatic, prefix-based caching
592
+ * (e.g. OpenAI `prompt_cache_key`). Derived from the stable prefix so the same
593
+ * system prompt + tools + model always routes to the same cache. Dependency-
594
+ * free FNV-1a; not cryptographic.
595
+ */
596
+ function promptCacheKey(seed) {
597
+ let hash = 0x811c9dc5;
598
+ for (let i = 0; i < seed.length; i += 1) {
599
+ hash ^= seed.charCodeAt(i);
600
+ hash = Math.imul(hash, 0x01000193);
601
+ }
602
+ return `jaypie-${(hash >>> 0).toString(16)}`;
603
+ }
604
+
566
605
  /**
567
606
  * Type guard to check if an item is a dedicated reasoning item (OpenAI)
568
607
  */
@@ -1414,6 +1453,14 @@ function tallyOperate({ toolCallNames = [], turns, usage = [], }) {
1414
1453
  usageByModel[key].output += item.output;
1415
1454
  usageByModel[key].reasoning += item.reasoning;
1416
1455
  usageByModel[key].total += item.total;
1456
+ if (item.cacheRead !== undefined) {
1457
+ usageByModel[key].cacheRead =
1458
+ (usageByModel[key].cacheRead ?? 0) + item.cacheRead;
1459
+ }
1460
+ if (item.cacheWrite !== undefined) {
1461
+ usageByModel[key].cacheWrite =
1462
+ (usageByModel[key].cacheWrite ?? 0) + item.cacheWrite;
1463
+ }
1417
1464
  }
1418
1465
  llm.usage = usageByModel;
1419
1466
  }
@@ -1520,27 +1567,26 @@ const MODULE$1 = {
1520
1567
  //
1521
1568
  // Helpers
1522
1569
  //
1523
- // CJS/ESM compatible require - handles bundling to CJS where import.meta.url
1524
- // becomes undefined (mirrors observability/llmobs.ts).
1525
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
1526
- // @ts-ignore - __filename exists in CJS context when ESM is bundled to CJS
1527
- const requireModule$1 = typeof __filename !== "undefined"
1528
- ? module$1.createRequire(url.pathToFileURL(__filename).href)
1529
- : module$1.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)));
1570
+ // Native dynamic import that neither rollup nor tsc rewrites to require(), so
1571
+ // a CJS-bundled build still loads @jaypie/dynamodb's ESM entry and shares the
1572
+ // host's initialized module instance. A require()-based resolution would load
1573
+ // the CJS build, whose module-level client state is separate from the ESM
1574
+ // instance an ESM host initializes (dual-package hazard, issue #429).
1575
+ const dynamicImport = new Function("s", "return import(s)");
1530
1576
  let resolved$1 = false;
1531
1577
  let cachedSdk$2 = null;
1532
1578
  /**
1533
1579
  * Lazily resolve @jaypie/dynamodb's storeExchange. Returns null (and never
1534
1580
  * throws) when the peer is absent. Cached after the first attempt.
1535
1581
  */
1536
- function resolveExchangeStore() {
1582
+ async function resolveExchangeStore() {
1537
1583
  if (resolved$1) {
1538
1584
  return cachedSdk$2;
1539
1585
  }
1540
1586
  resolved$1 = true;
1541
1587
  try {
1542
- const dynamodb = requireModule$1(MODULE$1.JAYPIE_DYNAMODB);
1543
- const sdk = dynamodb?.default ?? dynamodb;
1588
+ const dynamodb = await dynamicImport(MODULE$1.JAYPIE_DYNAMODB);
1589
+ const sdk = (dynamodb?.default ?? dynamodb);
1544
1590
  if (sdk && typeof sdk.storeExchange === "function") {
1545
1591
  cachedSdk$2 = sdk;
1546
1592
  }
@@ -1563,7 +1609,7 @@ async function persistExchange(envelope) {
1563
1609
  if (!isExchangeStoreEnabled()) {
1564
1610
  return;
1565
1611
  }
1566
- const sdk = resolveExchangeStore();
1612
+ const sdk = (await resolveExchangeStore());
1567
1613
  if (!sdk) {
1568
1614
  return;
1569
1615
  }
@@ -2351,8 +2397,19 @@ class AnthropicAdapter extends BaseProviderAdapter {
2351
2397
  PROVIDER.ANTHROPIC.MAX_TOKENS.DEFAULT,
2352
2398
  stream: false,
2353
2399
  };
2400
+ const cache = resolveCache(request.cache);
2401
+ const cacheControl = cache.enabled
2402
+ ? {
2403
+ type: "ephemeral",
2404
+ ...(cache.ttl === "1h" ? { ttl: "1h" } : {}),
2405
+ }
2406
+ : undefined;
2354
2407
  if (request.system) {
2355
- anthropicRequest.system = request.system;
2408
+ // A cache breakpoint on the system block caches tools+system together
2409
+ // (render order is tools -> system -> messages).
2410
+ anthropicRequest.system = cacheControl
2411
+ ? [{ type: "text", text: request.system, cache_control: cacheControl }]
2412
+ : request.system;
2356
2413
  }
2357
2414
  const useFallbackStructuredOutput = Boolean(request.format) &&
2358
2415
  !this.supportsStructuredOutput(anthropicRequest.model);
@@ -2369,7 +2426,7 @@ class AnthropicAdapter extends BaseProviderAdapter {
2369
2426
  });
2370
2427
  }
2371
2428
  if (allTools.length > 0) {
2372
- anthropicRequest.tools = allTools.map((tool) => ({
2429
+ anthropicRequest.tools = allTools.map((tool, index) => ({
2373
2430
  name: tool.name,
2374
2431
  description: tool.description,
2375
2432
  input_schema: {
@@ -2377,6 +2434,11 @@ class AnthropicAdapter extends BaseProviderAdapter {
2377
2434
  type: "object",
2378
2435
  },
2379
2436
  type: "custom",
2437
+ // Breakpoint on the last tool caches the tool list (which renders
2438
+ // before system) when there is no system prompt to anchor it.
2439
+ ...(cacheControl && index === allTools.length - 1
2440
+ ? { cache_control: cacheControl }
2441
+ : {}),
2380
2442
  }));
2381
2443
  anthropicRequest.tool_choice = useFallbackStructuredOutput
2382
2444
  ? { type: "any" }
@@ -2679,6 +2741,12 @@ class AnthropicAdapter extends BaseProviderAdapter {
2679
2741
  output: usage.output_tokens,
2680
2742
  reasoning: usage.thinking_tokens || 0,
2681
2743
  total: usage.input_tokens + usage.output_tokens,
2744
+ ...(usage.cache_read_input_tokens !== undefined
2745
+ ? { cacheRead: usage.cache_read_input_tokens }
2746
+ : {}),
2747
+ ...(usage.cache_creation_input_tokens !== undefined
2748
+ ? { cacheWrite: usage.cache_creation_input_tokens }
2749
+ : {}),
2682
2750
  provider: this.name,
2683
2751
  model,
2684
2752
  };
@@ -2931,6 +2999,14 @@ function isTemperatureDeprecationError$3(error) {
2931
2999
  const msg = error?.message ?? "";
2932
3000
  return /temperature.*deprecated|deprecated.*temperature/i.test(msg);
2933
3001
  }
3002
+ function isCachePointUnsupportedError(error) {
3003
+ const name = error?.constructor?.name ?? "";
3004
+ const msg = error?.message ?? "";
3005
+ // A model that cannot cache rejects the cachePoint block with a
3006
+ // ValidationException naming caching / cachePoint.
3007
+ return (/ValidationException|invalid|not support/i.test(name + " " + msg) &&
3008
+ /cachePoint|cache_point|prompt caching|caching/i.test(msg));
3009
+ }
2934
3010
  function extractJson(text) {
2935
3011
  // Try direct parse first
2936
3012
  try {
@@ -2966,6 +3042,13 @@ class BedrockAdapter extends BaseProviderAdapter {
2966
3042
  this.defaultModel = PROVIDER.BEDROCK.DEFAULT;
2967
3043
  this._modelsFallbackToStructuredOutputTool = new Set();
2968
3044
  this._modelsWithoutTemperature = new Set();
3045
+ this._modelsWithoutCachePoint = new Set();
3046
+ }
3047
+ rememberModelRejectsCachePoint(model) {
3048
+ this._modelsWithoutCachePoint.add(model);
3049
+ }
3050
+ supportsCachePoint(model) {
3051
+ return !this._modelsWithoutCachePoint.has(model);
2969
3052
  }
2970
3053
  rememberModelRejectsOutputConfig(model) {
2971
3054
  this._modelsFallbackToStructuredOutputTool.add(model);
@@ -3102,11 +3185,51 @@ class BedrockAdapter extends BaseProviderAdapter {
3102
3185
  };
3103
3186
  }
3104
3187
  }
3188
+ // Prompt caching via Converse cachePoint blocks (5-min default TTL; the
3189
+ // `ttl` option does not apply). Gated per model — cachePoint 400s on
3190
+ // unsupported models, which executeRequest catches and denylists.
3191
+ if (resolveCache(request.cache).enabled && this.supportsCachePoint(model)) {
3192
+ const cachePoint = { cachePoint: { type: "default" } };
3193
+ if (bedrockRequest.system && bedrockRequest.system.length > 0) {
3194
+ bedrockRequest.system = [
3195
+ ...bedrockRequest.system,
3196
+ cachePoint,
3197
+ ];
3198
+ }
3199
+ if (bedrockRequest.toolConfig?.tools?.length) {
3200
+ bedrockRequest.toolConfig = {
3201
+ ...bedrockRequest.toolConfig,
3202
+ tools: [
3203
+ ...bedrockRequest.toolConfig.tools,
3204
+ cachePoint,
3205
+ ],
3206
+ };
3207
+ }
3208
+ }
3105
3209
  if (request.providerOptions) {
3106
3210
  Object.assign(bedrockRequest, request.providerOptions);
3107
3211
  }
3108
3212
  return bedrockRequest;
3109
3213
  }
3214
+ /** Remove any cachePoint blocks so the request can be retried unsupported. */
3215
+ stripCachePoints(request) {
3216
+ const stripped = { ...request };
3217
+ if (stripped.system) {
3218
+ stripped.system = stripped.system.filter((block) => !("cachePoint" in block));
3219
+ }
3220
+ if (stripped.toolConfig?.tools) {
3221
+ stripped.toolConfig = {
3222
+ ...stripped.toolConfig,
3223
+ tools: stripped.toolConfig.tools.filter((tool) => !("cachePoint" in tool)),
3224
+ };
3225
+ }
3226
+ return stripped;
3227
+ }
3228
+ requestHasCachePoints(request) {
3229
+ const inSystem = (request.system ?? []).some((block) => "cachePoint" in block);
3230
+ const inTools = (request.toolConfig?.tools ?? []).some((tool) => "cachePoint" in tool);
3231
+ return inSystem || inTools;
3232
+ }
3110
3233
  formatTools(toolkit) {
3111
3234
  return toolkit.tools.map((tool) => ({
3112
3235
  name: tool.name,
@@ -3161,6 +3284,15 @@ class BedrockAdapter extends BaseProviderAdapter {
3161
3284
  const fallbackRequest = this.toFallbackStructuredOutputRequest(bedrockRequest);
3162
3285
  return (await bedrockClient.send(new ConverseCommand(fallbackRequest), signal ? { abortSignal: signal } : undefined));
3163
3286
  }
3287
+ if (this.requestHasCachePoints(bedrockRequest) &&
3288
+ isCachePointUnsupportedError(error)) {
3289
+ this.rememberModelRejectsCachePoint(bedrockRequest.modelId || this.defaultModel);
3290
+ const retryRequest = this.stripCachePoints(bedrockRequest);
3291
+ const response = (await bedrockClient.send(new ConverseCommand(retryRequest), signal ? { abortSignal: signal } : undefined));
3292
+ if (wantsStructuredOutput)
3293
+ response.__jaypieStructuredOutput = true;
3294
+ return response;
3295
+ }
3164
3296
  throw error;
3165
3297
  }
3166
3298
  }
@@ -3304,6 +3436,12 @@ class BedrockAdapter extends BaseProviderAdapter {
3304
3436
  reasoning: 0,
3305
3437
  total: usage?.totalTokens ??
3306
3438
  (usage?.inputTokens ?? 0) + (usage?.outputTokens ?? 0),
3439
+ ...(usage?.cacheReadInputTokens !== undefined
3440
+ ? { cacheRead: usage.cacheReadInputTokens }
3441
+ : {}),
3442
+ ...(usage?.cacheWriteInputTokens !== undefined
3443
+ ? { cacheWrite: usage.cacheWriteInputTokens }
3444
+ : {}),
3307
3445
  provider: this.name,
3308
3446
  model,
3309
3447
  };
@@ -5475,6 +5613,13 @@ class OpenAiAdapter extends BaseProviderAdapter {
5475
5613
  supportsReasoningEffort(model) {
5476
5614
  return isReasoningModel(model);
5477
5615
  }
5616
+ /**
5617
+ * Whether to emit `prompt_cache_key` (OpenAI Responses API). Overridable by
5618
+ * OpenAI-compatible subclasses whose backend rejects the field.
5619
+ */
5620
+ supportsPromptCacheKey() {
5621
+ return true;
5622
+ }
5478
5623
  /** Translate a normalized effort to this provider's `reasoning.effort` value. */
5479
5624
  mapReasoningEffort(effort, model) {
5480
5625
  return toOpenAiEffort(effort, { model });
@@ -5514,6 +5659,18 @@ class OpenAiAdapter extends BaseProviderAdapter {
5514
5659
  summary: "auto",
5515
5660
  };
5516
5661
  }
5662
+ // OpenAI prompt caching is automatic (prefix-based). Setting a stable
5663
+ // prompt_cache_key routes repeat traffic to the same cache across
5664
+ // instances. Keyed on the stable prefix only (system + instructions +
5665
+ // tools + model), never the volatile user turn.
5666
+ if (this.supportsPromptCacheKey() && resolveCache(request.cache).enabled) {
5667
+ openaiRequest.prompt_cache_key = promptCacheKey(JSON.stringify([
5668
+ model,
5669
+ request.system ?? "",
5670
+ request.instructions ?? "",
5671
+ request.tools ?? [],
5672
+ ]));
5673
+ }
5517
5674
  if (request.providerOptions) {
5518
5675
  Object.assign(openaiRequest, request.providerOptions);
5519
5676
  }
@@ -5772,11 +5929,13 @@ class OpenAiAdapter extends BaseProviderAdapter {
5772
5929
  model,
5773
5930
  };
5774
5931
  }
5932
+ const cachedTokens = openaiResponse.usage.input_tokens_details?.cached_tokens;
5775
5933
  return {
5776
5934
  input: openaiResponse.usage.input_tokens || 0,
5777
5935
  output: openaiResponse.usage.output_tokens || 0,
5778
5936
  reasoning: openaiResponse.usage.output_tokens_details?.reasoning_tokens || 0,
5779
5937
  total: openaiResponse.usage.total_tokens || 0,
5938
+ ...(cachedTokens !== undefined ? { cacheRead: cachedTokens } : {}),
5780
5939
  provider: this.name,
5781
5940
  model,
5782
5941
  };
@@ -6168,7 +6327,13 @@ class OpenRouterAdapter extends BaseProviderAdapter {
6168
6327
  //
6169
6328
  buildRequest(request) {
6170
6329
  // Convert messages to OpenRouter format (OpenAI-compatible)
6171
- const messages = this.convertMessagesToOpenRouter(request.messages, request.system);
6330
+ const cache = resolveCache(request.cache);
6331
+ const messages = this.convertMessagesToOpenRouter(request.messages, request.system, cache.enabled
6332
+ ? {
6333
+ type: "ephemeral",
6334
+ ...(cache.ttl === "1h" ? { ttl: "1h" } : {}),
6335
+ }
6336
+ : undefined);
6172
6337
  // Append instructions to last message if provided
6173
6338
  if (request.instructions && messages.length > 0) {
6174
6339
  const lastMsg = messages[messages.length - 1];
@@ -6532,11 +6697,15 @@ class OpenRouterAdapter extends BaseProviderAdapter {
6532
6697
  }
6533
6698
  // SDK returns camelCase, but support snake_case as fallback
6534
6699
  const usage = openRouterResponse.usage;
6700
+ const cachedTokens = usage.promptTokensDetails?.cachedTokens ??
6701
+ usage.promptTokensDetails?.cached_tokens ??
6702
+ usage.prompt_tokens_details?.cached_tokens;
6535
6703
  return {
6536
6704
  input: usage.promptTokens || usage.prompt_tokens || 0,
6537
6705
  output: usage.completionTokens || usage.completion_tokens || 0,
6538
6706
  reasoning: usage.completionTokensDetails?.reasoningTokens || 0,
6539
6707
  total: usage.totalTokens || usage.total_tokens || 0,
6708
+ ...(cachedTokens !== undefined ? { cacheRead: cachedTokens } : {}),
6540
6709
  provider: this.name,
6541
6710
  model,
6542
6711
  };
@@ -6737,13 +6906,17 @@ class OpenRouterAdapter extends BaseProviderAdapter {
6737
6906
  const choice = response.choices[0];
6738
6907
  return choice?.message?.content ?? undefined;
6739
6908
  }
6740
- convertMessagesToOpenRouter(messages, system) {
6909
+ convertMessagesToOpenRouter(messages, system, cacheControl) {
6741
6910
  const openRouterMessages = [];
6742
- // Add system message if provided
6911
+ // Add system message if provided. A cache_control breakpoint on the system
6912
+ // content is forwarded by OpenRouter to Anthropic/Gemini backends (and
6913
+ // ignored by others), caching the stable system prefix.
6743
6914
  if (system) {
6744
6915
  openRouterMessages.push({
6745
6916
  role: "system",
6746
- content: system,
6917
+ content: cacheControl
6918
+ ? [{ type: "text", text: system, cache_control: cacheControl }]
6919
+ : system,
6747
6920
  });
6748
6921
  }
6749
6922
  for (const msg of messages) {
@@ -8455,6 +8628,7 @@ class OperateLoop {
8455
8628
  }
8456
8629
  buildInitialRequest(state, options) {
8457
8630
  return {
8631
+ cache: options.cache,
8458
8632
  effort: options.effort,
8459
8633
  format: state.formattedFormat,
8460
8634
  instructions: options.instructions,
@@ -9088,6 +9262,7 @@ class StreamLoop {
9088
9262
  }
9089
9263
  buildInitialRequest(state, options) {
9090
9264
  return {
9265
+ cache: options.cache,
9091
9266
  effort: options.effort,
9092
9267
  format: state.formattedFormat,
9093
9268
  instructions: options.instructions,