@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.
package/dist/esm/index.js CHANGED
@@ -498,6 +498,12 @@ function sumUsageByProviderModel(usage) {
498
498
  totals[key].output += item.output;
499
499
  totals[key].reasoning += item.reasoning;
500
500
  totals[key].total += item.total;
501
+ if (item.cacheRead !== undefined) {
502
+ totals[key].cacheRead = (totals[key].cacheRead ?? 0) + item.cacheRead;
503
+ }
504
+ if (item.cacheWrite !== undefined) {
505
+ totals[key].cacheWrite = (totals[key].cacheWrite ?? 0) + item.cacheWrite;
506
+ }
501
507
  }
502
508
  return totals;
503
509
  }
@@ -523,6 +529,7 @@ function buildExchangeEnvelope({ duration, initialHistoryLength, input, options,
523
529
  return {
524
530
  ids: extractResponseIds(response.responses),
525
531
  request: {
532
+ cache: options.cache,
526
533
  data: options.data,
527
534
  effort: options.effort,
528
535
  explain: options.explain,
@@ -560,6 +567,38 @@ function buildExchangeEnvelope({ duration, initialHistoryLength, input, options,
560
567
  };
561
568
  }
562
569
 
570
+ const CACHE_TTL_DEFAULT = "5m";
571
+ /**
572
+ * Normalize the caller-facing `cache` option into a concrete decision.
573
+ * - `undefined` / `true` → enabled at the default TTL
574
+ * - `false` / `0` → disabled
575
+ * - `"5m"` / `"1h"` → enabled at that TTL
576
+ */
577
+ function resolveCache(cache) {
578
+ if (cache === false || cache === 0) {
579
+ return { enabled: false, ttl: CACHE_TTL_DEFAULT };
580
+ }
581
+ if (cache === "5m" || cache === "1h") {
582
+ return { enabled: true, ttl: cache };
583
+ }
584
+ // undefined or true
585
+ return { enabled: true, ttl: CACHE_TTL_DEFAULT };
586
+ }
587
+ /**
588
+ * Deterministic short key for providers with automatic, prefix-based caching
589
+ * (e.g. OpenAI `prompt_cache_key`). Derived from the stable prefix so the same
590
+ * system prompt + tools + model always routes to the same cache. Dependency-
591
+ * free FNV-1a; not cryptographic.
592
+ */
593
+ function promptCacheKey(seed) {
594
+ let hash = 0x811c9dc5;
595
+ for (let i = 0; i < seed.length; i += 1) {
596
+ hash ^= seed.charCodeAt(i);
597
+ hash = Math.imul(hash, 0x01000193);
598
+ }
599
+ return `jaypie-${(hash >>> 0).toString(16)}`;
600
+ }
601
+
563
602
  /**
564
603
  * Type guard to check if an item is a dedicated reasoning item (OpenAI)
565
604
  */
@@ -1411,6 +1450,14 @@ function tallyOperate({ toolCallNames = [], turns, usage = [], }) {
1411
1450
  usageByModel[key].output += item.output;
1412
1451
  usageByModel[key].reasoning += item.reasoning;
1413
1452
  usageByModel[key].total += item.total;
1453
+ if (item.cacheRead !== undefined) {
1454
+ usageByModel[key].cacheRead =
1455
+ (usageByModel[key].cacheRead ?? 0) + item.cacheRead;
1456
+ }
1457
+ if (item.cacheWrite !== undefined) {
1458
+ usageByModel[key].cacheWrite =
1459
+ (usageByModel[key].cacheWrite ?? 0) + item.cacheWrite;
1460
+ }
1414
1461
  }
1415
1462
  llm.usage = usageByModel;
1416
1463
  }
@@ -1517,27 +1564,26 @@ const MODULE$1 = {
1517
1564
  //
1518
1565
  // Helpers
1519
1566
  //
1520
- // CJS/ESM compatible require - handles bundling to CJS where import.meta.url
1521
- // becomes undefined (mirrors observability/llmobs.ts).
1522
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
1523
- // @ts-ignore - __filename exists in CJS context when ESM is bundled to CJS
1524
- const requireModule$1 = typeof __filename !== "undefined"
1525
- ? createRequire(pathToFileURL(__filename).href)
1526
- : createRequire(import.meta.url);
1567
+ // Native dynamic import that neither rollup nor tsc rewrites to require(), so
1568
+ // a CJS-bundled build still loads @jaypie/dynamodb's ESM entry and shares the
1569
+ // host's initialized module instance. A require()-based resolution would load
1570
+ // the CJS build, whose module-level client state is separate from the ESM
1571
+ // instance an ESM host initializes (dual-package hazard, issue #429).
1572
+ const dynamicImport = new Function("s", "return import(s)");
1527
1573
  let resolved$1 = false;
1528
1574
  let cachedSdk$2 = null;
1529
1575
  /**
1530
1576
  * Lazily resolve @jaypie/dynamodb's storeExchange. Returns null (and never
1531
1577
  * throws) when the peer is absent. Cached after the first attempt.
1532
1578
  */
1533
- function resolveExchangeStore() {
1579
+ async function resolveExchangeStore() {
1534
1580
  if (resolved$1) {
1535
1581
  return cachedSdk$2;
1536
1582
  }
1537
1583
  resolved$1 = true;
1538
1584
  try {
1539
- const dynamodb = requireModule$1(MODULE$1.JAYPIE_DYNAMODB);
1540
- const sdk = dynamodb?.default ?? dynamodb;
1585
+ const dynamodb = await dynamicImport(MODULE$1.JAYPIE_DYNAMODB);
1586
+ const sdk = (dynamodb?.default ?? dynamodb);
1541
1587
  if (sdk && typeof sdk.storeExchange === "function") {
1542
1588
  cachedSdk$2 = sdk;
1543
1589
  }
@@ -1560,7 +1606,7 @@ async function persistExchange(envelope) {
1560
1606
  if (!isExchangeStoreEnabled()) {
1561
1607
  return;
1562
1608
  }
1563
- const sdk = resolveExchangeStore();
1609
+ const sdk = (await resolveExchangeStore());
1564
1610
  if (!sdk) {
1565
1611
  return;
1566
1612
  }
@@ -2348,8 +2394,19 @@ class AnthropicAdapter extends BaseProviderAdapter {
2348
2394
  PROVIDER.ANTHROPIC.MAX_TOKENS.DEFAULT,
2349
2395
  stream: false,
2350
2396
  };
2397
+ const cache = resolveCache(request.cache);
2398
+ const cacheControl = cache.enabled
2399
+ ? {
2400
+ type: "ephemeral",
2401
+ ...(cache.ttl === "1h" ? { ttl: "1h" } : {}),
2402
+ }
2403
+ : undefined;
2351
2404
  if (request.system) {
2352
- anthropicRequest.system = request.system;
2405
+ // A cache breakpoint on the system block caches tools+system together
2406
+ // (render order is tools -> system -> messages).
2407
+ anthropicRequest.system = cacheControl
2408
+ ? [{ type: "text", text: request.system, cache_control: cacheControl }]
2409
+ : request.system;
2353
2410
  }
2354
2411
  const useFallbackStructuredOutput = Boolean(request.format) &&
2355
2412
  !this.supportsStructuredOutput(anthropicRequest.model);
@@ -2366,7 +2423,7 @@ class AnthropicAdapter extends BaseProviderAdapter {
2366
2423
  });
2367
2424
  }
2368
2425
  if (allTools.length > 0) {
2369
- anthropicRequest.tools = allTools.map((tool) => ({
2426
+ anthropicRequest.tools = allTools.map((tool, index) => ({
2370
2427
  name: tool.name,
2371
2428
  description: tool.description,
2372
2429
  input_schema: {
@@ -2374,6 +2431,11 @@ class AnthropicAdapter extends BaseProviderAdapter {
2374
2431
  type: "object",
2375
2432
  },
2376
2433
  type: "custom",
2434
+ // Breakpoint on the last tool caches the tool list (which renders
2435
+ // before system) when there is no system prompt to anchor it.
2436
+ ...(cacheControl && index === allTools.length - 1
2437
+ ? { cache_control: cacheControl }
2438
+ : {}),
2377
2439
  }));
2378
2440
  anthropicRequest.tool_choice = useFallbackStructuredOutput
2379
2441
  ? { type: "any" }
@@ -2676,6 +2738,12 @@ class AnthropicAdapter extends BaseProviderAdapter {
2676
2738
  output: usage.output_tokens,
2677
2739
  reasoning: usage.thinking_tokens || 0,
2678
2740
  total: usage.input_tokens + usage.output_tokens,
2741
+ ...(usage.cache_read_input_tokens !== undefined
2742
+ ? { cacheRead: usage.cache_read_input_tokens }
2743
+ : {}),
2744
+ ...(usage.cache_creation_input_tokens !== undefined
2745
+ ? { cacheWrite: usage.cache_creation_input_tokens }
2746
+ : {}),
2679
2747
  provider: this.name,
2680
2748
  model,
2681
2749
  };
@@ -2928,6 +2996,14 @@ function isTemperatureDeprecationError$3(error) {
2928
2996
  const msg = error?.message ?? "";
2929
2997
  return /temperature.*deprecated|deprecated.*temperature/i.test(msg);
2930
2998
  }
2999
+ function isCachePointUnsupportedError(error) {
3000
+ const name = error?.constructor?.name ?? "";
3001
+ const msg = error?.message ?? "";
3002
+ // A model that cannot cache rejects the cachePoint block with a
3003
+ // ValidationException naming caching / cachePoint.
3004
+ return (/ValidationException|invalid|not support/i.test(name + " " + msg) &&
3005
+ /cachePoint|cache_point|prompt caching|caching/i.test(msg));
3006
+ }
2931
3007
  function extractJson(text) {
2932
3008
  // Try direct parse first
2933
3009
  try {
@@ -2963,6 +3039,13 @@ class BedrockAdapter extends BaseProviderAdapter {
2963
3039
  this.defaultModel = PROVIDER.BEDROCK.DEFAULT;
2964
3040
  this._modelsFallbackToStructuredOutputTool = new Set();
2965
3041
  this._modelsWithoutTemperature = new Set();
3042
+ this._modelsWithoutCachePoint = new Set();
3043
+ }
3044
+ rememberModelRejectsCachePoint(model) {
3045
+ this._modelsWithoutCachePoint.add(model);
3046
+ }
3047
+ supportsCachePoint(model) {
3048
+ return !this._modelsWithoutCachePoint.has(model);
2966
3049
  }
2967
3050
  rememberModelRejectsOutputConfig(model) {
2968
3051
  this._modelsFallbackToStructuredOutputTool.add(model);
@@ -3099,11 +3182,51 @@ class BedrockAdapter extends BaseProviderAdapter {
3099
3182
  };
3100
3183
  }
3101
3184
  }
3185
+ // Prompt caching via Converse cachePoint blocks (5-min default TTL; the
3186
+ // `ttl` option does not apply). Gated per model — cachePoint 400s on
3187
+ // unsupported models, which executeRequest catches and denylists.
3188
+ if (resolveCache(request.cache).enabled && this.supportsCachePoint(model)) {
3189
+ const cachePoint = { cachePoint: { type: "default" } };
3190
+ if (bedrockRequest.system && bedrockRequest.system.length > 0) {
3191
+ bedrockRequest.system = [
3192
+ ...bedrockRequest.system,
3193
+ cachePoint,
3194
+ ];
3195
+ }
3196
+ if (bedrockRequest.toolConfig?.tools?.length) {
3197
+ bedrockRequest.toolConfig = {
3198
+ ...bedrockRequest.toolConfig,
3199
+ tools: [
3200
+ ...bedrockRequest.toolConfig.tools,
3201
+ cachePoint,
3202
+ ],
3203
+ };
3204
+ }
3205
+ }
3102
3206
  if (request.providerOptions) {
3103
3207
  Object.assign(bedrockRequest, request.providerOptions);
3104
3208
  }
3105
3209
  return bedrockRequest;
3106
3210
  }
3211
+ /** Remove any cachePoint blocks so the request can be retried unsupported. */
3212
+ stripCachePoints(request) {
3213
+ const stripped = { ...request };
3214
+ if (stripped.system) {
3215
+ stripped.system = stripped.system.filter((block) => !("cachePoint" in block));
3216
+ }
3217
+ if (stripped.toolConfig?.tools) {
3218
+ stripped.toolConfig = {
3219
+ ...stripped.toolConfig,
3220
+ tools: stripped.toolConfig.tools.filter((tool) => !("cachePoint" in tool)),
3221
+ };
3222
+ }
3223
+ return stripped;
3224
+ }
3225
+ requestHasCachePoints(request) {
3226
+ const inSystem = (request.system ?? []).some((block) => "cachePoint" in block);
3227
+ const inTools = (request.toolConfig?.tools ?? []).some((tool) => "cachePoint" in tool);
3228
+ return inSystem || inTools;
3229
+ }
3107
3230
  formatTools(toolkit) {
3108
3231
  return toolkit.tools.map((tool) => ({
3109
3232
  name: tool.name,
@@ -3158,6 +3281,15 @@ class BedrockAdapter extends BaseProviderAdapter {
3158
3281
  const fallbackRequest = this.toFallbackStructuredOutputRequest(bedrockRequest);
3159
3282
  return (await bedrockClient.send(new ConverseCommand(fallbackRequest), signal ? { abortSignal: signal } : undefined));
3160
3283
  }
3284
+ if (this.requestHasCachePoints(bedrockRequest) &&
3285
+ isCachePointUnsupportedError(error)) {
3286
+ this.rememberModelRejectsCachePoint(bedrockRequest.modelId || this.defaultModel);
3287
+ const retryRequest = this.stripCachePoints(bedrockRequest);
3288
+ const response = (await bedrockClient.send(new ConverseCommand(retryRequest), signal ? { abortSignal: signal } : undefined));
3289
+ if (wantsStructuredOutput)
3290
+ response.__jaypieStructuredOutput = true;
3291
+ return response;
3292
+ }
3161
3293
  throw error;
3162
3294
  }
3163
3295
  }
@@ -3301,6 +3433,12 @@ class BedrockAdapter extends BaseProviderAdapter {
3301
3433
  reasoning: 0,
3302
3434
  total: usage?.totalTokens ??
3303
3435
  (usage?.inputTokens ?? 0) + (usage?.outputTokens ?? 0),
3436
+ ...(usage?.cacheReadInputTokens !== undefined
3437
+ ? { cacheRead: usage.cacheReadInputTokens }
3438
+ : {}),
3439
+ ...(usage?.cacheWriteInputTokens !== undefined
3440
+ ? { cacheWrite: usage.cacheWriteInputTokens }
3441
+ : {}),
3304
3442
  provider: this.name,
3305
3443
  model,
3306
3444
  };
@@ -5472,6 +5610,13 @@ class OpenAiAdapter extends BaseProviderAdapter {
5472
5610
  supportsReasoningEffort(model) {
5473
5611
  return isReasoningModel(model);
5474
5612
  }
5613
+ /**
5614
+ * Whether to emit `prompt_cache_key` (OpenAI Responses API). Overridable by
5615
+ * OpenAI-compatible subclasses whose backend rejects the field.
5616
+ */
5617
+ supportsPromptCacheKey() {
5618
+ return true;
5619
+ }
5475
5620
  /** Translate a normalized effort to this provider's `reasoning.effort` value. */
5476
5621
  mapReasoningEffort(effort, model) {
5477
5622
  return toOpenAiEffort(effort, { model });
@@ -5511,6 +5656,18 @@ class OpenAiAdapter extends BaseProviderAdapter {
5511
5656
  summary: "auto",
5512
5657
  };
5513
5658
  }
5659
+ // OpenAI prompt caching is automatic (prefix-based). Setting a stable
5660
+ // prompt_cache_key routes repeat traffic to the same cache across
5661
+ // instances. Keyed on the stable prefix only (system + instructions +
5662
+ // tools + model), never the volatile user turn.
5663
+ if (this.supportsPromptCacheKey() && resolveCache(request.cache).enabled) {
5664
+ openaiRequest.prompt_cache_key = promptCacheKey(JSON.stringify([
5665
+ model,
5666
+ request.system ?? "",
5667
+ request.instructions ?? "",
5668
+ request.tools ?? [],
5669
+ ]));
5670
+ }
5514
5671
  if (request.providerOptions) {
5515
5672
  Object.assign(openaiRequest, request.providerOptions);
5516
5673
  }
@@ -5769,11 +5926,13 @@ class OpenAiAdapter extends BaseProviderAdapter {
5769
5926
  model,
5770
5927
  };
5771
5928
  }
5929
+ const cachedTokens = openaiResponse.usage.input_tokens_details?.cached_tokens;
5772
5930
  return {
5773
5931
  input: openaiResponse.usage.input_tokens || 0,
5774
5932
  output: openaiResponse.usage.output_tokens || 0,
5775
5933
  reasoning: openaiResponse.usage.output_tokens_details?.reasoning_tokens || 0,
5776
5934
  total: openaiResponse.usage.total_tokens || 0,
5935
+ ...(cachedTokens !== undefined ? { cacheRead: cachedTokens } : {}),
5777
5936
  provider: this.name,
5778
5937
  model,
5779
5938
  };
@@ -6165,7 +6324,13 @@ class OpenRouterAdapter extends BaseProviderAdapter {
6165
6324
  //
6166
6325
  buildRequest(request) {
6167
6326
  // Convert messages to OpenRouter format (OpenAI-compatible)
6168
- const messages = this.convertMessagesToOpenRouter(request.messages, request.system);
6327
+ const cache = resolveCache(request.cache);
6328
+ const messages = this.convertMessagesToOpenRouter(request.messages, request.system, cache.enabled
6329
+ ? {
6330
+ type: "ephemeral",
6331
+ ...(cache.ttl === "1h" ? { ttl: "1h" } : {}),
6332
+ }
6333
+ : undefined);
6169
6334
  // Append instructions to last message if provided
6170
6335
  if (request.instructions && messages.length > 0) {
6171
6336
  const lastMsg = messages[messages.length - 1];
@@ -6529,11 +6694,15 @@ class OpenRouterAdapter extends BaseProviderAdapter {
6529
6694
  }
6530
6695
  // SDK returns camelCase, but support snake_case as fallback
6531
6696
  const usage = openRouterResponse.usage;
6697
+ const cachedTokens = usage.promptTokensDetails?.cachedTokens ??
6698
+ usage.promptTokensDetails?.cached_tokens ??
6699
+ usage.prompt_tokens_details?.cached_tokens;
6532
6700
  return {
6533
6701
  input: usage.promptTokens || usage.prompt_tokens || 0,
6534
6702
  output: usage.completionTokens || usage.completion_tokens || 0,
6535
6703
  reasoning: usage.completionTokensDetails?.reasoningTokens || 0,
6536
6704
  total: usage.totalTokens || usage.total_tokens || 0,
6705
+ ...(cachedTokens !== undefined ? { cacheRead: cachedTokens } : {}),
6537
6706
  provider: this.name,
6538
6707
  model,
6539
6708
  };
@@ -6734,13 +6903,17 @@ class OpenRouterAdapter extends BaseProviderAdapter {
6734
6903
  const choice = response.choices[0];
6735
6904
  return choice?.message?.content ?? undefined;
6736
6905
  }
6737
- convertMessagesToOpenRouter(messages, system) {
6906
+ convertMessagesToOpenRouter(messages, system, cacheControl) {
6738
6907
  const openRouterMessages = [];
6739
- // Add system message if provided
6908
+ // Add system message if provided. A cache_control breakpoint on the system
6909
+ // content is forwarded by OpenRouter to Anthropic/Gemini backends (and
6910
+ // ignored by others), caching the stable system prefix.
6740
6911
  if (system) {
6741
6912
  openRouterMessages.push({
6742
6913
  role: "system",
6743
- content: system,
6914
+ content: cacheControl
6915
+ ? [{ type: "text", text: system, cache_control: cacheControl }]
6916
+ : system,
6744
6917
  });
6745
6918
  }
6746
6919
  for (const msg of messages) {
@@ -8452,6 +8625,7 @@ class OperateLoop {
8452
8625
  }
8453
8626
  buildInitialRequest(state, options) {
8454
8627
  return {
8628
+ cache: options.cache,
8455
8629
  effort: options.effort,
8456
8630
  format: state.formattedFormat,
8457
8631
  instructions: options.instructions,
@@ -9085,6 +9259,7 @@ class StreamLoop {
9085
9259
  }
9086
9260
  buildInitialRequest(state, options) {
9087
9261
  return {
9262
+ cache: options.cache,
9088
9263
  effort: options.effort,
9089
9264
  format: state.formattedFormat,
9090
9265
  instructions: options.instructions,