@jaypie/llm 1.3.14 → 1.3.16

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.
@@ -47,6 +47,61 @@ export declare const MODEL: {
47
47
  SONNET: string;
48
48
  };
49
49
  };
50
+ /** Price of one million tokens, in US dollars. */
51
+ export interface LlmModelCost {
52
+ /**
53
+ * Cache-read (cached input) tokens. Omitted when the provider does not price
54
+ * cache reads separately from uncached input.
55
+ */
56
+ cachedInputRead?: number;
57
+ /**
58
+ * Cache-write tokens. A scalar when the rate does not vary by TTL; keyed by
59
+ * the same `"5m"` / `"1h"` literals as `LlmCache` when it does, so a cost
60
+ * calculation reads the TTL straight off `OperateRequest.cache`. Omitted
61
+ * means cache writes bill at the `input` rate.
62
+ */
63
+ cachedInputWrite?: number | {
64
+ "1h": number;
65
+ "5m": number;
66
+ };
67
+ /** Uncached input tokens. */
68
+ input: number;
69
+ /** Output tokens. */
70
+ output: number;
71
+ /**
72
+ * Reasoning tokens, when billed at a rate other than `output`. Omitted means
73
+ * reasoning bills as output, which is true of every provider listed here.
74
+ */
75
+ reasoning?: number;
76
+ }
77
+ /**
78
+ * Standard list price per million tokens, keyed by literal model id (verified
79
+ * 2026-07-21). Keys are string literals rather than `MODEL.*` references so a
80
+ * model retired from the catalog keeps its price here: historic ids stay
81
+ * priceable after they leave `MODEL.*`, which is what makes replaying old
82
+ * usage records possible.
83
+ *
84
+ * Caveats the numbers cannot carry:
85
+ * - **Standard rate only.** Introductory, batch, flex, priority, fast-mode, and
86
+ * data-residency rates are excluded. Sonnet 5 is listed at its standard
87
+ * $3/$15, not the introductory rate.
88
+ * - **Cache writes are Anthropic-only.** Fireworks writes bill at the input
89
+ * rate. OpenAI and xAI discount reads automatically and publish no write
90
+ * premium. Google charges nothing to write an implicit cache; explicit
91
+ * caching bills storage per hour, a unit this table does not carry (and one
92
+ * `@jaypie/llm` does not wire).
93
+ * - **Short-context tier.** Long-prompt surcharges are not modeled: Gemini 3.1
94
+ * Pro doubles above 200K, Grok doubles at 200K, GPT-5.5 is 2x in / 1.5x out
95
+ * above 272K.
96
+ * - **Text rates.** Gemini prices audio input higher than text/image/video.
97
+ * - **Proxies are deliberately absent.** Bedrock (`PROVIDER.BEDROCK.*`) and
98
+ * OpenRouter (`MODEL.OPENROUTER.*`) resell many vendors and price per backend
99
+ * route, so no single rate is correct for a proxy id. Price those against the
100
+ * backend model, or against the proxy's own published rate.
101
+ *
102
+ * Unlisted ids return `undefined` — callers must handle a miss.
103
+ */
104
+ export declare const COST: Record<string, LlmModelCost>;
50
105
  export declare const PROVIDER: {
51
106
  readonly BEDROCK: {
52
107
  readonly DEFAULT: "amazon.nova-pro-v1:0";
@@ -201,7 +256,7 @@ export declare const DEFAULT: {
201
256
  */
202
257
  export declare const ALL: {
203
258
  readonly BASE: readonly ["claude-sonnet-4-6", "gemini-3.1-pro-preview", "gpt-5.4", "grok-latest"];
204
- readonly COMBINED: readonly ("claude-opus-4-8" | "claude-haiku-4-5" | "gemini-3.5-flash" | "gemini-3.1-flash-lite" | "gemini-3.1-pro-preview" | "gpt-5.5" | "gpt-5.4-mini" | "gpt-5.4-nano" | "grok-latest" | "claude-sonnet-4-6" | "gpt-5.4" | "grok-4-1-fast-reasoning" | "grok-4-1-fast-non-reasoning")[];
259
+ readonly COMBINED: readonly ("claude-opus-4-8" | "claude-haiku-4-5" | "gemini-3.1-pro-preview" | "gpt-5.5" | "gpt-5.4-mini" | "gpt-5.4-nano" | "grok-latest" | "claude-sonnet-4-6" | "gemini-3.1-flash-lite" | "gemini-3.5-flash" | "gpt-5.4" | "grok-4-1-fast-non-reasoning" | "grok-4-1-fast-reasoning")[];
205
260
  readonly LARGE: readonly ["claude-opus-4-8", "gemini-3.1-pro-preview", "gpt-5.5", "grok-latest"];
206
261
  readonly SMALL: readonly ["claude-sonnet-4-6", "gemini-3.5-flash", "gpt-5.4-mini", "grok-4-1-fast-reasoning"];
207
262
  readonly TINY: readonly ["claude-haiku-4-5", "gemini-3.1-flash-lite", "gpt-5.4-nano", "grok-4-1-fast-non-reasoning"];
@@ -46,8 +46,8 @@ const MODEL = {
46
46
  QWEN: "accounts/fireworks/models/qwen3p7-plus",
47
47
  },
48
48
  // Google
49
- GEMINI_FLASH: "gemini-3.5-flash",
50
- GEMINI_FLASH_LITE: "gemini-3.1-flash-lite",
49
+ GEMINI_FLASH: "gemini-3.6-flash",
50
+ GEMINI_FLASH_LITE: "gemini-3.5-flash-lite",
51
51
  GEMINI_PRO: "gemini-3.1-pro-preview",
52
52
  // OpenAI
53
53
  SOL: "gpt-5.6-sol",
@@ -68,6 +68,177 @@ const MODEL = {
68
68
  SONNET: "anthropic/claude-sonnet-5",
69
69
  },
70
70
  };
71
+ /**
72
+ * Standard list price per million tokens, keyed by literal model id (verified
73
+ * 2026-07-21). Keys are string literals rather than `MODEL.*` references so a
74
+ * model retired from the catalog keeps its price here: historic ids stay
75
+ * priceable after they leave `MODEL.*`, which is what makes replaying old
76
+ * usage records possible.
77
+ *
78
+ * Caveats the numbers cannot carry:
79
+ * - **Standard rate only.** Introductory, batch, flex, priority, fast-mode, and
80
+ * data-residency rates are excluded. Sonnet 5 is listed at its standard
81
+ * $3/$15, not the introductory rate.
82
+ * - **Cache writes are Anthropic-only.** Fireworks writes bill at the input
83
+ * rate. OpenAI and xAI discount reads automatically and publish no write
84
+ * premium. Google charges nothing to write an implicit cache; explicit
85
+ * caching bills storage per hour, a unit this table does not carry (and one
86
+ * `@jaypie/llm` does not wire).
87
+ * - **Short-context tier.** Long-prompt surcharges are not modeled: Gemini 3.1
88
+ * Pro doubles above 200K, Grok doubles at 200K, GPT-5.5 is 2x in / 1.5x out
89
+ * above 272K.
90
+ * - **Text rates.** Gemini prices audio input higher than text/image/video.
91
+ * - **Proxies are deliberately absent.** Bedrock (`PROVIDER.BEDROCK.*`) and
92
+ * OpenRouter (`MODEL.OPENROUTER.*`) resell many vendors and price per backend
93
+ * route, so no single rate is correct for a proxy id. Price those against the
94
+ * backend model, or against the proxy's own published rate.
95
+ *
96
+ * Unlisted ids return `undefined` — callers must handle a miss.
97
+ */
98
+ const COST = {
99
+ // Anthropic — https://platform.claude.com/docs/en/about-claude/pricing
100
+ "claude-3-5-haiku-20241022": {
101
+ cachedInputRead: 0.08,
102
+ cachedInputWrite: { "1h": 1.6, "5m": 1.0 },
103
+ input: 0.8,
104
+ output: 4.0,
105
+ },
106
+ "claude-fable-5": {
107
+ cachedInputRead: 1.0,
108
+ cachedInputWrite: { "1h": 20.0, "5m": 12.5 },
109
+ input: 10.0,
110
+ output: 50.0,
111
+ },
112
+ "claude-haiku-4-5": {
113
+ cachedInputRead: 0.1,
114
+ cachedInputWrite: { "1h": 2.0, "5m": 1.25 },
115
+ input: 1.0,
116
+ output: 5.0,
117
+ },
118
+ "claude-mythos-5": {
119
+ cachedInputRead: 1.0,
120
+ cachedInputWrite: { "1h": 20.0, "5m": 12.5 },
121
+ input: 10.0,
122
+ output: 50.0,
123
+ },
124
+ "claude-opus-4-1": {
125
+ cachedInputRead: 1.5,
126
+ cachedInputWrite: { "1h": 30.0, "5m": 18.75 },
127
+ input: 15.0,
128
+ output: 75.0,
129
+ },
130
+ "claude-opus-4-5": {
131
+ cachedInputRead: 0.5,
132
+ cachedInputWrite: { "1h": 10.0, "5m": 6.25 },
133
+ input: 5.0,
134
+ output: 25.0,
135
+ },
136
+ "claude-opus-4-6": {
137
+ cachedInputRead: 0.5,
138
+ cachedInputWrite: { "1h": 10.0, "5m": 6.25 },
139
+ input: 5.0,
140
+ output: 25.0,
141
+ },
142
+ "claude-opus-4-7": {
143
+ cachedInputRead: 0.5,
144
+ cachedInputWrite: { "1h": 10.0, "5m": 6.25 },
145
+ input: 5.0,
146
+ output: 25.0,
147
+ },
148
+ "claude-opus-4-8": {
149
+ cachedInputRead: 0.5,
150
+ cachedInputWrite: { "1h": 10.0, "5m": 6.25 },
151
+ input: 5.0,
152
+ output: 25.0,
153
+ },
154
+ "claude-sonnet-4-20250514": {
155
+ cachedInputRead: 0.3,
156
+ cachedInputWrite: { "1h": 6.0, "5m": 3.75 },
157
+ input: 3.0,
158
+ output: 15.0,
159
+ },
160
+ "claude-sonnet-4-5": {
161
+ cachedInputRead: 0.3,
162
+ cachedInputWrite: { "1h": 6.0, "5m": 3.75 },
163
+ input: 3.0,
164
+ output: 15.0,
165
+ },
166
+ "claude-sonnet-4-6": {
167
+ cachedInputRead: 0.3,
168
+ cachedInputWrite: { "1h": 6.0, "5m": 3.75 },
169
+ input: 3.0,
170
+ output: 15.0,
171
+ },
172
+ "claude-sonnet-5": {
173
+ cachedInputRead: 0.3,
174
+ cachedInputWrite: { "1h": 6.0, "5m": 3.75 },
175
+ input: 3.0,
176
+ output: 15.0,
177
+ },
178
+ // Fireworks — https://docs.fireworks.ai/serverless/pricing
179
+ "accounts/fireworks/models/deepseek-v4-pro": {
180
+ cachedInputRead: 0.145,
181
+ input: 1.74,
182
+ output: 3.48,
183
+ },
184
+ "accounts/fireworks/models/glm-5p2": {
185
+ cachedInputRead: 0.14,
186
+ input: 1.4,
187
+ output: 4.4,
188
+ },
189
+ "accounts/fireworks/models/kimi-k2p7-code": {
190
+ cachedInputRead: 0.19,
191
+ input: 0.95,
192
+ output: 4.0,
193
+ },
194
+ "accounts/fireworks/models/minimax-m2p7": {
195
+ cachedInputRead: 0.06,
196
+ input: 0.3,
197
+ output: 1.2,
198
+ },
199
+ "accounts/fireworks/models/qwen3p7-plus": {
200
+ cachedInputRead: 0.08,
201
+ input: 0.4,
202
+ output: 1.6,
203
+ },
204
+ // Google — https://ai.google.dev/gemini-api/docs/pricing
205
+ "gemini-2.5-flash": { cachedInputRead: 0.03, input: 0.3, output: 2.5 },
206
+ "gemini-3.1-flash-lite": {
207
+ cachedInputRead: 0.025,
208
+ input: 0.25,
209
+ output: 1.5,
210
+ },
211
+ "gemini-3.1-flash-lite-preview": {
212
+ cachedInputRead: 0.025,
213
+ input: 0.25,
214
+ output: 1.5,
215
+ },
216
+ "gemini-3.1-pro-preview": { cachedInputRead: 0.2, input: 2.0, output: 12.0 },
217
+ "gemini-3.5-flash": { cachedInputRead: 0.15, input: 1.5, output: 9.0 },
218
+ // Flash-Lite 3.5 has no separate cache-read rate on the standard tier
219
+ "gemini-3.5-flash-lite": { input: 0.3, output: 2.5 },
220
+ "gemini-3.6-flash": { cachedInputRead: 0.15, input: 1.5, output: 7.5 },
221
+ // OpenAI — https://developers.openai.com/api/docs/pricing
222
+ "gpt-5.4": { cachedInputRead: 0.25, input: 2.5, output: 15.0 },
223
+ "gpt-5.4-mini": { cachedInputRead: 0.075, input: 0.75, output: 4.5 },
224
+ "gpt-5.4-nano": { cachedInputRead: 0.02, input: 0.2, output: 1.25 },
225
+ "gpt-5.5": { cachedInputRead: 0.5, input: 5.0, output: 30.0 },
226
+ "gpt-5.6-luna": { cachedInputRead: 0.1, input: 1.0, output: 6.0 },
227
+ "gpt-5.6-sol": { cachedInputRead: 0.5, input: 5.0, output: 30.0 },
228
+ "gpt-5.6-terra": { cachedInputRead: 0.25, input: 2.5, output: 15.0 },
229
+ // xAI — https://docs.x.ai/docs/models ("grok-latest" aliases grok-4.3-latest)
230
+ "grok-4-1-fast-non-reasoning": {
231
+ cachedInputRead: 0.05,
232
+ input: 0.2,
233
+ output: 0.5,
234
+ },
235
+ "grok-4-1-fast-reasoning": {
236
+ cachedInputRead: 0.05,
237
+ input: 0.2,
238
+ output: 0.5,
239
+ },
240
+ "grok-latest": { cachedInputRead: 0.2, input: 1.25, output: 2.5 },
241
+ };
71
242
  const GOOGLE_PROVIDER = {
72
243
  // https://ai.google.dev/gemini-api/docs/models
73
244
  DEFAULT: MODEL.GEMINI_FLASH,
@@ -273,6 +444,7 @@ const ALL = {
273
444
  var constants = /*#__PURE__*/Object.freeze({
274
445
  __proto__: null,
275
446
  ALL: ALL,
447
+ COST: COST,
276
448
  DEFAULT: DEFAULT,
277
449
  EFFORT: EFFORT,
278
450
  MODEL: MODEL,
@@ -501,6 +673,12 @@ function sumUsageByProviderModel(usage) {
501
673
  totals[key].output += item.output;
502
674
  totals[key].reasoning += item.reasoning;
503
675
  totals[key].total += item.total;
676
+ if (item.cacheRead !== undefined) {
677
+ totals[key].cacheRead = (totals[key].cacheRead ?? 0) + item.cacheRead;
678
+ }
679
+ if (item.cacheWrite !== undefined) {
680
+ totals[key].cacheWrite = (totals[key].cacheWrite ?? 0) + item.cacheWrite;
681
+ }
504
682
  }
505
683
  return totals;
506
684
  }
@@ -526,6 +704,7 @@ function buildExchangeEnvelope({ duration, initialHistoryLength, input, options,
526
704
  return {
527
705
  ids: extractResponseIds(response.responses),
528
706
  request: {
707
+ cache: options.cache,
529
708
  data: options.data,
530
709
  effort: options.effort,
531
710
  explain: options.explain,
@@ -563,6 +742,38 @@ function buildExchangeEnvelope({ duration, initialHistoryLength, input, options,
563
742
  };
564
743
  }
565
744
 
745
+ const CACHE_TTL_DEFAULT = "5m";
746
+ /**
747
+ * Normalize the caller-facing `cache` option into a concrete decision.
748
+ * - `undefined` / `true` → enabled at the default TTL
749
+ * - `false` / `0` → disabled
750
+ * - `"5m"` / `"1h"` → enabled at that TTL
751
+ */
752
+ function resolveCache(cache) {
753
+ if (cache === false || cache === 0) {
754
+ return { enabled: false, ttl: CACHE_TTL_DEFAULT };
755
+ }
756
+ if (cache === "5m" || cache === "1h") {
757
+ return { enabled: true, ttl: cache };
758
+ }
759
+ // undefined or true
760
+ return { enabled: true, ttl: CACHE_TTL_DEFAULT };
761
+ }
762
+ /**
763
+ * Deterministic short key for providers with automatic, prefix-based caching
764
+ * (e.g. OpenAI `prompt_cache_key`). Derived from the stable prefix so the same
765
+ * system prompt + tools + model always routes to the same cache. Dependency-
766
+ * free FNV-1a; not cryptographic.
767
+ */
768
+ function promptCacheKey(seed) {
769
+ let hash = 0x811c9dc5;
770
+ for (let i = 0; i < seed.length; i += 1) {
771
+ hash ^= seed.charCodeAt(i);
772
+ hash = Math.imul(hash, 0x01000193);
773
+ }
774
+ return `jaypie-${(hash >>> 0).toString(16)}`;
775
+ }
776
+
566
777
  /**
567
778
  * Type guard to check if an item is a dedicated reasoning item (OpenAI)
568
779
  */
@@ -1414,6 +1625,14 @@ function tallyOperate({ toolCallNames = [], turns, usage = [], }) {
1414
1625
  usageByModel[key].output += item.output;
1415
1626
  usageByModel[key].reasoning += item.reasoning;
1416
1627
  usageByModel[key].total += item.total;
1628
+ if (item.cacheRead !== undefined) {
1629
+ usageByModel[key].cacheRead =
1630
+ (usageByModel[key].cacheRead ?? 0) + item.cacheRead;
1631
+ }
1632
+ if (item.cacheWrite !== undefined) {
1633
+ usageByModel[key].cacheWrite =
1634
+ (usageByModel[key].cacheWrite ?? 0) + item.cacheWrite;
1635
+ }
1417
1636
  }
1418
1637
  llm.usage = usageByModel;
1419
1638
  }
@@ -1520,27 +1739,26 @@ const MODULE$1 = {
1520
1739
  //
1521
1740
  // Helpers
1522
1741
  //
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)));
1742
+ // Native dynamic import that neither rollup nor tsc rewrites to require(), so
1743
+ // a CJS-bundled build still loads @jaypie/dynamodb's ESM entry and shares the
1744
+ // host's initialized module instance. A require()-based resolution would load
1745
+ // the CJS build, whose module-level client state is separate from the ESM
1746
+ // instance an ESM host initializes (dual-package hazard, issue #429).
1747
+ const dynamicImport = new Function("s", "return import(s)");
1530
1748
  let resolved$1 = false;
1531
1749
  let cachedSdk$2 = null;
1532
1750
  /**
1533
1751
  * Lazily resolve @jaypie/dynamodb's storeExchange. Returns null (and never
1534
1752
  * throws) when the peer is absent. Cached after the first attempt.
1535
1753
  */
1536
- function resolveExchangeStore() {
1754
+ async function resolveExchangeStore() {
1537
1755
  if (resolved$1) {
1538
1756
  return cachedSdk$2;
1539
1757
  }
1540
1758
  resolved$1 = true;
1541
1759
  try {
1542
- const dynamodb = requireModule$1(MODULE$1.JAYPIE_DYNAMODB);
1543
- const sdk = dynamodb?.default ?? dynamodb;
1760
+ const dynamodb = await dynamicImport(MODULE$1.JAYPIE_DYNAMODB);
1761
+ const sdk = (dynamodb?.default ?? dynamodb);
1544
1762
  if (sdk && typeof sdk.storeExchange === "function") {
1545
1763
  cachedSdk$2 = sdk;
1546
1764
  }
@@ -1563,7 +1781,7 @@ async function persistExchange(envelope) {
1563
1781
  if (!isExchangeStoreEnabled()) {
1564
1782
  return;
1565
1783
  }
1566
- const sdk = resolveExchangeStore();
1784
+ const sdk = (await resolveExchangeStore());
1567
1785
  if (!sdk) {
1568
1786
  return;
1569
1787
  }
@@ -2351,8 +2569,19 @@ class AnthropicAdapter extends BaseProviderAdapter {
2351
2569
  PROVIDER.ANTHROPIC.MAX_TOKENS.DEFAULT,
2352
2570
  stream: false,
2353
2571
  };
2572
+ const cache = resolveCache(request.cache);
2573
+ const cacheControl = cache.enabled
2574
+ ? {
2575
+ type: "ephemeral",
2576
+ ...(cache.ttl === "1h" ? { ttl: "1h" } : {}),
2577
+ }
2578
+ : undefined;
2354
2579
  if (request.system) {
2355
- anthropicRequest.system = request.system;
2580
+ // A cache breakpoint on the system block caches tools+system together
2581
+ // (render order is tools -> system -> messages).
2582
+ anthropicRequest.system = cacheControl
2583
+ ? [{ type: "text", text: request.system, cache_control: cacheControl }]
2584
+ : request.system;
2356
2585
  }
2357
2586
  const useFallbackStructuredOutput = Boolean(request.format) &&
2358
2587
  !this.supportsStructuredOutput(anthropicRequest.model);
@@ -2369,7 +2598,7 @@ class AnthropicAdapter extends BaseProviderAdapter {
2369
2598
  });
2370
2599
  }
2371
2600
  if (allTools.length > 0) {
2372
- anthropicRequest.tools = allTools.map((tool) => ({
2601
+ anthropicRequest.tools = allTools.map((tool, index) => ({
2373
2602
  name: tool.name,
2374
2603
  description: tool.description,
2375
2604
  input_schema: {
@@ -2377,6 +2606,11 @@ class AnthropicAdapter extends BaseProviderAdapter {
2377
2606
  type: "object",
2378
2607
  },
2379
2608
  type: "custom",
2609
+ // Breakpoint on the last tool caches the tool list (which renders
2610
+ // before system) when there is no system prompt to anchor it.
2611
+ ...(cacheControl && index === allTools.length - 1
2612
+ ? { cache_control: cacheControl }
2613
+ : {}),
2380
2614
  }));
2381
2615
  anthropicRequest.tool_choice = useFallbackStructuredOutput
2382
2616
  ? { type: "any" }
@@ -2679,6 +2913,12 @@ class AnthropicAdapter extends BaseProviderAdapter {
2679
2913
  output: usage.output_tokens,
2680
2914
  reasoning: usage.thinking_tokens || 0,
2681
2915
  total: usage.input_tokens + usage.output_tokens,
2916
+ ...(usage.cache_read_input_tokens !== undefined
2917
+ ? { cacheRead: usage.cache_read_input_tokens }
2918
+ : {}),
2919
+ ...(usage.cache_creation_input_tokens !== undefined
2920
+ ? { cacheWrite: usage.cache_creation_input_tokens }
2921
+ : {}),
2682
2922
  provider: this.name,
2683
2923
  model,
2684
2924
  };
@@ -2931,6 +3171,17 @@ function isTemperatureDeprecationError$3(error) {
2931
3171
  const msg = error?.message ?? "";
2932
3172
  return /temperature.*deprecated|deprecated.*temperature/i.test(msg);
2933
3173
  }
3174
+ /** Exported for tests; not part of the package's public surface. */
3175
+ function isCachePointUnsupportedError(error) {
3176
+ const name = error?.constructor?.name ?? "";
3177
+ const msg = error?.message ?? "";
3178
+ // A model that cannot cache rejects the cachePoint block with a
3179
+ // ValidationException naming caching / cachePoint. Bedrock words this as
3180
+ // "You invoked an unsupported model or your request did not allow prompt
3181
+ // caching" — "unsupported" is one word, so it must be matched separately
3182
+ // from "not support".
3183
+ return (/ValidationException|invalid|not support|unsupported/i.test(name + " " + msg) && /cachePoint|cache_point|prompt caching|caching/i.test(msg));
3184
+ }
2934
3185
  function extractJson(text) {
2935
3186
  // Try direct parse first
2936
3187
  try {
@@ -2966,6 +3217,13 @@ class BedrockAdapter extends BaseProviderAdapter {
2966
3217
  this.defaultModel = PROVIDER.BEDROCK.DEFAULT;
2967
3218
  this._modelsFallbackToStructuredOutputTool = new Set();
2968
3219
  this._modelsWithoutTemperature = new Set();
3220
+ this._modelsWithoutCachePoint = new Set();
3221
+ }
3222
+ rememberModelRejectsCachePoint(model) {
3223
+ this._modelsWithoutCachePoint.add(model);
3224
+ }
3225
+ supportsCachePoint(model) {
3226
+ return !this._modelsWithoutCachePoint.has(model);
2969
3227
  }
2970
3228
  rememberModelRejectsOutputConfig(model) {
2971
3229
  this._modelsFallbackToStructuredOutputTool.add(model);
@@ -3102,11 +3360,51 @@ class BedrockAdapter extends BaseProviderAdapter {
3102
3360
  };
3103
3361
  }
3104
3362
  }
3363
+ // Prompt caching via Converse cachePoint blocks (5-min default TTL; the
3364
+ // `ttl` option does not apply). Gated per model — cachePoint 400s on
3365
+ // unsupported models, which executeRequest catches and denylists.
3366
+ if (resolveCache(request.cache).enabled && this.supportsCachePoint(model)) {
3367
+ const cachePoint = { cachePoint: { type: "default" } };
3368
+ if (bedrockRequest.system && bedrockRequest.system.length > 0) {
3369
+ bedrockRequest.system = [
3370
+ ...bedrockRequest.system,
3371
+ cachePoint,
3372
+ ];
3373
+ }
3374
+ if (bedrockRequest.toolConfig?.tools?.length) {
3375
+ bedrockRequest.toolConfig = {
3376
+ ...bedrockRequest.toolConfig,
3377
+ tools: [
3378
+ ...bedrockRequest.toolConfig.tools,
3379
+ cachePoint,
3380
+ ],
3381
+ };
3382
+ }
3383
+ }
3105
3384
  if (request.providerOptions) {
3106
3385
  Object.assign(bedrockRequest, request.providerOptions);
3107
3386
  }
3108
3387
  return bedrockRequest;
3109
3388
  }
3389
+ /** Remove any cachePoint blocks so the request can be retried unsupported. */
3390
+ stripCachePoints(request) {
3391
+ const stripped = { ...request };
3392
+ if (stripped.system) {
3393
+ stripped.system = stripped.system.filter((block) => !("cachePoint" in block));
3394
+ }
3395
+ if (stripped.toolConfig?.tools) {
3396
+ stripped.toolConfig = {
3397
+ ...stripped.toolConfig,
3398
+ tools: stripped.toolConfig.tools.filter((tool) => !("cachePoint" in tool)),
3399
+ };
3400
+ }
3401
+ return stripped;
3402
+ }
3403
+ requestHasCachePoints(request) {
3404
+ const inSystem = (request.system ?? []).some((block) => "cachePoint" in block);
3405
+ const inTools = (request.toolConfig?.tools ?? []).some((tool) => "cachePoint" in tool);
3406
+ return inSystem || inTools;
3407
+ }
3110
3408
  formatTools(toolkit) {
3111
3409
  return toolkit.tools.map((tool) => ({
3112
3410
  name: tool.name,
@@ -3161,6 +3459,15 @@ class BedrockAdapter extends BaseProviderAdapter {
3161
3459
  const fallbackRequest = this.toFallbackStructuredOutputRequest(bedrockRequest);
3162
3460
  return (await bedrockClient.send(new ConverseCommand(fallbackRequest), signal ? { abortSignal: signal } : undefined));
3163
3461
  }
3462
+ if (this.requestHasCachePoints(bedrockRequest) &&
3463
+ isCachePointUnsupportedError(error)) {
3464
+ this.rememberModelRejectsCachePoint(bedrockRequest.modelId || this.defaultModel);
3465
+ const retryRequest = this.stripCachePoints(bedrockRequest);
3466
+ const response = (await bedrockClient.send(new ConverseCommand(retryRequest), signal ? { abortSignal: signal } : undefined));
3467
+ if (wantsStructuredOutput)
3468
+ response.__jaypieStructuredOutput = true;
3469
+ return response;
3470
+ }
3164
3471
  throw error;
3165
3472
  }
3166
3473
  }
@@ -3304,6 +3611,12 @@ class BedrockAdapter extends BaseProviderAdapter {
3304
3611
  reasoning: 0,
3305
3612
  total: usage?.totalTokens ??
3306
3613
  (usage?.inputTokens ?? 0) + (usage?.outputTokens ?? 0),
3614
+ ...(usage?.cacheReadInputTokens !== undefined
3615
+ ? { cacheRead: usage.cacheReadInputTokens }
3616
+ : {}),
3617
+ ...(usage?.cacheWriteInputTokens !== undefined
3618
+ ? { cacheWrite: usage.cacheWriteInputTokens }
3619
+ : {}),
3307
3620
  provider: this.name,
3308
3621
  model,
3309
3622
  };
@@ -5475,6 +5788,13 @@ class OpenAiAdapter extends BaseProviderAdapter {
5475
5788
  supportsReasoningEffort(model) {
5476
5789
  return isReasoningModel(model);
5477
5790
  }
5791
+ /**
5792
+ * Whether to emit `prompt_cache_key` (OpenAI Responses API). Overridable by
5793
+ * OpenAI-compatible subclasses whose backend rejects the field.
5794
+ */
5795
+ supportsPromptCacheKey() {
5796
+ return true;
5797
+ }
5478
5798
  /** Translate a normalized effort to this provider's `reasoning.effort` value. */
5479
5799
  mapReasoningEffort(effort, model) {
5480
5800
  return toOpenAiEffort(effort, { model });
@@ -5514,6 +5834,18 @@ class OpenAiAdapter extends BaseProviderAdapter {
5514
5834
  summary: "auto",
5515
5835
  };
5516
5836
  }
5837
+ // OpenAI prompt caching is automatic (prefix-based). Setting a stable
5838
+ // prompt_cache_key routes repeat traffic to the same cache across
5839
+ // instances. Keyed on the stable prefix only (system + instructions +
5840
+ // tools + model), never the volatile user turn.
5841
+ if (this.supportsPromptCacheKey() && resolveCache(request.cache).enabled) {
5842
+ openaiRequest.prompt_cache_key = promptCacheKey(JSON.stringify([
5843
+ model,
5844
+ request.system ?? "",
5845
+ request.instructions ?? "",
5846
+ request.tools ?? [],
5847
+ ]));
5848
+ }
5517
5849
  if (request.providerOptions) {
5518
5850
  Object.assign(openaiRequest, request.providerOptions);
5519
5851
  }
@@ -5772,11 +6104,13 @@ class OpenAiAdapter extends BaseProviderAdapter {
5772
6104
  model,
5773
6105
  };
5774
6106
  }
6107
+ const cachedTokens = openaiResponse.usage.input_tokens_details?.cached_tokens;
5775
6108
  return {
5776
6109
  input: openaiResponse.usage.input_tokens || 0,
5777
6110
  output: openaiResponse.usage.output_tokens || 0,
5778
6111
  reasoning: openaiResponse.usage.output_tokens_details?.reasoning_tokens || 0,
5779
6112
  total: openaiResponse.usage.total_tokens || 0,
6113
+ ...(cachedTokens !== undefined ? { cacheRead: cachedTokens } : {}),
5780
6114
  provider: this.name,
5781
6115
  model,
5782
6116
  };
@@ -6168,7 +6502,13 @@ class OpenRouterAdapter extends BaseProviderAdapter {
6168
6502
  //
6169
6503
  buildRequest(request) {
6170
6504
  // Convert messages to OpenRouter format (OpenAI-compatible)
6171
- const messages = this.convertMessagesToOpenRouter(request.messages, request.system);
6505
+ const cache = resolveCache(request.cache);
6506
+ const messages = this.convertMessagesToOpenRouter(request.messages, request.system, cache.enabled
6507
+ ? {
6508
+ type: "ephemeral",
6509
+ ...(cache.ttl === "1h" ? { ttl: "1h" } : {}),
6510
+ }
6511
+ : undefined);
6172
6512
  // Append instructions to last message if provided
6173
6513
  if (request.instructions && messages.length > 0) {
6174
6514
  const lastMsg = messages[messages.length - 1];
@@ -6532,11 +6872,15 @@ class OpenRouterAdapter extends BaseProviderAdapter {
6532
6872
  }
6533
6873
  // SDK returns camelCase, but support snake_case as fallback
6534
6874
  const usage = openRouterResponse.usage;
6875
+ const cachedTokens = usage.promptTokensDetails?.cachedTokens ??
6876
+ usage.promptTokensDetails?.cached_tokens ??
6877
+ usage.prompt_tokens_details?.cached_tokens;
6535
6878
  return {
6536
6879
  input: usage.promptTokens || usage.prompt_tokens || 0,
6537
6880
  output: usage.completionTokens || usage.completion_tokens || 0,
6538
6881
  reasoning: usage.completionTokensDetails?.reasoningTokens || 0,
6539
6882
  total: usage.totalTokens || usage.total_tokens || 0,
6883
+ ...(cachedTokens !== undefined ? { cacheRead: cachedTokens } : {}),
6540
6884
  provider: this.name,
6541
6885
  model,
6542
6886
  };
@@ -6737,13 +7081,17 @@ class OpenRouterAdapter extends BaseProviderAdapter {
6737
7081
  const choice = response.choices[0];
6738
7082
  return choice?.message?.content ?? undefined;
6739
7083
  }
6740
- convertMessagesToOpenRouter(messages, system) {
7084
+ convertMessagesToOpenRouter(messages, system, cacheControl) {
6741
7085
  const openRouterMessages = [];
6742
- // Add system message if provided
7086
+ // Add system message if provided. A cache_control breakpoint on the system
7087
+ // content is forwarded by OpenRouter to Anthropic/Gemini backends (and
7088
+ // ignored by others), caching the stable system prefix.
6743
7089
  if (system) {
6744
7090
  openRouterMessages.push({
6745
7091
  role: "system",
6746
- content: system,
7092
+ content: cacheControl
7093
+ ? [{ type: "text", text: system, cache_control: cacheControl }]
7094
+ : system,
6747
7095
  });
6748
7096
  }
6749
7097
  for (const msg of messages) {
@@ -8455,6 +8803,7 @@ class OperateLoop {
8455
8803
  }
8456
8804
  buildInitialRequest(state, options) {
8457
8805
  return {
8806
+ cache: options.cache,
8458
8807
  effort: options.effort,
8459
8808
  format: state.formattedFormat,
8460
8809
  instructions: options.instructions,
@@ -9088,6 +9437,7 @@ class StreamLoop {
9088
9437
  }
9089
9438
  buildInitialRequest(state, options) {
9090
9439
  return {
9440
+ cache: options.cache,
9091
9441
  effort: options.effort,
9092
9442
  format: state.formattedFormat,
9093
9443
  instructions: options.instructions,