@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.
package/dist/esm/index.js CHANGED
@@ -43,8 +43,8 @@ const MODEL = {
43
43
  QWEN: "accounts/fireworks/models/qwen3p7-plus",
44
44
  },
45
45
  // Google
46
- GEMINI_FLASH: "gemini-3.5-flash",
47
- GEMINI_FLASH_LITE: "gemini-3.1-flash-lite",
46
+ GEMINI_FLASH: "gemini-3.6-flash",
47
+ GEMINI_FLASH_LITE: "gemini-3.5-flash-lite",
48
48
  GEMINI_PRO: "gemini-3.1-pro-preview",
49
49
  // OpenAI
50
50
  SOL: "gpt-5.6-sol",
@@ -65,6 +65,177 @@ const MODEL = {
65
65
  SONNET: "anthropic/claude-sonnet-5",
66
66
  },
67
67
  };
68
+ /**
69
+ * Standard list price per million tokens, keyed by literal model id (verified
70
+ * 2026-07-21). Keys are string literals rather than `MODEL.*` references so a
71
+ * model retired from the catalog keeps its price here: historic ids stay
72
+ * priceable after they leave `MODEL.*`, which is what makes replaying old
73
+ * usage records possible.
74
+ *
75
+ * Caveats the numbers cannot carry:
76
+ * - **Standard rate only.** Introductory, batch, flex, priority, fast-mode, and
77
+ * data-residency rates are excluded. Sonnet 5 is listed at its standard
78
+ * $3/$15, not the introductory rate.
79
+ * - **Cache writes are Anthropic-only.** Fireworks writes bill at the input
80
+ * rate. OpenAI and xAI discount reads automatically and publish no write
81
+ * premium. Google charges nothing to write an implicit cache; explicit
82
+ * caching bills storage per hour, a unit this table does not carry (and one
83
+ * `@jaypie/llm` does not wire).
84
+ * - **Short-context tier.** Long-prompt surcharges are not modeled: Gemini 3.1
85
+ * Pro doubles above 200K, Grok doubles at 200K, GPT-5.5 is 2x in / 1.5x out
86
+ * above 272K.
87
+ * - **Text rates.** Gemini prices audio input higher than text/image/video.
88
+ * - **Proxies are deliberately absent.** Bedrock (`PROVIDER.BEDROCK.*`) and
89
+ * OpenRouter (`MODEL.OPENROUTER.*`) resell many vendors and price per backend
90
+ * route, so no single rate is correct for a proxy id. Price those against the
91
+ * backend model, or against the proxy's own published rate.
92
+ *
93
+ * Unlisted ids return `undefined` — callers must handle a miss.
94
+ */
95
+ const COST = {
96
+ // Anthropic — https://platform.claude.com/docs/en/about-claude/pricing
97
+ "claude-3-5-haiku-20241022": {
98
+ cachedInputRead: 0.08,
99
+ cachedInputWrite: { "1h": 1.6, "5m": 1.0 },
100
+ input: 0.8,
101
+ output: 4.0,
102
+ },
103
+ "claude-fable-5": {
104
+ cachedInputRead: 1.0,
105
+ cachedInputWrite: { "1h": 20.0, "5m": 12.5 },
106
+ input: 10.0,
107
+ output: 50.0,
108
+ },
109
+ "claude-haiku-4-5": {
110
+ cachedInputRead: 0.1,
111
+ cachedInputWrite: { "1h": 2.0, "5m": 1.25 },
112
+ input: 1.0,
113
+ output: 5.0,
114
+ },
115
+ "claude-mythos-5": {
116
+ cachedInputRead: 1.0,
117
+ cachedInputWrite: { "1h": 20.0, "5m": 12.5 },
118
+ input: 10.0,
119
+ output: 50.0,
120
+ },
121
+ "claude-opus-4-1": {
122
+ cachedInputRead: 1.5,
123
+ cachedInputWrite: { "1h": 30.0, "5m": 18.75 },
124
+ input: 15.0,
125
+ output: 75.0,
126
+ },
127
+ "claude-opus-4-5": {
128
+ cachedInputRead: 0.5,
129
+ cachedInputWrite: { "1h": 10.0, "5m": 6.25 },
130
+ input: 5.0,
131
+ output: 25.0,
132
+ },
133
+ "claude-opus-4-6": {
134
+ cachedInputRead: 0.5,
135
+ cachedInputWrite: { "1h": 10.0, "5m": 6.25 },
136
+ input: 5.0,
137
+ output: 25.0,
138
+ },
139
+ "claude-opus-4-7": {
140
+ cachedInputRead: 0.5,
141
+ cachedInputWrite: { "1h": 10.0, "5m": 6.25 },
142
+ input: 5.0,
143
+ output: 25.0,
144
+ },
145
+ "claude-opus-4-8": {
146
+ cachedInputRead: 0.5,
147
+ cachedInputWrite: { "1h": 10.0, "5m": 6.25 },
148
+ input: 5.0,
149
+ output: 25.0,
150
+ },
151
+ "claude-sonnet-4-20250514": {
152
+ cachedInputRead: 0.3,
153
+ cachedInputWrite: { "1h": 6.0, "5m": 3.75 },
154
+ input: 3.0,
155
+ output: 15.0,
156
+ },
157
+ "claude-sonnet-4-5": {
158
+ cachedInputRead: 0.3,
159
+ cachedInputWrite: { "1h": 6.0, "5m": 3.75 },
160
+ input: 3.0,
161
+ output: 15.0,
162
+ },
163
+ "claude-sonnet-4-6": {
164
+ cachedInputRead: 0.3,
165
+ cachedInputWrite: { "1h": 6.0, "5m": 3.75 },
166
+ input: 3.0,
167
+ output: 15.0,
168
+ },
169
+ "claude-sonnet-5": {
170
+ cachedInputRead: 0.3,
171
+ cachedInputWrite: { "1h": 6.0, "5m": 3.75 },
172
+ input: 3.0,
173
+ output: 15.0,
174
+ },
175
+ // Fireworks — https://docs.fireworks.ai/serverless/pricing
176
+ "accounts/fireworks/models/deepseek-v4-pro": {
177
+ cachedInputRead: 0.145,
178
+ input: 1.74,
179
+ output: 3.48,
180
+ },
181
+ "accounts/fireworks/models/glm-5p2": {
182
+ cachedInputRead: 0.14,
183
+ input: 1.4,
184
+ output: 4.4,
185
+ },
186
+ "accounts/fireworks/models/kimi-k2p7-code": {
187
+ cachedInputRead: 0.19,
188
+ input: 0.95,
189
+ output: 4.0,
190
+ },
191
+ "accounts/fireworks/models/minimax-m2p7": {
192
+ cachedInputRead: 0.06,
193
+ input: 0.3,
194
+ output: 1.2,
195
+ },
196
+ "accounts/fireworks/models/qwen3p7-plus": {
197
+ cachedInputRead: 0.08,
198
+ input: 0.4,
199
+ output: 1.6,
200
+ },
201
+ // Google — https://ai.google.dev/gemini-api/docs/pricing
202
+ "gemini-2.5-flash": { cachedInputRead: 0.03, input: 0.3, output: 2.5 },
203
+ "gemini-3.1-flash-lite": {
204
+ cachedInputRead: 0.025,
205
+ input: 0.25,
206
+ output: 1.5,
207
+ },
208
+ "gemini-3.1-flash-lite-preview": {
209
+ cachedInputRead: 0.025,
210
+ input: 0.25,
211
+ output: 1.5,
212
+ },
213
+ "gemini-3.1-pro-preview": { cachedInputRead: 0.2, input: 2.0, output: 12.0 },
214
+ "gemini-3.5-flash": { cachedInputRead: 0.15, input: 1.5, output: 9.0 },
215
+ // Flash-Lite 3.5 has no separate cache-read rate on the standard tier
216
+ "gemini-3.5-flash-lite": { input: 0.3, output: 2.5 },
217
+ "gemini-3.6-flash": { cachedInputRead: 0.15, input: 1.5, output: 7.5 },
218
+ // OpenAI — https://developers.openai.com/api/docs/pricing
219
+ "gpt-5.4": { cachedInputRead: 0.25, input: 2.5, output: 15.0 },
220
+ "gpt-5.4-mini": { cachedInputRead: 0.075, input: 0.75, output: 4.5 },
221
+ "gpt-5.4-nano": { cachedInputRead: 0.02, input: 0.2, output: 1.25 },
222
+ "gpt-5.5": { cachedInputRead: 0.5, input: 5.0, output: 30.0 },
223
+ "gpt-5.6-luna": { cachedInputRead: 0.1, input: 1.0, output: 6.0 },
224
+ "gpt-5.6-sol": { cachedInputRead: 0.5, input: 5.0, output: 30.0 },
225
+ "gpt-5.6-terra": { cachedInputRead: 0.25, input: 2.5, output: 15.0 },
226
+ // xAI — https://docs.x.ai/docs/models ("grok-latest" aliases grok-4.3-latest)
227
+ "grok-4-1-fast-non-reasoning": {
228
+ cachedInputRead: 0.05,
229
+ input: 0.2,
230
+ output: 0.5,
231
+ },
232
+ "grok-4-1-fast-reasoning": {
233
+ cachedInputRead: 0.05,
234
+ input: 0.2,
235
+ output: 0.5,
236
+ },
237
+ "grok-latest": { cachedInputRead: 0.2, input: 1.25, output: 2.5 },
238
+ };
68
239
  const GOOGLE_PROVIDER = {
69
240
  // https://ai.google.dev/gemini-api/docs/models
70
241
  DEFAULT: MODEL.GEMINI_FLASH,
@@ -270,6 +441,7 @@ const ALL = {
270
441
  var constants = /*#__PURE__*/Object.freeze({
271
442
  __proto__: null,
272
443
  ALL: ALL,
444
+ COST: COST,
273
445
  DEFAULT: DEFAULT,
274
446
  EFFORT: EFFORT,
275
447
  MODEL: MODEL,
@@ -498,6 +670,12 @@ function sumUsageByProviderModel(usage) {
498
670
  totals[key].output += item.output;
499
671
  totals[key].reasoning += item.reasoning;
500
672
  totals[key].total += item.total;
673
+ if (item.cacheRead !== undefined) {
674
+ totals[key].cacheRead = (totals[key].cacheRead ?? 0) + item.cacheRead;
675
+ }
676
+ if (item.cacheWrite !== undefined) {
677
+ totals[key].cacheWrite = (totals[key].cacheWrite ?? 0) + item.cacheWrite;
678
+ }
501
679
  }
502
680
  return totals;
503
681
  }
@@ -523,6 +701,7 @@ function buildExchangeEnvelope({ duration, initialHistoryLength, input, options,
523
701
  return {
524
702
  ids: extractResponseIds(response.responses),
525
703
  request: {
704
+ cache: options.cache,
526
705
  data: options.data,
527
706
  effort: options.effort,
528
707
  explain: options.explain,
@@ -560,6 +739,38 @@ function buildExchangeEnvelope({ duration, initialHistoryLength, input, options,
560
739
  };
561
740
  }
562
741
 
742
+ const CACHE_TTL_DEFAULT = "5m";
743
+ /**
744
+ * Normalize the caller-facing `cache` option into a concrete decision.
745
+ * - `undefined` / `true` → enabled at the default TTL
746
+ * - `false` / `0` → disabled
747
+ * - `"5m"` / `"1h"` → enabled at that TTL
748
+ */
749
+ function resolveCache(cache) {
750
+ if (cache === false || cache === 0) {
751
+ return { enabled: false, ttl: CACHE_TTL_DEFAULT };
752
+ }
753
+ if (cache === "5m" || cache === "1h") {
754
+ return { enabled: true, ttl: cache };
755
+ }
756
+ // undefined or true
757
+ return { enabled: true, ttl: CACHE_TTL_DEFAULT };
758
+ }
759
+ /**
760
+ * Deterministic short key for providers with automatic, prefix-based caching
761
+ * (e.g. OpenAI `prompt_cache_key`). Derived from the stable prefix so the same
762
+ * system prompt + tools + model always routes to the same cache. Dependency-
763
+ * free FNV-1a; not cryptographic.
764
+ */
765
+ function promptCacheKey(seed) {
766
+ let hash = 0x811c9dc5;
767
+ for (let i = 0; i < seed.length; i += 1) {
768
+ hash ^= seed.charCodeAt(i);
769
+ hash = Math.imul(hash, 0x01000193);
770
+ }
771
+ return `jaypie-${(hash >>> 0).toString(16)}`;
772
+ }
773
+
563
774
  /**
564
775
  * Type guard to check if an item is a dedicated reasoning item (OpenAI)
565
776
  */
@@ -1411,6 +1622,14 @@ function tallyOperate({ toolCallNames = [], turns, usage = [], }) {
1411
1622
  usageByModel[key].output += item.output;
1412
1623
  usageByModel[key].reasoning += item.reasoning;
1413
1624
  usageByModel[key].total += item.total;
1625
+ if (item.cacheRead !== undefined) {
1626
+ usageByModel[key].cacheRead =
1627
+ (usageByModel[key].cacheRead ?? 0) + item.cacheRead;
1628
+ }
1629
+ if (item.cacheWrite !== undefined) {
1630
+ usageByModel[key].cacheWrite =
1631
+ (usageByModel[key].cacheWrite ?? 0) + item.cacheWrite;
1632
+ }
1414
1633
  }
1415
1634
  llm.usage = usageByModel;
1416
1635
  }
@@ -1517,27 +1736,26 @@ const MODULE$1 = {
1517
1736
  //
1518
1737
  // Helpers
1519
1738
  //
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);
1739
+ // Native dynamic import that neither rollup nor tsc rewrites to require(), so
1740
+ // a CJS-bundled build still loads @jaypie/dynamodb's ESM entry and shares the
1741
+ // host's initialized module instance. A require()-based resolution would load
1742
+ // the CJS build, whose module-level client state is separate from the ESM
1743
+ // instance an ESM host initializes (dual-package hazard, issue #429).
1744
+ const dynamicImport = new Function("s", "return import(s)");
1527
1745
  let resolved$1 = false;
1528
1746
  let cachedSdk$2 = null;
1529
1747
  /**
1530
1748
  * Lazily resolve @jaypie/dynamodb's storeExchange. Returns null (and never
1531
1749
  * throws) when the peer is absent. Cached after the first attempt.
1532
1750
  */
1533
- function resolveExchangeStore() {
1751
+ async function resolveExchangeStore() {
1534
1752
  if (resolved$1) {
1535
1753
  return cachedSdk$2;
1536
1754
  }
1537
1755
  resolved$1 = true;
1538
1756
  try {
1539
- const dynamodb = requireModule$1(MODULE$1.JAYPIE_DYNAMODB);
1540
- const sdk = dynamodb?.default ?? dynamodb;
1757
+ const dynamodb = await dynamicImport(MODULE$1.JAYPIE_DYNAMODB);
1758
+ const sdk = (dynamodb?.default ?? dynamodb);
1541
1759
  if (sdk && typeof sdk.storeExchange === "function") {
1542
1760
  cachedSdk$2 = sdk;
1543
1761
  }
@@ -1560,7 +1778,7 @@ async function persistExchange(envelope) {
1560
1778
  if (!isExchangeStoreEnabled()) {
1561
1779
  return;
1562
1780
  }
1563
- const sdk = resolveExchangeStore();
1781
+ const sdk = (await resolveExchangeStore());
1564
1782
  if (!sdk) {
1565
1783
  return;
1566
1784
  }
@@ -2348,8 +2566,19 @@ class AnthropicAdapter extends BaseProviderAdapter {
2348
2566
  PROVIDER.ANTHROPIC.MAX_TOKENS.DEFAULT,
2349
2567
  stream: false,
2350
2568
  };
2569
+ const cache = resolveCache(request.cache);
2570
+ const cacheControl = cache.enabled
2571
+ ? {
2572
+ type: "ephemeral",
2573
+ ...(cache.ttl === "1h" ? { ttl: "1h" } : {}),
2574
+ }
2575
+ : undefined;
2351
2576
  if (request.system) {
2352
- anthropicRequest.system = request.system;
2577
+ // A cache breakpoint on the system block caches tools+system together
2578
+ // (render order is tools -> system -> messages).
2579
+ anthropicRequest.system = cacheControl
2580
+ ? [{ type: "text", text: request.system, cache_control: cacheControl }]
2581
+ : request.system;
2353
2582
  }
2354
2583
  const useFallbackStructuredOutput = Boolean(request.format) &&
2355
2584
  !this.supportsStructuredOutput(anthropicRequest.model);
@@ -2366,7 +2595,7 @@ class AnthropicAdapter extends BaseProviderAdapter {
2366
2595
  });
2367
2596
  }
2368
2597
  if (allTools.length > 0) {
2369
- anthropicRequest.tools = allTools.map((tool) => ({
2598
+ anthropicRequest.tools = allTools.map((tool, index) => ({
2370
2599
  name: tool.name,
2371
2600
  description: tool.description,
2372
2601
  input_schema: {
@@ -2374,6 +2603,11 @@ class AnthropicAdapter extends BaseProviderAdapter {
2374
2603
  type: "object",
2375
2604
  },
2376
2605
  type: "custom",
2606
+ // Breakpoint on the last tool caches the tool list (which renders
2607
+ // before system) when there is no system prompt to anchor it.
2608
+ ...(cacheControl && index === allTools.length - 1
2609
+ ? { cache_control: cacheControl }
2610
+ : {}),
2377
2611
  }));
2378
2612
  anthropicRequest.tool_choice = useFallbackStructuredOutput
2379
2613
  ? { type: "any" }
@@ -2676,6 +2910,12 @@ class AnthropicAdapter extends BaseProviderAdapter {
2676
2910
  output: usage.output_tokens,
2677
2911
  reasoning: usage.thinking_tokens || 0,
2678
2912
  total: usage.input_tokens + usage.output_tokens,
2913
+ ...(usage.cache_read_input_tokens !== undefined
2914
+ ? { cacheRead: usage.cache_read_input_tokens }
2915
+ : {}),
2916
+ ...(usage.cache_creation_input_tokens !== undefined
2917
+ ? { cacheWrite: usage.cache_creation_input_tokens }
2918
+ : {}),
2679
2919
  provider: this.name,
2680
2920
  model,
2681
2921
  };
@@ -2928,6 +3168,17 @@ function isTemperatureDeprecationError$3(error) {
2928
3168
  const msg = error?.message ?? "";
2929
3169
  return /temperature.*deprecated|deprecated.*temperature/i.test(msg);
2930
3170
  }
3171
+ /** Exported for tests; not part of the package's public surface. */
3172
+ function isCachePointUnsupportedError(error) {
3173
+ const name = error?.constructor?.name ?? "";
3174
+ const msg = error?.message ?? "";
3175
+ // A model that cannot cache rejects the cachePoint block with a
3176
+ // ValidationException naming caching / cachePoint. Bedrock words this as
3177
+ // "You invoked an unsupported model or your request did not allow prompt
3178
+ // caching" — "unsupported" is one word, so it must be matched separately
3179
+ // from "not support".
3180
+ return (/ValidationException|invalid|not support|unsupported/i.test(name + " " + msg) && /cachePoint|cache_point|prompt caching|caching/i.test(msg));
3181
+ }
2931
3182
  function extractJson(text) {
2932
3183
  // Try direct parse first
2933
3184
  try {
@@ -2963,6 +3214,13 @@ class BedrockAdapter extends BaseProviderAdapter {
2963
3214
  this.defaultModel = PROVIDER.BEDROCK.DEFAULT;
2964
3215
  this._modelsFallbackToStructuredOutputTool = new Set();
2965
3216
  this._modelsWithoutTemperature = new Set();
3217
+ this._modelsWithoutCachePoint = new Set();
3218
+ }
3219
+ rememberModelRejectsCachePoint(model) {
3220
+ this._modelsWithoutCachePoint.add(model);
3221
+ }
3222
+ supportsCachePoint(model) {
3223
+ return !this._modelsWithoutCachePoint.has(model);
2966
3224
  }
2967
3225
  rememberModelRejectsOutputConfig(model) {
2968
3226
  this._modelsFallbackToStructuredOutputTool.add(model);
@@ -3099,11 +3357,51 @@ class BedrockAdapter extends BaseProviderAdapter {
3099
3357
  };
3100
3358
  }
3101
3359
  }
3360
+ // Prompt caching via Converse cachePoint blocks (5-min default TTL; the
3361
+ // `ttl` option does not apply). Gated per model — cachePoint 400s on
3362
+ // unsupported models, which executeRequest catches and denylists.
3363
+ if (resolveCache(request.cache).enabled && this.supportsCachePoint(model)) {
3364
+ const cachePoint = { cachePoint: { type: "default" } };
3365
+ if (bedrockRequest.system && bedrockRequest.system.length > 0) {
3366
+ bedrockRequest.system = [
3367
+ ...bedrockRequest.system,
3368
+ cachePoint,
3369
+ ];
3370
+ }
3371
+ if (bedrockRequest.toolConfig?.tools?.length) {
3372
+ bedrockRequest.toolConfig = {
3373
+ ...bedrockRequest.toolConfig,
3374
+ tools: [
3375
+ ...bedrockRequest.toolConfig.tools,
3376
+ cachePoint,
3377
+ ],
3378
+ };
3379
+ }
3380
+ }
3102
3381
  if (request.providerOptions) {
3103
3382
  Object.assign(bedrockRequest, request.providerOptions);
3104
3383
  }
3105
3384
  return bedrockRequest;
3106
3385
  }
3386
+ /** Remove any cachePoint blocks so the request can be retried unsupported. */
3387
+ stripCachePoints(request) {
3388
+ const stripped = { ...request };
3389
+ if (stripped.system) {
3390
+ stripped.system = stripped.system.filter((block) => !("cachePoint" in block));
3391
+ }
3392
+ if (stripped.toolConfig?.tools) {
3393
+ stripped.toolConfig = {
3394
+ ...stripped.toolConfig,
3395
+ tools: stripped.toolConfig.tools.filter((tool) => !("cachePoint" in tool)),
3396
+ };
3397
+ }
3398
+ return stripped;
3399
+ }
3400
+ requestHasCachePoints(request) {
3401
+ const inSystem = (request.system ?? []).some((block) => "cachePoint" in block);
3402
+ const inTools = (request.toolConfig?.tools ?? []).some((tool) => "cachePoint" in tool);
3403
+ return inSystem || inTools;
3404
+ }
3107
3405
  formatTools(toolkit) {
3108
3406
  return toolkit.tools.map((tool) => ({
3109
3407
  name: tool.name,
@@ -3158,6 +3456,15 @@ class BedrockAdapter extends BaseProviderAdapter {
3158
3456
  const fallbackRequest = this.toFallbackStructuredOutputRequest(bedrockRequest);
3159
3457
  return (await bedrockClient.send(new ConverseCommand(fallbackRequest), signal ? { abortSignal: signal } : undefined));
3160
3458
  }
3459
+ if (this.requestHasCachePoints(bedrockRequest) &&
3460
+ isCachePointUnsupportedError(error)) {
3461
+ this.rememberModelRejectsCachePoint(bedrockRequest.modelId || this.defaultModel);
3462
+ const retryRequest = this.stripCachePoints(bedrockRequest);
3463
+ const response = (await bedrockClient.send(new ConverseCommand(retryRequest), signal ? { abortSignal: signal } : undefined));
3464
+ if (wantsStructuredOutput)
3465
+ response.__jaypieStructuredOutput = true;
3466
+ return response;
3467
+ }
3161
3468
  throw error;
3162
3469
  }
3163
3470
  }
@@ -3301,6 +3608,12 @@ class BedrockAdapter extends BaseProviderAdapter {
3301
3608
  reasoning: 0,
3302
3609
  total: usage?.totalTokens ??
3303
3610
  (usage?.inputTokens ?? 0) + (usage?.outputTokens ?? 0),
3611
+ ...(usage?.cacheReadInputTokens !== undefined
3612
+ ? { cacheRead: usage.cacheReadInputTokens }
3613
+ : {}),
3614
+ ...(usage?.cacheWriteInputTokens !== undefined
3615
+ ? { cacheWrite: usage.cacheWriteInputTokens }
3616
+ : {}),
3304
3617
  provider: this.name,
3305
3618
  model,
3306
3619
  };
@@ -5472,6 +5785,13 @@ class OpenAiAdapter extends BaseProviderAdapter {
5472
5785
  supportsReasoningEffort(model) {
5473
5786
  return isReasoningModel(model);
5474
5787
  }
5788
+ /**
5789
+ * Whether to emit `prompt_cache_key` (OpenAI Responses API). Overridable by
5790
+ * OpenAI-compatible subclasses whose backend rejects the field.
5791
+ */
5792
+ supportsPromptCacheKey() {
5793
+ return true;
5794
+ }
5475
5795
  /** Translate a normalized effort to this provider's `reasoning.effort` value. */
5476
5796
  mapReasoningEffort(effort, model) {
5477
5797
  return toOpenAiEffort(effort, { model });
@@ -5511,6 +5831,18 @@ class OpenAiAdapter extends BaseProviderAdapter {
5511
5831
  summary: "auto",
5512
5832
  };
5513
5833
  }
5834
+ // OpenAI prompt caching is automatic (prefix-based). Setting a stable
5835
+ // prompt_cache_key routes repeat traffic to the same cache across
5836
+ // instances. Keyed on the stable prefix only (system + instructions +
5837
+ // tools + model), never the volatile user turn.
5838
+ if (this.supportsPromptCacheKey() && resolveCache(request.cache).enabled) {
5839
+ openaiRequest.prompt_cache_key = promptCacheKey(JSON.stringify([
5840
+ model,
5841
+ request.system ?? "",
5842
+ request.instructions ?? "",
5843
+ request.tools ?? [],
5844
+ ]));
5845
+ }
5514
5846
  if (request.providerOptions) {
5515
5847
  Object.assign(openaiRequest, request.providerOptions);
5516
5848
  }
@@ -5769,11 +6101,13 @@ class OpenAiAdapter extends BaseProviderAdapter {
5769
6101
  model,
5770
6102
  };
5771
6103
  }
6104
+ const cachedTokens = openaiResponse.usage.input_tokens_details?.cached_tokens;
5772
6105
  return {
5773
6106
  input: openaiResponse.usage.input_tokens || 0,
5774
6107
  output: openaiResponse.usage.output_tokens || 0,
5775
6108
  reasoning: openaiResponse.usage.output_tokens_details?.reasoning_tokens || 0,
5776
6109
  total: openaiResponse.usage.total_tokens || 0,
6110
+ ...(cachedTokens !== undefined ? { cacheRead: cachedTokens } : {}),
5777
6111
  provider: this.name,
5778
6112
  model,
5779
6113
  };
@@ -6165,7 +6499,13 @@ class OpenRouterAdapter extends BaseProviderAdapter {
6165
6499
  //
6166
6500
  buildRequest(request) {
6167
6501
  // Convert messages to OpenRouter format (OpenAI-compatible)
6168
- const messages = this.convertMessagesToOpenRouter(request.messages, request.system);
6502
+ const cache = resolveCache(request.cache);
6503
+ const messages = this.convertMessagesToOpenRouter(request.messages, request.system, cache.enabled
6504
+ ? {
6505
+ type: "ephemeral",
6506
+ ...(cache.ttl === "1h" ? { ttl: "1h" } : {}),
6507
+ }
6508
+ : undefined);
6169
6509
  // Append instructions to last message if provided
6170
6510
  if (request.instructions && messages.length > 0) {
6171
6511
  const lastMsg = messages[messages.length - 1];
@@ -6529,11 +6869,15 @@ class OpenRouterAdapter extends BaseProviderAdapter {
6529
6869
  }
6530
6870
  // SDK returns camelCase, but support snake_case as fallback
6531
6871
  const usage = openRouterResponse.usage;
6872
+ const cachedTokens = usage.promptTokensDetails?.cachedTokens ??
6873
+ usage.promptTokensDetails?.cached_tokens ??
6874
+ usage.prompt_tokens_details?.cached_tokens;
6532
6875
  return {
6533
6876
  input: usage.promptTokens || usage.prompt_tokens || 0,
6534
6877
  output: usage.completionTokens || usage.completion_tokens || 0,
6535
6878
  reasoning: usage.completionTokensDetails?.reasoningTokens || 0,
6536
6879
  total: usage.totalTokens || usage.total_tokens || 0,
6880
+ ...(cachedTokens !== undefined ? { cacheRead: cachedTokens } : {}),
6537
6881
  provider: this.name,
6538
6882
  model,
6539
6883
  };
@@ -6734,13 +7078,17 @@ class OpenRouterAdapter extends BaseProviderAdapter {
6734
7078
  const choice = response.choices[0];
6735
7079
  return choice?.message?.content ?? undefined;
6736
7080
  }
6737
- convertMessagesToOpenRouter(messages, system) {
7081
+ convertMessagesToOpenRouter(messages, system, cacheControl) {
6738
7082
  const openRouterMessages = [];
6739
- // Add system message if provided
7083
+ // Add system message if provided. A cache_control breakpoint on the system
7084
+ // content is forwarded by OpenRouter to Anthropic/Gemini backends (and
7085
+ // ignored by others), caching the stable system prefix.
6740
7086
  if (system) {
6741
7087
  openRouterMessages.push({
6742
7088
  role: "system",
6743
- content: system,
7089
+ content: cacheControl
7090
+ ? [{ type: "text", text: system, cache_control: cacheControl }]
7091
+ : system,
6744
7092
  });
6745
7093
  }
6746
7094
  for (const msg of messages) {
@@ -8452,6 +8800,7 @@ class OperateLoop {
8452
8800
  }
8453
8801
  buildInitialRequest(state, options) {
8454
8802
  return {
8803
+ cache: options.cache,
8455
8804
  effort: options.effort,
8456
8805
  format: state.formattedFormat,
8457
8806
  instructions: options.instructions,
@@ -9085,6 +9434,7 @@ class StreamLoop {
9085
9434
  }
9086
9435
  buildInitialRequest(state, options) {
9087
9436
  return {
9437
+ cache: options.cache,
9088
9438
  effort: options.effort,
9089
9439
  format: state.formattedFormat,
9090
9440
  instructions: options.instructions,