@axlsdk/axl 0.23.2 → 0.23.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.
package/dist/index.js CHANGED
@@ -1193,38 +1193,112 @@ function unsafeTransportError(surface, remediation, exposedData) {
1193
1193
  );
1194
1194
  }
1195
1195
 
1196
+ // src/providers/rich-input.ts
1197
+ function richInputParts(input) {
1198
+ return typeof input === "string" ? [] : input.filter((part) => part.type !== "text");
1199
+ }
1200
+ function firstRichPart(input, history, modality) {
1201
+ const find = (value) => richInputParts(value).find((part) => modality === void 0 || part.type === modality);
1202
+ return find(input) ?? history.map((message) => find(message.content)).find((part) => part !== void 0);
1203
+ }
1204
+
1205
+ // src/providers/audio-format.ts
1206
+ var OPENAI_CHAT_AUDIO_FORMATS = Object.freeze({
1207
+ "audio/wav": "wav",
1208
+ "audio/x-wav": "wav",
1209
+ "audio/wave": "wav",
1210
+ "audio/mpeg": "mp3",
1211
+ "audio/mp3": "mp3"
1212
+ });
1213
+ var OPENROUTER_AUDIO_FORMATS = Object.freeze({
1214
+ ...OPENAI_CHAT_AUDIO_FORMATS,
1215
+ "audio/aiff": "aiff",
1216
+ "audio/x-aiff": "aiff",
1217
+ "audio/aac": "aac",
1218
+ "audio/ogg": "ogg",
1219
+ "audio/flac": "flac",
1220
+ "audio/x-flac": "flac",
1221
+ "audio/m4a": "m4a",
1222
+ "audio/x-m4a": "m4a",
1223
+ "audio/mp4": "m4a",
1224
+ "audio/l16": "pcm16"
1225
+ });
1226
+ function resolveAudioFormat(formats, mediaType2) {
1227
+ if (typeof mediaType2 !== "string") return void 0;
1228
+ const essence = mediaType2.split(";", 1)[0].trim().toLowerCase();
1229
+ return Object.prototype.hasOwnProperty.call(formats, essence) ? formats[essence] : void 0;
1230
+ }
1231
+
1196
1232
  // src/providers/openai-compatible.ts
1197
- function compatibleImageBase64(source) {
1233
+ function unmappableMediaTypeFeature(mediaType2) {
1234
+ return mediaType2 === void 0 ? "audio media type (missing)" : `audio media type '${mediaType2}'`;
1235
+ }
1236
+ function compatibleInlineBase64(source) {
1198
1237
  return source.type === "base64" ? source.data : Buffer.from(source.data.buffer, source.data.byteOffset, source.data.byteLength).toString(
1199
1238
  "base64"
1200
1239
  );
1201
1240
  }
1202
- function openRouterImageParts(parts, model) {
1241
+ function compatibleRichParts(parts, model, modalities, provider) {
1242
+ const reject = (modality, source, feature) => new UnsupportedModelInputError({
1243
+ provider,
1244
+ model,
1245
+ modality,
1246
+ source,
1247
+ ...feature ? { feature } : {}
1248
+ });
1203
1249
  const content = [];
1204
1250
  for (const part of parts) {
1205
1251
  if (part.type === "text") {
1206
1252
  content.push({ type: "text", text: part.text });
1207
1253
  continue;
1208
1254
  }
1209
- const { source } = part;
1210
- if (source.type === "provider-file") {
1255
+ if (part.type === "audio") {
1256
+ const { source: source2 } = part;
1257
+ const audio = modalities.audio;
1258
+ if (!audio) throw reject("audio", source2.type, "audio input");
1259
+ if (source2.type === "provider-file" || !audio.sources.includes(source2.type)) {
1260
+ throw reject("audio", source2.type);
1261
+ }
1262
+ const format = resolveAudioFormat(audio.formats, source2.mediaType);
1263
+ if (format === void 0) {
1264
+ throw reject("audio", source2.type, unmappableMediaTypeFeature(source2.mediaType));
1265
+ }
1266
+ content.push({
1267
+ type: "input_audio",
1268
+ // Caller base64 passes through verbatim — never decoded and re-encoded.
1269
+ input_audio: { data: compatibleInlineBase64(source2), format }
1270
+ });
1271
+ if (part.label) content.push({ type: "text", text: `[Audio: ${part.label}]` });
1272
+ continue;
1273
+ }
1274
+ if (part.type !== "image") {
1275
+ const unmapped = part;
1211
1276
  throw new UnsupportedModelInputError({
1212
- provider: "openrouter",
1277
+ provider,
1213
1278
  model,
1214
- modality: "image",
1215
- source: "provider-file"
1279
+ modality: unmapped.type,
1280
+ feature: "this input modality"
1216
1281
  });
1217
1282
  }
1283
+ const { source } = part;
1284
+ const image = modalities.image;
1285
+ if (!image) throw reject("image", source.type, "image input for this model");
1286
+ if (source.type === "provider-file" || !image.sources.includes(source.type)) {
1287
+ throw reject("image", source.type);
1288
+ }
1218
1289
  content.push({
1219
1290
  type: "image_url",
1220
1291
  image_url: {
1221
- url: source.type === "url" ? source.url : `data:${source.mediaType};base64,${compatibleImageBase64(source)}`
1292
+ url: source.type === "url" ? source.url : `data:${source.mediaType};base64,${compatibleInlineBase64(source)}`
1222
1293
  }
1223
1294
  });
1224
1295
  if (part.label) content.push({ type: "text", text: `[Image: ${part.label}]` });
1225
1296
  }
1226
1297
  return content;
1227
1298
  }
1299
+ function requestAudioParts(input, history) {
1300
+ return [input, ...history.map((message) => message.content)].flatMap((content) => richInputParts(content)).filter((part) => part.type === "audio");
1301
+ }
1228
1302
  function resolvePerModel(value, model, fallback) {
1229
1303
  if (value === void 0) return fallback;
1230
1304
  return typeof value === "function" ? value(model) : value;
@@ -1238,6 +1312,9 @@ function sortedKeys(table) {
1238
1312
  }
1239
1313
  return keys;
1240
1314
  }
1315
+ function reportedTokenCount(value) {
1316
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : void 0;
1317
+ }
1241
1318
  function priceFromTable(table, model, promptTokens, completionTokens, cachedTokens, match = "prefix") {
1242
1319
  const cached = cachedTokens ?? 0;
1243
1320
  if (!Number.isSafeInteger(promptTokens) || !Number.isSafeInteger(completionTokens) || !Number.isSafeInteger(cached) || promptTokens < 0 || completionTokens < 0 || cached < 0 || cached > promptTokens) {
@@ -1341,26 +1418,47 @@ function normalizeMessageContent(content) {
1341
1418
  var OpenAICompatibleProvider = class {
1342
1419
  name;
1343
1420
  reportsRequestLifecycle = true;
1421
+ /**
1422
+ * Modalities the effective profile declares for this model. A blank model
1423
+ * declares nothing, so a rich request against one fails validation exactly
1424
+ * as it did before any modality existed.
1425
+ */
1426
+ inputModalities(model) {
1427
+ return model.trim().length > 0 ? this.profile.capabilities?.inputModalities ?? {} : {};
1428
+ }
1344
1429
  inputCapabilities(model) {
1345
- return this.name === "openrouter" && model.trim().length > 0 ? { image: { sources: ["url", "bytes", "base64"] } } : {};
1430
+ const { image, audio } = this.inputModalities(model);
1431
+ return {
1432
+ ...image ? { image: { sources: image.sources } } : {},
1433
+ // `formats` is engine-internal wire detail; the runtime contract is the
1434
+ // accepted source kinds only.
1435
+ ...audio ? { audio: { sources: audio.sources } } : {}
1436
+ };
1346
1437
  }
1347
1438
  validateInput(request) {
1348
1439
  const modelOverride = request.providerOptions?.model;
1349
1440
  const effectiveModel = typeof modelOverride === "string" ? modelOverride : request.model;
1350
- const fail = (source, feature) => {
1441
+ const failWith = (modality, source, feature) => {
1351
1442
  throw new UnsupportedModelInputError({
1352
1443
  provider: this.name,
1353
1444
  model: effectiveModel || request.model,
1354
- modality: "image",
1445
+ modality,
1355
1446
  ...source ? { source } : {},
1356
1447
  ...feature ? { feature } : {}
1357
1448
  });
1358
1449
  };
1450
+ const fail = (source, feature) => failWith(firstRichPart(request.input, request.history)?.type ?? "image", source, feature);
1451
+ const modalities = this.inputModalities(effectiveModel);
1452
+ const audioPart = firstRichPart(request.input, request.history, "audio");
1453
+ if (audioPart && !modalities.audio) {
1454
+ failWith("audio", audioPart.source.type, "audio input");
1455
+ }
1359
1456
  if (request.providerOptions && "model" in request.providerOptions && (typeof modelOverride !== "string" || modelOverride.trim().length === 0)) {
1360
1457
  fail(void 0, "invalid model providerOptions");
1361
1458
  }
1362
- if (this.name !== "openrouter" || effectiveModel.trim().length === 0) {
1363
- fail(void 0, "image input for this model");
1459
+ const imagePart = firstRichPart(request.input, request.history, "image");
1460
+ if (!modalities.image && (!audioPart || imagePart)) {
1461
+ failWith("image", void 0, "image input for this model");
1364
1462
  }
1365
1463
  if (request.providerOptions && "messages" in request.providerOptions) {
1366
1464
  fail(void 0, "raw messages providerOptions");
@@ -1371,11 +1469,21 @@ var OpenAICompatibleProvider = class {
1371
1469
  if (message.content.some(
1372
1470
  (part) => part.type === "image" && part.source.type === "provider-file"
1373
1471
  )) {
1374
- fail("provider-file");
1472
+ failWith("image", "provider-file");
1375
1473
  }
1376
1474
  }
1377
1475
  if (Array.isArray(request.input) && request.input.some((part) => part.type === "image" && part.source.type === "provider-file")) {
1378
- fail("provider-file");
1476
+ failWith("image", "provider-file");
1477
+ }
1478
+ const audio = modalities.audio;
1479
+ if (audio) {
1480
+ for (const part of requestAudioParts(request.input, request.history)) {
1481
+ const source = part.source;
1482
+ if (!audio.sources.includes(source.type)) failWith("audio", source.type);
1483
+ if (resolveAudioFormat(audio.formats, source.mediaType) === void 0) {
1484
+ failWith("audio", source.type, unmappableMediaTypeFeature(source.mediaType));
1485
+ }
1486
+ }
1379
1487
  }
1380
1488
  return { effectiveModel };
1381
1489
  }
@@ -1560,9 +1668,15 @@ var OpenAICompatibleProvider = class {
1560
1668
  return body;
1561
1669
  }
1562
1670
  formatMessage(msg, model) {
1671
+ const modalities = this.inputModalities(model);
1563
1672
  const out = {
1564
1673
  role: this.profile.roleFor ? this.profile.roleFor(msg.role, model) : msg.role,
1565
- content: Array.isArray(msg.content) && this.name === "openrouter" ? openRouterImageParts(msg.content, model) : msg.content
1674
+ // History user turns go through the same builder as the caller's input,
1675
+ // so a rich part is reproduced identically in every continuation request.
1676
+ // Array content is routed UNCONDITIONALLY: a profile that declares no
1677
+ // modality has the builder reject every rich part (and pass text through)
1678
+ // rather than emitting raw `InputContentPart` objects on the wire.
1679
+ content: Array.isArray(msg.content) ? compatibleRichParts(msg.content, model, modalities, this.name) : msg.content
1566
1680
  };
1567
1681
  if (msg.name && (this.profile.capabilities?.emitsMessageName ?? true)) out.name = msg.name;
1568
1682
  if (msg.tool_calls) out.tool_calls = msg.tool_calls;
@@ -1593,6 +1707,8 @@ var OpenAICompatibleProvider = class {
1593
1707
  }
1594
1708
  toUsage(raw) {
1595
1709
  if (!raw) return void 0;
1710
+ const audioInput = reportedTokenCount(raw.prompt_tokens_details?.audio_tokens);
1711
+ const audioOutput = reportedTokenCount(raw.completion_tokens_details?.audio_tokens);
1596
1712
  return {
1597
1713
  prompt_tokens: raw.prompt_tokens,
1598
1714
  completion_tokens: raw.completion_tokens,
@@ -1602,7 +1718,9 @@ var OpenAICompatibleProvider = class {
1602
1718
  // prompt_tokens_details. Preserve the normalized hit count for the
1603
1719
  // generic table estimator; its split is validated before pricing.
1604
1720
  cached_tokens: raw.prompt_cache_hit_tokens ?? raw.prompt_tokens_details?.cached_tokens,
1605
- cache_write_tokens: raw.prompt_tokens_details?.cache_write_tokens
1721
+ cache_write_tokens: raw.prompt_tokens_details?.cache_write_tokens,
1722
+ ...audioInput !== void 0 ? { audio_input_tokens: audioInput } : {},
1723
+ ...audioOutput !== void 0 ? { audio_output_tokens: audioOutput } : {}
1606
1724
  };
1607
1725
  }
1608
1726
  reportedCost(raw) {
@@ -1628,6 +1746,9 @@ var OpenAICompatibleProvider = class {
1628
1746
  case "from-response":
1629
1747
  return typeof reportedCost === "number" && Number.isFinite(reportedCost) && reportedCost >= 0 ? reportedCost : void 0;
1630
1748
  case "table": {
1749
+ if (typeof usage.audio_input_tokens === "number" && usage.audio_input_tokens > 0 || typeof usage.audio_output_tokens === "number" && usage.audio_output_tokens > 0) {
1750
+ return void 0;
1751
+ }
1631
1752
  if (!isBuiltinTablePricingEligible(this.profile, {
1632
1753
  baseUrl: this.baseUrl,
1633
1754
  request: context?.request,
@@ -1975,6 +2096,26 @@ var DIRECT_OPENAI_CATALOG = [
1975
2096
  }),
1976
2097
  contextBoundary: LONG_CONTEXT_BOUNDARY
1977
2098
  },
2099
+ // Audio-capable models. Audio rates reviewed 2026-09-08 against
2100
+ // https://developers.openai.com/api/docs/pricing (no version stamp on the
2101
+ // page): `gpt-audio-1.5` and `gpt-audio` both list Text $2.50 in / $10.00
2102
+ // out and Audio $32.00 in / $64.00 out per 1M tokens, with no cached-input
2103
+ // row — hence no `cachedInput` here, which makes a cache-hit report unpriced
2104
+ // rather than silently billed at the full input rate. Only these exact ids
2105
+ // are listed: no snapshot id is published for either, and inventing one
2106
+ // would price an unknown model.
2107
+ {
2108
+ aliases: ["gpt-audio-1.5"],
2109
+ snapshotBase: "gpt-audio-1.5",
2110
+ short: { input: 2.5 / M, output: 10 / M },
2111
+ audio: { input: 32 / M, output: 64 / M }
2112
+ },
2113
+ {
2114
+ aliases: ["gpt-audio"],
2115
+ snapshotBase: "gpt-audio",
2116
+ short: { input: 2.5 / M, output: 10 / M },
2117
+ audio: { input: 32 / M, output: 64 / M }
2118
+ },
1978
2119
  // Existing direct models retain literal current Standard rows. These flat
1979
2120
  // rows keep native pricing aligned with the compatibility wrapper while the
1980
2121
  // private estimator still validates categories and billing mode strictly.
@@ -2125,32 +2266,45 @@ var CANONICAL_OPENAI_BASE_URL = "https://api.openai.com/v1";
2125
2266
  function isTextContentPart(value, type) {
2126
2267
  return value !== null && typeof value === "object" && value.type === type && typeof value.text === "string";
2127
2268
  }
2128
- function hasUnmodeledDirectOpenAIContent(request) {
2269
+ function isChatAudioContentPart(value) {
2270
+ if (value === null || typeof value !== "object") return false;
2271
+ const part = value;
2272
+ return part.type === "input_audio" && part.input_audio !== null && typeof part.input_audio === "object";
2273
+ }
2274
+ function classifyDirectOpenAIContent(request) {
2275
+ if (!request) return "text";
2129
2276
  if ("messages" in request) {
2130
- if (!Array.isArray(request.messages)) return true;
2277
+ if (!Array.isArray(request.messages)) return "unmodeled";
2278
+ let carriesAudio = false;
2131
2279
  for (const message of request.messages) {
2132
- if (message === null || typeof message !== "object") return true;
2280
+ if (message === null || typeof message !== "object") return "unmodeled";
2133
2281
  const content = message.content;
2134
2282
  if (content === void 0 || content === null || typeof content === "string") continue;
2135
- if (!Array.isArray(content) || content.some((part) => !isTextContentPart(part, "text"))) {
2136
- return true;
2283
+ if (!Array.isArray(content)) return "unmodeled";
2284
+ for (const part of content) {
2285
+ if (isTextContentPart(part, "text")) continue;
2286
+ if (isChatAudioContentPart(part)) {
2287
+ carriesAudio = true;
2288
+ continue;
2289
+ }
2290
+ return "unmodeled";
2137
2291
  }
2138
2292
  }
2139
- return false;
2293
+ return carriesAudio ? "audio" : "text";
2140
2294
  }
2141
2295
  if ("input" in request) {
2142
2296
  const input = request.input;
2143
- if (typeof input === "string") return false;
2144
- if (!Array.isArray(input)) return true;
2297
+ if (typeof input === "string") return "text";
2298
+ if (!Array.isArray(input)) return "unmodeled";
2145
2299
  for (const item of input) {
2146
- if (item === null || typeof item !== "object") return true;
2300
+ if (item === null || typeof item !== "object") return "unmodeled";
2147
2301
  const typed = item;
2148
2302
  switch (typed.type) {
2149
2303
  case "message": {
2150
2304
  const content = typed.content;
2151
2305
  if (typeof content === "string") break;
2152
2306
  if (!Array.isArray(content) || content.some((part) => !isTextContentPart(part, "input_text"))) {
2153
- return true;
2307
+ return "unmodeled";
2154
2308
  }
2155
2309
  break;
2156
2310
  }
@@ -2160,31 +2314,30 @@ function hasUnmodeledDirectOpenAIContent(request) {
2160
2314
  const output = typed.output;
2161
2315
  if (typeof output === "string") break;
2162
2316
  if (!Array.isArray(output) || output.some((part) => !isTextContentPart(part, "input_text"))) {
2163
- return true;
2317
+ return "unmodeled";
2164
2318
  }
2165
2319
  break;
2166
2320
  }
2167
2321
  case "custom_tool_call":
2168
- if (typeof typed.input !== "string") return true;
2322
+ if (typeof typed.input !== "string") return "unmodeled";
2169
2323
  break;
2170
2324
  case "custom_tool_call_output":
2171
- if (typeof typed.output !== "string") return true;
2325
+ if (typeof typed.output !== "string") return "unmodeled";
2172
2326
  break;
2173
2327
  case "reasoning":
2174
2328
  break;
2175
2329
  default:
2176
- return true;
2330
+ return "unmodeled";
2177
2331
  }
2178
2332
  }
2179
- return false;
2333
+ return "text";
2180
2334
  }
2181
- return false;
2335
+ return "text";
2182
2336
  }
2183
- function isEligibleDirectOpenAIContext(context) {
2337
+ function isEligibleDirectOpenAIContext(context, entry) {
2184
2338
  const request = context?.request;
2185
2339
  const response = context?.response;
2186
2340
  if (context?.baseUrl !== void 0 && context.baseUrl !== CANONICAL_OPENAI_BASE_URL) return false;
2187
- if (request && hasUnmodeledDirectOpenAIContent(request)) return false;
2188
2341
  const tier = response?.service_tier ?? response?.serviceTier ?? request?.service_tier ?? request?.serviceTier;
2189
2342
  if (tier !== void 0 && (typeof tier !== "string" || !["standard", "default"].includes(tier.toLowerCase()))) {
2190
2343
  return false;
@@ -2199,7 +2352,14 @@ function isEligibleDirectOpenAIContext(context) {
2199
2352
  if (reasoning !== null && typeof reasoning === "object" && "mode" in reasoning && reasoning.mode !== void 0) {
2200
2353
  return false;
2201
2354
  }
2202
- if (request?.modalities !== void 0 && (!Array.isArray(request.modalities) || request.modalities.length === 0 || request.modalities.some((modality) => modality !== "text")) || ["audio", "image_generation", "web_search_options"].some((key) => request?.[key] !== void 0)) {
2355
+ const audioOutputRated = entry.audio?.output !== void 0;
2356
+ if (request?.modalities !== void 0 && (!Array.isArray(request.modalities) || request.modalities.length === 0 || request.modalities.some(
2357
+ (modality) => modality !== "text" && !(modality === "audio" && audioOutputRated)
2358
+ ))) {
2359
+ return false;
2360
+ }
2361
+ if (request?.audio !== void 0 && !audioOutputRated) return false;
2362
+ if (["image_generation", "web_search_options"].some((key) => request?.[key] !== void 0)) {
2203
2363
  return false;
2204
2364
  }
2205
2365
  if (Array.isArray(request?.tools) && request.tools.some(
@@ -2210,23 +2370,42 @@ function isEligibleDirectOpenAIContext(context) {
2210
2370
  return true;
2211
2371
  }
2212
2372
  function estimateDirectOpenAICost(model, usage, context) {
2213
- if (!isEligibleDirectOpenAIContext(context)) return void 0;
2214
2373
  const entry = directOpenAIModel(model);
2215
2374
  if (!entry) return void 0;
2216
- const { prompt_tokens, completion_tokens, cached_tokens, cache_write_tokens } = usage;
2375
+ const content = classifyDirectOpenAIContent(context?.request);
2376
+ if (content === "unmodeled") return void 0;
2377
+ if (!isEligibleDirectOpenAIContext(context, entry)) return void 0;
2378
+ const {
2379
+ prompt_tokens,
2380
+ completion_tokens,
2381
+ cached_tokens,
2382
+ cache_write_tokens,
2383
+ audio_input_tokens,
2384
+ audio_output_tokens
2385
+ } = usage;
2386
+ if (content === "audio" && audio_input_tokens === void 0) return void 0;
2217
2387
  const cached = cached_tokens ?? 0;
2218
2388
  const cacheWrite = cache_write_tokens ?? 0;
2219
- if (![prompt_tokens, completion_tokens, cached, cacheWrite].every(
2389
+ const audioInput = audio_input_tokens ?? 0;
2390
+ const audioOutput = audio_output_tokens ?? 0;
2391
+ if (![prompt_tokens, completion_tokens, cached, cacheWrite, audioInput, audioOutput].every(
2220
2392
  (count) => Number.isSafeInteger(count) && count >= 0
2221
- ) || cached + cacheWrite > prompt_tokens) {
2393
+ ) || cached + cacheWrite + audioInput > prompt_tokens || audioOutput > completion_tokens) {
2222
2394
  return void 0;
2223
2395
  }
2224
- const rates = entry.long !== void 0 && entry.contextBoundary !== void 0 && prompt_tokens > entry.contextBoundary ? entry.long : entry.short;
2396
+ if (audioInput > 0 && entry.audio === void 0 || audioOutput > 0 && entry.audio?.output === void 0) {
2397
+ return void 0;
2398
+ }
2399
+ if (audioInput > 0 && cached + cacheWrite > 0) return void 0;
2400
+ const crossesLongContext = entry.long !== void 0 && entry.contextBoundary !== void 0 && prompt_tokens > entry.contextBoundary;
2401
+ if (crossesLongContext && (audioInput > 0 || audioOutput > 0)) return void 0;
2402
+ const rates = crossesLongContext ? entry.long : entry.short;
2225
2403
  if (cached > 0 && rates.cachedInput === void 0 || cacheWrite > 0 && rates.cacheWrite === void 0) {
2226
2404
  return void 0;
2227
2405
  }
2228
- const ordinary = prompt_tokens - cached - cacheWrite;
2229
- return ordinary * rates.input + cached * (rates.cachedInput ?? 0) + cacheWrite * (rates.cacheWrite ?? 0) + completion_tokens * rates.output;
2406
+ const ordinary = prompt_tokens - cached - cacheWrite - audioInput;
2407
+ const textOutput = completion_tokens - audioOutput;
2408
+ return ordinary * rates.input + cached * (rates.cachedInput ?? 0) + cacheWrite * (rates.cacheWrite ?? 0) + audioInput * (entry.audio?.input ?? 0) + textOutput * rates.output + audioOutput * (entry.audio?.output ?? 0);
2230
2409
  }
2231
2410
  function estimateOpenAICost(model, promptTokens, completionTokens, cachedTokens) {
2232
2411
  return estimateDirectOpenAICost(model, {
@@ -2316,6 +2495,14 @@ var OPENAI_PROFILE = {
2316
2495
  envBaseUrl: "OPENAI_BASE_URL",
2317
2496
  pricing: { kind: "table", table: OPENAI_PRICING, match: "exact" },
2318
2497
  reasoning: { emit: openaiReasoningEmit, capture: "none" },
2498
+ capabilities: {
2499
+ // Chat Completions carries recorded audio via `input_audio` (wav/mp3 only).
2500
+ // Images are deliberately NOT declared: the Responses API serves images, and
2501
+ // Chat-Completions image pricing is unmodeled here (fork F4).
2502
+ inputModalities: {
2503
+ audio: { sources: ["bytes", "base64"], formats: OPENAI_CHAT_AUDIO_FORMATS }
2504
+ }
2505
+ },
2319
2506
  roleFor: (role, model) => role === "system" && isOSeriesModel(model) ? "developer" : role,
2320
2507
  maxTokensField: "max_completion_tokens",
2321
2508
  parallelToolCalls: (model) => !isOSeriesModel(model)
@@ -2344,19 +2531,37 @@ function base64FromSource(source) {
2344
2531
  "base64"
2345
2532
  );
2346
2533
  }
2347
- function responseImageParts(parts) {
2534
+ function responseImageParts(parts, model) {
2348
2535
  const mapped = [];
2349
2536
  for (const part of parts) {
2350
2537
  if (part.type === "text") {
2351
2538
  mapped.push({ type: "input_text", text: part.text });
2352
2539
  continue;
2353
2540
  }
2541
+ if (part.type === "audio") {
2542
+ throw new UnsupportedModelInputError({
2543
+ provider: "openai-responses",
2544
+ model,
2545
+ modality: "audio",
2546
+ source: part.source.type,
2547
+ feature: "audio input"
2548
+ });
2549
+ }
2550
+ if (part.type !== "image") {
2551
+ const unmapped = part;
2552
+ throw new UnsupportedModelInputError({
2553
+ provider: "openai-responses",
2554
+ model,
2555
+ modality: unmapped.type,
2556
+ feature: "this input modality"
2557
+ });
2558
+ }
2354
2559
  const { source } = part;
2355
2560
  if (source.type === "provider-file") {
2356
2561
  if (source.provider !== "openai-responses") {
2357
2562
  throw new UnsupportedModelInputError({
2358
2563
  provider: "openai-responses",
2359
- model: "unknown",
2564
+ model,
2360
2565
  modality: "image",
2361
2566
  source: "provider-file"
2362
2567
  });
@@ -2374,6 +2579,21 @@ function responseImageParts(parts) {
2374
2579
  }
2375
2580
  return mapped;
2376
2581
  }
2582
+ function toResponsesUsage(raw) {
2583
+ if (!raw) return void 0;
2584
+ const audioInput = reportedTokenCount(raw.input_tokens_details?.audio_tokens);
2585
+ const audioOutput = reportedTokenCount(raw.output_tokens_details?.audio_tokens);
2586
+ return {
2587
+ prompt_tokens: raw.input_tokens,
2588
+ completion_tokens: raw.output_tokens,
2589
+ total_tokens: raw.total_tokens,
2590
+ reasoning_tokens: raw.output_tokens_details?.reasoning_tokens,
2591
+ cached_tokens: raw.input_tokens_details?.cached_tokens,
2592
+ cache_write_tokens: raw.input_tokens_details?.cache_write_tokens,
2593
+ ...audioInput !== void 0 ? { audio_input_tokens: audioInput } : {},
2594
+ ...audioOutput !== void 0 ? { audio_output_tokens: audioOutput } : {}
2595
+ };
2596
+ }
2377
2597
  var OpenAIResponsesProvider = class {
2378
2598
  name = "openai-responses";
2379
2599
  reportsRequestLifecycle = true;
@@ -2389,15 +2609,18 @@ var OpenAIResponsesProvider = class {
2389
2609
  validateInput(request) {
2390
2610
  const modelOverride = request.providerOptions?.model;
2391
2611
  const effectiveModel = typeof modelOverride === "string" ? modelOverride : request.model;
2392
- const fail = (source, feature) => {
2612
+ const failWith = (modality, source, feature) => {
2393
2613
  throw new UnsupportedModelInputError({
2394
2614
  provider: this.name,
2395
2615
  model: effectiveModel || request.model,
2396
- modality: "image",
2616
+ modality,
2397
2617
  ...source ? { source } : {},
2398
2618
  ...feature ? { feature } : {}
2399
2619
  });
2400
2620
  };
2621
+ const fail = (source, feature) => failWith("image", source, feature);
2622
+ const audioPart = firstRichPart(request.input, request.history, "audio");
2623
+ if (audioPart) failWith("audio", audioPart.source.type, "audio input");
2401
2624
  if (request.providerOptions && "model" in request.providerOptions && (typeof modelOverride !== "string" || modelOverride.trim().length === 0)) {
2402
2625
  fail(void 0, "invalid model providerOptions");
2403
2626
  }
@@ -2544,7 +2767,7 @@ var OpenAIResponsesProvider = class {
2544
2767
  const nonSystemMessages = messages.filter((m) => m.role !== "system");
2545
2768
  const body = {
2546
2769
  model: effectiveModel,
2547
- input: this.buildInput(nonSystemMessages),
2770
+ input: this.buildInput(nonSystemMessages, effectiveModel),
2548
2771
  store: false,
2549
2772
  stream
2550
2773
  };
@@ -2596,7 +2819,7 @@ var OpenAIResponsesProvider = class {
2596
2819
  // ---------------------------------------------------------------------------
2597
2820
  // Internal: message → input mapping
2598
2821
  // ---------------------------------------------------------------------------
2599
- buildInput(messages) {
2822
+ buildInput(messages, model) {
2600
2823
  const input = [];
2601
2824
  for (const msg of messages) {
2602
2825
  const text = typeof msg.content === "string" ? msg.content : "";
@@ -2634,7 +2857,7 @@ var OpenAIResponsesProvider = class {
2634
2857
  input.push({
2635
2858
  type: "message",
2636
2859
  role: msg.role,
2637
- content: typeof msg.content === "string" ? msg.content : responseImageParts(msg.content)
2860
+ content: typeof msg.content === "string" ? msg.content : responseImageParts(msg.content, model)
2638
2861
  });
2639
2862
  }
2640
2863
  }
@@ -2693,14 +2916,7 @@ var OpenAIResponsesProvider = class {
2693
2916
  }
2694
2917
  }
2695
2918
  }
2696
- const usage = json.usage ? {
2697
- prompt_tokens: json.usage.input_tokens,
2698
- completion_tokens: json.usage.output_tokens,
2699
- total_tokens: json.usage.total_tokens,
2700
- reasoning_tokens: json.usage.output_tokens_details?.reasoning_tokens,
2701
- cached_tokens: json.usage.input_tokens_details?.cached_tokens,
2702
- cache_write_tokens: json.usage.input_tokens_details?.cache_write_tokens
2703
- } : void 0;
2919
+ const usage = toResponsesUsage(json.usage);
2704
2920
  const cost = usage && !this.requestContainsImages(request) ? estimateDirectOpenAICost(json.model ?? model, usage, {
2705
2921
  baseUrl: this.baseUrl,
2706
2922
  request,
@@ -2807,14 +3023,7 @@ var OpenAIResponsesProvider = class {
2807
3023
  }
2808
3024
  case "response.completed": {
2809
3025
  const response = data.response;
2810
- const usage = response?.usage ? {
2811
- prompt_tokens: response.usage.input_tokens,
2812
- completion_tokens: response.usage.output_tokens,
2813
- total_tokens: response.usage.total_tokens,
2814
- reasoning_tokens: response.usage.output_tokens_details?.reasoning_tokens,
2815
- cached_tokens: response.usage.input_tokens_details?.cached_tokens,
2816
- cache_write_tokens: response.usage.input_tokens_details?.cache_write_tokens
2817
- } : void 0;
3026
+ const usage = toResponsesUsage(response?.usage);
2818
3027
  const reasoningItems = response?.output?.filter((item) => item.type === "reasoning") ?? [];
2819
3028
  const providerMetadata = reasoningItems.length > 0 ? { openaiReasoningItems: reasoningItems } : void 0;
2820
3029
  return {
@@ -2863,19 +3072,37 @@ function anthropicBase64(source) {
2863
3072
  "base64"
2864
3073
  );
2865
3074
  }
2866
- function anthropicImageBlocks(parts) {
3075
+ function anthropicImageBlocks(parts, model) {
2867
3076
  const blocks = [];
2868
3077
  for (const part of parts) {
2869
3078
  if (part.type === "text") {
2870
3079
  blocks.push({ type: "text", text: part.text });
2871
3080
  continue;
2872
3081
  }
3082
+ if (part.type === "audio") {
3083
+ throw new UnsupportedModelInputError({
3084
+ provider: "anthropic",
3085
+ model,
3086
+ modality: "audio",
3087
+ source: part.source.type,
3088
+ feature: "audio input"
3089
+ });
3090
+ }
3091
+ if (part.type !== "image") {
3092
+ const unmapped = part;
3093
+ throw new UnsupportedModelInputError({
3094
+ provider: "anthropic",
3095
+ model,
3096
+ modality: unmapped.type,
3097
+ feature: "this input modality"
3098
+ });
3099
+ }
2873
3100
  const { source } = part;
2874
3101
  if (source.type === "provider-file") {
2875
3102
  if (source.provider !== "anthropic") {
2876
3103
  throw new UnsupportedModelInputError({
2877
3104
  provider: "anthropic",
2878
- model: "unknown",
3105
+ model,
2879
3106
  modality: "image",
2880
3107
  source: "provider-file"
2881
3108
  });
@@ -3337,15 +3564,18 @@ var AnthropicProvider = class {
3337
3564
  validateInput(request) {
3338
3565
  const modelOverride = request.providerOptions?.model;
3339
3566
  const effectiveModel = typeof modelOverride === "string" ? modelOverride : request.model;
3340
- const fail = (source, feature) => {
3567
+ const failWith = (modality, source, feature) => {
3341
3568
  throw new UnsupportedModelInputError({
3342
3569
  provider: this.name,
3343
3570
  model: effectiveModel || request.model,
3344
- modality: "image",
3571
+ modality,
3345
3572
  ...source ? { source } : {},
3346
3573
  ...feature ? { feature } : {}
3347
3574
  });
3348
3575
  };
3576
+ const fail = (source, feature) => failWith("image", source, feature);
3577
+ const audioPart = firstRichPart(request.input, request.history, "audio");
3578
+ if (audioPart) failWith("audio", audioPart.source.type, "audio input");
3349
3579
  if (request.providerOptions && "model" in request.providerOptions && (typeof modelOverride !== "string" || modelOverride.trim().length === 0)) {
3350
3580
  fail(void 0, "invalid model providerOptions");
3351
3581
  }
@@ -3516,7 +3746,7 @@ var AnthropicProvider = class {
3516
3746
  const systemText = systemMessages.map((m) => m.content).join("\n\n");
3517
3747
  const body = {
3518
3748
  model: effectiveModel,
3519
- messages: this.mapMessages(nonSystemMessages),
3749
+ messages: this.mapMessages(nonSystemMessages, effectiveModel),
3520
3750
  max_tokens: options.maxTokens ?? 4096,
3521
3751
  stream
3522
3752
  };
@@ -3583,7 +3813,7 @@ ${jsonInstruction}` : jsonInstruction;
3583
3813
  * - assistant messages with tool_calls -> assistant with tool_use content blocks
3584
3814
  * - tool messages (tool results) -> user messages with tool_result content blocks
3585
3815
  */
3586
- mapMessages(messages) {
3816
+ mapMessages(messages, model) {
3587
3817
  const result = [];
3588
3818
  for (const msg of messages) {
3589
3819
  const text = typeof msg.content === "string" ? msg.content : "";
@@ -3630,7 +3860,7 @@ ${jsonInstruction}` : jsonInstruction;
3630
3860
  } else if (msg.role === "user") {
3631
3861
  result.push({
3632
3862
  role: "user",
3633
- content: typeof msg.content === "string" ? msg.content : anthropicImageBlocks(msg.content)
3863
+ content: typeof msg.content === "string" ? msg.content : anthropicImageBlocks(msg.content, model)
3634
3864
  });
3635
3865
  }
3636
3866
  }
@@ -3934,7 +4164,7 @@ function lastFallbackIndex(blocks) {
3934
4164
  function hasRichGeminiMessages(messages) {
3935
4165
  return messages.some((message) => Array.isArray(message.content));
3936
4166
  }
3937
- function geminiImageBase64(source) {
4167
+ function geminiInlineBase64(source) {
3938
4168
  return source.type === "base64" ? source.data : Buffer.from(source.data.buffer, source.data.byteOffset, source.data.byteLength).toString(
3939
4169
  "base64"
3940
4170
  );
@@ -3946,6 +4176,50 @@ function geminiInteractionContent(parts, model) {
3946
4176
  content.push({ type: "text", text: part.text });
3947
4177
  continue;
3948
4178
  }
4179
+ if (part.type === "audio") {
4180
+ const { source: audioSource } = part;
4181
+ if (audioSource.type === "provider-file") {
4182
+ if (audioSource.provider !== "google") {
4183
+ throw new UnsupportedModelInputError({
4184
+ provider: "google",
4185
+ model,
4186
+ modality: "audio",
4187
+ source: "provider-file"
4188
+ });
4189
+ }
4190
+ if (!audioSource.mediaType) {
4191
+ throw new UnsupportedModelInputError({
4192
+ provider: "google",
4193
+ model,
4194
+ modality: "audio",
4195
+ source: "provider-file",
4196
+ feature: "Interactions URI audio mediaType"
4197
+ });
4198
+ }
4199
+ content.push({
4200
+ type: "audio",
4201
+ uri: audioSource.reference,
4202
+ mime_type: audioSource.mediaType
4203
+ });
4204
+ } else {
4205
+ content.push({
4206
+ type: "audio",
4207
+ data: geminiInlineBase64(audioSource),
4208
+ mime_type: audioSource.mediaType
4209
+ });
4210
+ }
4211
+ if (part.label) content.push({ type: "text", text: `[Audio: ${part.label}]` });
4212
+ continue;
4213
+ }
4214
+ if (part.type !== "image") {
4215
+ const unmapped = part;
4216
+ throw new UnsupportedModelInputError({
4217
+ provider: "google",
4218
+ model,
4219
+ modality: unmapped.type,
4220
+ feature: "this input modality"
4221
+ });
4222
+ }
3949
4223
  const { source } = part;
3950
4224
  if (source.type === "provider-file") {
3951
4225
  if (source.provider !== "google") {
@@ -3979,7 +4253,11 @@ function geminiInteractionContent(parts, model) {
3979
4253
  feature: "direct URL image input; pass bytes/base64 or a Gemini provider-file"
3980
4254
  });
3981
4255
  } else {
3982
- content.push({ type: "image", data: geminiImageBase64(source), mime_type: source.mediaType });
4256
+ content.push({
4257
+ type: "image",
4258
+ data: geminiInlineBase64(source),
4259
+ mime_type: source.mediaType
4260
+ });
3983
4261
  }
3984
4262
  if (part.label) content.push({ type: "text", text: `[Image: ${part.label}]` });
3985
4263
  }
@@ -4036,7 +4314,7 @@ var GEMINI_PRICING = {
4036
4314
  output: 1e-5,
4037
4315
  longContext: { input: 25e-7, cached: 25e-8, output: 15e-6 }
4038
4316
  },
4039
- "gemini-2.5-flash": { input: 3e-7, cached: 3e-8, output: 25e-7 },
4317
+ "gemini-2.5-flash": { input: 3e-7, cached: 3e-8, output: 25e-7, audioInput: 1e-6 },
4040
4318
  "gemini-2.5-flash-lite": { input: 1e-7, cached: 1e-8, output: 4e-7 },
4041
4319
  "gemini-3.1-pro-preview": {
4042
4320
  input: 2e-6,
@@ -4054,18 +4332,43 @@ var GEMINI_PRICING = {
4054
4332
  "gemini-3.5-flash": { input: 15e-7, cached: 15e-8, output: 9e-6 },
4055
4333
  "gemini-3.5-flash-lite": { input: 3e-7, cached: 3e-8, output: 25e-7 },
4056
4334
  "gemini-3.6-flash": { input: 75e-8, cached: 75e-9, output: 375e-8 },
4057
- "gemini-3.7-flash": { input: 75e-8, cached: 75e-9, output: 375e-8 },
4335
+ "gemini-3.7-flash": {
4336
+ input: 75e-8,
4337
+ cached: 75e-9,
4338
+ output: 375e-8,
4339
+ audioInput: 75e-8
4340
+ },
4058
4341
  "gemini-3.8-flash": { input: 75e-8, cached: 75e-9, output: 375e-8 }
4059
4342
  };
4343
+ var CANONICAL_GEMINI_BASE_URL = "https://generativelanguage.googleapis.com/v1beta";
4060
4344
  function isValidTokenCount2(value) {
4061
4345
  return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
4062
4346
  }
4063
4347
  function isStandardGeminiRequestTier(value) {
4064
- return value === void 0 || value === "SERVICE_TIER_UNSPECIFIED" || value === "SERVICE_TIER_STANDARD";
4348
+ return value === void 0 || value === null || isDefinitiveStandardGeminiResponseTier(value);
4065
4349
  }
4066
4350
  function isDefinitiveStandardGeminiResponseTier(value) {
4067
4351
  return value === "SERVICE_TIER_UNSPECIFIED" || value === "SERVICE_TIER_STANDARD" || value === "standard";
4068
4352
  }
4353
+ function recordGeminiResponseTier(evidence, value) {
4354
+ if (value === void 0) return;
4355
+ evidence.reportsTier = true;
4356
+ if (isDefinitiveStandardGeminiResponseTier(value)) {
4357
+ evidence.hasDefinitiveStandardTier = true;
4358
+ } else {
4359
+ evidence.hasInvalidTier = true;
4360
+ }
4361
+ }
4362
+ function geminiResponseTierEvidence(responseHeaders, ...values) {
4363
+ const evidence = {
4364
+ reportsTier: false,
4365
+ hasDefinitiveStandardTier: false,
4366
+ hasInvalidTier: false
4367
+ };
4368
+ recordGeminiResponseTier(evidence, responseHeaders?.get("x-gemini-service-tier") ?? void 0);
4369
+ for (const value of values) recordGeminiResponseTier(evidence, value);
4370
+ return evidence;
4371
+ }
4069
4372
  function geminiInteractionErrorStatus(code) {
4070
4373
  if (typeof code === "number" && Number.isInteger(code) && code >= 100 && code <= 599) {
4071
4374
  return code;
@@ -4124,8 +4427,19 @@ function estimateGeminiCost(model, usage) {
4124
4427
  if (!pricing || !isValidTokenCount2(usage.inputTokens) || !isValidTokenCount2(usage.outputTokens) || !isValidTokenCount2(cached) || cached > usage.inputTokens) {
4125
4428
  return void 0;
4126
4429
  }
4127
- const rate = usage.inputTokens > 2e5 && pricing.longContext ? pricing.longContext : pricing;
4128
- return (usage.inputTokens - cached) * rate.input + cached * rate.cached + usage.outputTokens * rate.output;
4430
+ const split = usage.modalities;
4431
+ const audio = split?.audioTokens ?? 0;
4432
+ if (split !== void 0) {
4433
+ if (audio > 0 && pricing.audioInput === void 0) return void 0;
4434
+ if (!isValidTokenCount2(audio) || !isValidTokenCount2(split.nonAudioTokens) || audio + split.nonAudioTokens !== usage.inputTokens || cached + audio > usage.inputTokens) {
4435
+ return void 0;
4436
+ }
4437
+ if (audio > 0 && cached > 0) return void 0;
4438
+ }
4439
+ const crossesLongContext = usage.inputTokens > 2e5 && pricing.longContext !== void 0;
4440
+ if (crossesLongContext && audio > 0) return void 0;
4441
+ const rate = crossesLongContext ? pricing.longContext : pricing;
4442
+ return (usage.inputTokens - cached - audio) * rate.input + cached * rate.cached + audio * (pricing.audioInput ?? 0) + usage.outputTokens * rate.output;
4129
4443
  }
4130
4444
  function safeTokenSum2(...values) {
4131
4445
  const total = values.reduce((sum, value) => sum + value, 0);
@@ -4285,20 +4599,44 @@ var GeminiProvider = class {
4285
4599
  name = "google";
4286
4600
  reportsRequestLifecycle = true;
4287
4601
  inputCapabilities(model) {
4288
- return model.trim().length > 0 ? { image: { sources: ["bytes", "base64", "provider-file"] } } : {};
4602
+ return model.trim().length > 0 ? {
4603
+ image: { sources: ["bytes", "base64", "provider-file"] },
4604
+ audio: { sources: ["bytes", "base64", "provider-file"] }
4605
+ } : {};
4289
4606
  }
4290
4607
  validateInput(request) {
4291
4608
  const modelOverride = request.providerOptions?.model;
4292
4609
  const effectiveModel = typeof modelOverride === "string" ? modelOverride : request.model;
4293
- const fail = (source, feature) => {
4610
+ const failWith = (modality, source, feature) => {
4294
4611
  throw new UnsupportedModelInputError({
4295
4612
  provider: this.name,
4296
4613
  model: effectiveModel || request.model,
4297
- modality: "image",
4614
+ modality,
4298
4615
  ...source ? { source } : {},
4299
4616
  ...feature ? { feature } : {}
4300
4617
  });
4301
4618
  };
4619
+ const fail = (source, feature) => failWith(firstRichPart(request.input, request.history)?.type ?? "image", source, feature);
4620
+ const checkPart = (part) => {
4621
+ if (part.type === "audio") {
4622
+ if (part.source.type === "provider-file" && part.source.provider !== this.name)
4623
+ failWith("audio", "provider-file");
4624
+ if (part.source.type === "provider-file" && !part.source.mediaType)
4625
+ failWith("audio", "provider-file", "Interactions URI audio mediaType");
4626
+ return;
4627
+ }
4628
+ if (part.type !== "image") return;
4629
+ if (part.source.type === "url")
4630
+ failWith(
4631
+ "image",
4632
+ "url",
4633
+ "direct URL image input; pass bytes/base64 or a Gemini provider-file"
4634
+ );
4635
+ if (part.source.type === "provider-file" && part.source.provider !== this.name)
4636
+ failWith("image", "provider-file");
4637
+ if (part.source.type === "provider-file" && !part.source.mediaType)
4638
+ failWith("image", "provider-file", "Interactions URI image mediaType");
4639
+ };
4302
4640
  if (request.providerOptions && "model" in request.providerOptions && (typeof modelOverride !== "string" || modelOverride.trim().length === 0)) {
4303
4641
  fail(void 0, "invalid model providerOptions");
4304
4642
  }
@@ -4320,26 +4658,10 @@ var GeminiProvider = class {
4320
4658
  for (const message of request.history) {
4321
4659
  if (!Array.isArray(message.content)) continue;
4322
4660
  if (message.role !== "user") fail(void 0, "rich non-user history");
4323
- for (const part of message.content) {
4324
- if (part.type !== "image") continue;
4325
- if (part.source.type === "url")
4326
- fail("url", "direct URL image input; pass bytes/base64 or a Gemini provider-file");
4327
- if (part.source.type === "provider-file" && part.source.provider !== this.name)
4328
- fail("provider-file");
4329
- if (part.source.type === "provider-file" && !part.source.mediaType)
4330
- fail("provider-file", "Interactions URI image mediaType");
4331
- }
4661
+ for (const part of message.content) checkPart(part);
4332
4662
  }
4333
4663
  if (Array.isArray(request.input)) {
4334
- for (const part of request.input) {
4335
- if (part.type !== "image") continue;
4336
- if (part.source.type === "url")
4337
- fail("url", "direct URL image input; pass bytes/base64 or a Gemini provider-file");
4338
- if (part.source.type === "provider-file" && part.source.provider !== this.name)
4339
- fail("provider-file");
4340
- if (part.source.type === "provider-file" && !part.source.mediaType)
4341
- fail("provider-file", "Interactions URI image mediaType");
4342
- }
4664
+ for (const part of request.input) checkPart(part);
4343
4665
  }
4344
4666
  return { effectiveModel };
4345
4667
  }
@@ -4475,6 +4797,7 @@ var GeminiProvider = class {
4475
4797
  async chatInteraction(messages, options) {
4476
4798
  this.assertSafeInteractionOptions(options);
4477
4799
  const body = this.buildInteractionRequestBody(messages, options, false);
4800
+ const pricingContext = this.interactionPricingContext(this.requestModel(options), body);
4478
4801
  const headers = this.buildHeaders(await this.resolveKey());
4479
4802
  const recorder = new CallTimingRecorder(options.requestLifecycle);
4480
4803
  const res = await fetchWithRetry(
@@ -4495,12 +4818,17 @@ var GeminiProvider = class {
4495
4818
  }
4496
4819
  return withChatTiming(
4497
4820
  recorder,
4498
- this.parseInteractionResponse(await res.json())
4821
+ this.parseInteractionResponse(
4822
+ await res.json(),
4823
+ pricingContext,
4824
+ res.headers
4825
+ )
4499
4826
  );
4500
4827
  }
4501
4828
  async *streamInteraction(messages, options) {
4502
4829
  this.assertSafeInteractionOptions(options);
4503
4830
  const body = this.buildInteractionRequestBody(messages, options, true);
4831
+ const pricingContext = this.interactionPricingContext(this.requestModel(options), body);
4504
4832
  const headers = this.buildHeaders(await this.resolveKey());
4505
4833
  const recorder = new CallTimingRecorder(options.requestLifecycle);
4506
4834
  const res = await fetchWithRetry(
@@ -4522,7 +4850,7 @@ var GeminiProvider = class {
4522
4850
  if (!res.body) throw new Error("Gemini Interactions stream has no body");
4523
4851
  yield* withCallTiming(
4524
4852
  recorder,
4525
- this.parseInteractionSSEStream(res.body, res.headers, recorder)
4853
+ this.parseInteractionSSEStream(res.body, res.headers, recorder, pricingContext)
4526
4854
  );
4527
4855
  }
4528
4856
  buildInteractionRequestBody(messages, options, stream) {
@@ -4617,6 +4945,7 @@ var GeminiProvider = class {
4617
4945
  }
4618
4946
  mapInteractionInput(messages, model) {
4619
4947
  const steps = [];
4948
+ const functionNames = /* @__PURE__ */ new Map();
4620
4949
  for (const message of messages) {
4621
4950
  if (message.role === "user") {
4622
4951
  steps.push({
@@ -4626,6 +4955,10 @@ var GeminiProvider = class {
4626
4955
  } else if (message.role === "assistant") {
4627
4956
  const nativeSteps = message.providerMetadata?.geminiInteractionSteps;
4628
4957
  if (Array.isArray(nativeSteps) && nativeSteps.every(isGeminiInteractionStep)) {
4958
+ for (const step of nativeSteps) {
4959
+ if (step.type === "function_call" && typeof step.id === "string" && step.name)
4960
+ functionNames.set(step.id, step.name);
4961
+ }
4629
4962
  steps.push(...nativeSteps);
4630
4963
  continue;
4631
4964
  }
@@ -4633,6 +4966,7 @@ var GeminiProvider = class {
4633
4966
  steps.push({ type: "model_output", content: [{ type: "text", text: message.content }] });
4634
4967
  }
4635
4968
  for (const call of message.tool_calls ?? []) {
4969
+ functionNames.set(call.id, call.function.name);
4636
4970
  steps.push({
4637
4971
  type: "function_call",
4638
4972
  id: call.id,
@@ -4641,9 +4975,17 @@ var GeminiProvider = class {
4641
4975
  });
4642
4976
  }
4643
4977
  } else if (message.role === "tool") {
4978
+ const callId = message.tool_call_id ?? "";
4979
+ const name = functionNames.get(callId) ?? message.name;
4980
+ if (!name) {
4981
+ throw new InvalidModelInputError(
4982
+ `Gemini Interactions tool result for call '${callId}' has no function name: no preceding function_call carries that id and the tool message has no 'name'`
4983
+ );
4984
+ }
4644
4985
  steps.push({
4645
4986
  type: "function_result",
4646
- call_id: message.tool_call_id ?? "",
4987
+ name,
4988
+ call_id: callId,
4647
4989
  result: [
4648
4990
  { type: "text", text: typeof message.content === "string" ? message.content : "" }
4649
4991
  ]
@@ -4652,7 +4994,7 @@ var GeminiProvider = class {
4652
4994
  }
4653
4995
  return steps;
4654
4996
  }
4655
- parseInteractionResponse(json) {
4997
+ parseInteractionResponse(json, pricingContext, responseHeaders) {
4656
4998
  let content = "";
4657
4999
  let thinkingContent = "";
4658
5000
  const toolCalls = [];
@@ -4668,18 +5010,63 @@ var GeminiProvider = class {
4668
5010
  }
4669
5011
  }
4670
5012
  this.assertInteractionTerminalStatus(json.status, toolCalls.length);
4671
- const usage = normalizeInteractionUsage(json.usage);
5013
+ const normalized = normalizeInteractionUsage(json.usage);
4672
5014
  return {
4673
5015
  content,
4674
5016
  thinking_content: thinkingContent || void 0,
4675
5017
  tool_calls: toolCalls.length ? toolCalls : void 0,
4676
- usage,
4677
- // Interactions returns modality usage but no authoritative monetary cost.
4678
- cost: void 0,
5018
+ usage: normalized?.usage,
5019
+ cost: this.interactionCost({
5020
+ normalized,
5021
+ steps: json.steps,
5022
+ responseTierEvidence: geminiResponseTierEvidence(
5023
+ responseHeaders,
5024
+ json.service_tier,
5025
+ json.usage?.service_tier
5026
+ ),
5027
+ effectiveModel: typeof json.model === "string" ? json.model : pricingContext.model,
5028
+ pricingContext,
5029
+ // The whole body is in hand: every part is on `json.steps`, so there
5030
+ // is nothing this path could have failed to materialize.
5031
+ unmodeledOutput: false
5032
+ }),
4679
5033
  providerMetadata: json.steps?.length ? { geminiInteractionSteps: json.steps.filter(isGeminiInteractionStep) } : void 0
4680
5034
  };
4681
5035
  }
4682
- async *parseInteractionSSEStream(body, responseHeaders, timing) {
5036
+ /**
5037
+ * Cost for one Interactions call, or `undefined` when any precondition
5038
+ * fails. Streaming and non-streaming share this one body, so identical usage
5039
+ * always produces an identical number on both paths.
5040
+ */
5041
+ interactionCost(call) {
5042
+ const { normalized, steps, responseTierEvidence, effectiveModel, pricingContext } = call;
5043
+ if (!normalized?.pricingUsage || normalized.hasUnmodeledBilledUsage) return void 0;
5044
+ if (call.unmodeledOutput || !this.isTextOnlyInteractionOutput(steps)) return void 0;
5045
+ if (!isEligibleGeminiPricing(
5046
+ pricingContext,
5047
+ !responseTierEvidence.reportsTier || responseTierEvidence.hasDefinitiveStandardTier,
5048
+ responseTierEvidence.hasInvalidTier
5049
+ )) {
5050
+ return void 0;
5051
+ }
5052
+ return estimateGeminiCost(effectiveModel, normalized.pricingUsage);
5053
+ }
5054
+ /**
5055
+ * Whether every model-produced step is text. A non-text reply part (image,
5056
+ * audio, inline data) is billed on an output track this adapter carries no
5057
+ * rate for, so the call stays unpriced. Echoed `user_input` and
5058
+ * Axl-authored `function_result` steps are not model output and are ignored.
5059
+ */
5060
+ isTextOnlyInteractionOutput(steps) {
5061
+ return (steps ?? []).every((step) => {
5062
+ if (step.type !== "model_output" && step.type !== "thought") return true;
5063
+ const parts = [...step.content ?? [], ...step.summary ?? []];
5064
+ return parts.every(
5065
+ (part) => part !== null && typeof part === "object" && part.type === "text"
5066
+ );
5067
+ });
5068
+ }
5069
+ async *parseInteractionSSEStream(body, responseHeaders, timing, pricingContext) {
4683
5070
  const reader = body.getReader();
4684
5071
  const decoder = new TextDecoder();
4685
5072
  let buffer = "";
@@ -4687,6 +5074,8 @@ var GeminiProvider = class {
4687
5074
  const argumentBuffers = /* @__PURE__ */ new Map();
4688
5075
  const stopped = /* @__PURE__ */ new Set();
4689
5076
  let completed = false;
5077
+ const responseTierEvidence = geminiResponseTierEvidence(responseHeaders);
5078
+ let unmodeledOutput = false;
4690
5079
  try {
4691
5080
  while (true) {
4692
5081
  const { done, value } = await timing.read(reader);
@@ -4704,6 +5093,8 @@ var GeminiProvider = class {
4704
5093
  } catch {
4705
5094
  continue;
4706
5095
  }
5096
+ recordGeminiResponseTier(responseTierEvidence, event.interaction?.service_tier);
5097
+ recordGeminiResponseTier(responseTierEvidence, event.interaction?.usage?.service_tier);
4707
5098
  if (event.event_type === "error") {
4708
5099
  const status = geminiInteractionErrorStatus(event.error?.code);
4709
5100
  const message = event.error?.message ?? "Gemini Interactions stream failed";
@@ -4731,6 +5122,7 @@ var GeminiProvider = class {
4731
5122
  }
4732
5123
  const step = cloneInteractionStep(event.step);
4733
5124
  steps.set(index, step);
5125
+ if (!this.isTextOnlyInteractionOutput([step])) unmodeledOutput = true;
4734
5126
  if (step.type === "model_output") {
4735
5127
  for (const text of interactionTextChunks(step.content)) {
4736
5128
  yield { type: "text_delta", content: text };
@@ -4753,6 +5145,12 @@ var GeminiProvider = class {
4753
5145
  const step = steps.get(index);
4754
5146
  if (!step)
4755
5147
  throw new Error("Gemini Interactions stream delta arrived before step.start");
5148
+ if (step.type === "model_output" && !(delta.type === "text" && typeof delta.text === "string")) {
5149
+ unmodeledOutput = true;
5150
+ }
5151
+ if (step.type === "thought" && !(delta.type === "thought_summary" && !!delta.content) && !(delta.type === "thought_signature" && typeof delta.signature === "string")) {
5152
+ unmodeledOutput = true;
5153
+ }
4756
5154
  if (delta.type === "text" && typeof delta.text === "string" && step.type === "model_output") {
4757
5155
  appendInteractionContent(step, "content", { type: "text", text: delta.text });
4758
5156
  yield { type: "text_delta", content: delta.text };
@@ -4800,12 +5198,20 @@ var GeminiProvider = class {
4800
5198
  ).length;
4801
5199
  this.assertInteractionTerminalStatus(event.interaction?.status, toolCallCount);
4802
5200
  completed = true;
5201
+ const orderedSteps = [...steps.entries()].sort(([a], [b]) => a - b).map(([, step]) => step);
5202
+ const normalized = normalizeInteractionUsage(event.interaction?.usage);
4803
5203
  yield {
4804
5204
  type: "done",
4805
- usage: normalizeInteractionUsage(event.interaction?.usage),
4806
- providerMetadata: steps.size ? {
4807
- geminiInteractionSteps: [...steps.entries()].sort(([a], [b]) => a - b).map(([, step]) => step)
4808
- } : void 0
5205
+ usage: normalized?.usage,
5206
+ cost: this.interactionCost({
5207
+ normalized,
5208
+ steps: orderedSteps,
5209
+ responseTierEvidence,
5210
+ effectiveModel: typeof event.interaction?.model === "string" ? event.interaction.model : pricingContext.model,
5211
+ pricingContext,
5212
+ unmodeledOutput
5213
+ }),
5214
+ providerMetadata: steps.size ? { geminiInteractionSteps: orderedSteps } : void 0
4809
5215
  };
4810
5216
  return;
4811
5217
  }
@@ -4847,6 +5253,51 @@ var GeminiProvider = class {
4847
5253
  }
4848
5254
  return `Gemini API error (${status}): ${body}`;
4849
5255
  }
5256
+ /**
5257
+ * Pricing context for the stateless Interactions transport.
5258
+ *
5259
+ * The endpoint reports a per-modality input split, so a rich call IS
5260
+ * priceable — but only from the canonical endpoint, on a standard tier, with
5261
+ * a request whose billing is fully represented by the token totals.
5262
+ */
5263
+ interactionPricingContext(fallbackModel, body) {
5264
+ return {
5265
+ model: typeof body.model === "string" ? body.model : fallbackModel,
5266
+ serviceTier: body.service_tier ?? body.serviceTier,
5267
+ eligibleRequest: this.baseUrl === CANONICAL_GEMINI_BASE_URL && isStandardGeminiRequestTier(body.serviceTier) && this.isModeledInteractionRequest(body)
5268
+ };
5269
+ }
5270
+ /**
5271
+ * Whether an Interactions request body's billing is fully represented by the
5272
+ * reported token totals. `providerOptions` merges arbitrary keys into the
5273
+ * body, so anything that can move billing off the local totals — a
5274
+ * server-side cache, a server-executed tool, a residency/region override, a
5275
+ * non-text response modality — makes the call unpriced.
5276
+ *
5277
+ * Input CONTENT is deliberately not inspected: `input_tokens_by_modality` is
5278
+ * the authority on what the input billed as, and it unprices any modality
5279
+ * without a billing category (video, documents, anything new).
5280
+ */
5281
+ isModeledInteractionRequest(body) {
5282
+ if ([
5283
+ "cached_content",
5284
+ "cachedContent",
5285
+ "previous_interaction_id",
5286
+ "region",
5287
+ "inference_geo",
5288
+ "data_residency"
5289
+ ].some((key) => body[key] !== void 0)) {
5290
+ return false;
5291
+ }
5292
+ if (body.tools !== void 0 && (!Array.isArray(body.tools) || body.tools.some(
5293
+ (tool2) => tool2 === null || typeof tool2 !== "object" || tool2.type !== "function"
5294
+ ))) {
5295
+ return false;
5296
+ }
5297
+ const generationConfig = body.generation_config;
5298
+ const modalities = generationConfig !== null && typeof generationConfig === "object" ? generationConfig.response_modalities : void 0;
5299
+ return modalities === void 0 || modalities === "TEXT" || Array.isArray(modalities) && modalities.length === 1 && modalities[0] === "TEXT";
5300
+ }
4850
5301
  pricingContext(fallbackModel, body) {
4851
5302
  return {
4852
5303
  // Gemini's model is normally selected in the URL. providerOptions is an
@@ -4854,7 +5305,7 @@ var GeminiProvider = class {
4854
5305
  // the URL and pricing rather than pricing a different request.
4855
5306
  model: typeof body.model === "string" ? body.model : fallbackModel,
4856
5307
  serviceTier: body.serviceTier,
4857
- eligibleRequest: this.baseUrl === "https://generativelanguage.googleapis.com/v1beta" && this.isTextOnlyClientRequest(body)
5308
+ eligibleRequest: this.baseUrl === CANONICAL_GEMINI_BASE_URL && this.isTextOnlyClientRequest(body)
4858
5309
  };
4859
5310
  }
4860
5311
  requestModel(options) {
@@ -5177,7 +5628,7 @@ var GeminiProvider = class {
5177
5628
  }
5178
5629
  const normalized = json.usageMetadata ? normalizeGeminiUsage(json.usageMetadata) : void 0;
5179
5630
  const effectiveModel = typeof json.modelVersion === "string" ? json.modelVersion : pricingContext.model;
5180
- const cost = normalized && !normalized.hasUnmodeledBilledUsage && this.isTextOnlyGeminiResponse(candidate) && isEligibleGeminiPricing(
5631
+ const cost = normalized?.pricingUsage && !normalized.hasUnmodeledBilledUsage && this.isTextOnlyGeminiResponse(candidate) && isEligibleGeminiPricing(
5181
5632
  pricingContext,
5182
5633
  isDefinitiveStandardGeminiResponseTier(json.usageMetadata?.serviceTier),
5183
5634
  json.usageMetadata?.serviceTier !== void 0 && !isDefinitiveStandardGeminiResponseTier(json.usageMetadata.serviceTier)
@@ -5270,7 +5721,7 @@ var GeminiProvider = class {
5270
5721
  yield {
5271
5722
  type: "done",
5272
5723
  usage: normalizedUsage?.usage,
5273
- cost: normalizedUsage && !normalizedUsage.hasUnmodeledBilledUsage && responseIsTextOnly && isEligibleGeminiPricing(
5724
+ cost: normalizedUsage?.pricingUsage && !normalizedUsage.hasUnmodeledBilledUsage && responseIsTextOnly && isEligibleGeminiPricing(
5274
5725
  pricingContext,
5275
5726
  hasDefinitiveStandardResponseTier,
5276
5727
  hasInvalidResponseTier
@@ -5316,20 +5767,73 @@ function safeJsonObject(value) {
5316
5767
  return {};
5317
5768
  }
5318
5769
  }
5770
+ var KNOWN_GEMINI_MODALITIES = /* @__PURE__ */ new Set(["text", "image", "audio"]);
5771
+ function foldGeminiModalities(raw, totalInputTokens) {
5772
+ if (!Array.isArray(raw)) return void 0;
5773
+ let audioTokens = 0;
5774
+ let nonAudioTokens = 0;
5775
+ for (const entry of raw) {
5776
+ if (entry === null || typeof entry !== "object") return void 0;
5777
+ const { modality, tokens } = entry;
5778
+ if (typeof modality !== "string" || !isValidTokenCount2(tokens)) return void 0;
5779
+ const normalized = modality.toLowerCase();
5780
+ if (!KNOWN_GEMINI_MODALITIES.has(normalized)) return void 0;
5781
+ if (normalized === "audio") audioTokens += tokens;
5782
+ else nonAudioTokens += tokens;
5783
+ }
5784
+ const total = safeTokenSum2(audioTokens, nonAudioTokens);
5785
+ if (total === void 0 || total !== totalInputTokens) return void 0;
5786
+ return { audioTokens, nonAudioTokens };
5787
+ }
5788
+ function reportedGeminiAudioTokens(raw, totalInputTokens) {
5789
+ if (!Array.isArray(raw)) return void 0;
5790
+ let audioTokens;
5791
+ for (const entry of raw) {
5792
+ if (entry === null || typeof entry !== "object") return void 0;
5793
+ const { modality, tokens } = entry;
5794
+ if (typeof modality !== "string" || !isValidTokenCount2(tokens)) return void 0;
5795
+ if (modality.toLowerCase() === "audio") audioTokens = (audioTokens ?? 0) + tokens;
5796
+ }
5797
+ return audioTokens !== void 0 && audioTokens <= totalInputTokens ? audioTokens : void 0;
5798
+ }
5319
5799
  function normalizeInteractionUsage(usage) {
5320
5800
  if (!usage || !isValidTokenCount2(usage.total_input_tokens) || !isValidTokenCount2(usage.total_output_tokens) || !isValidTokenCount2(usage.total_tokens)) {
5321
5801
  return void 0;
5322
5802
  }
5323
5803
  const cached = usage.total_cached_tokens;
5324
5804
  const thinking = usage.total_thought_tokens;
5325
- if (cached !== void 0 && (!isValidTokenCount2(cached) || cached > usage.total_input_tokens) || thinking !== void 0 && !isValidTokenCount2(thinking))
5805
+ const toolUse = usage.total_tool_use_tokens;
5806
+ if (cached !== void 0 && (!isValidTokenCount2(cached) || cached > usage.total_input_tokens) || thinking !== void 0 && !isValidTokenCount2(thinking) || toolUse !== void 0 && !isValidTokenCount2(toolUse))
5326
5807
  return void 0;
5808
+ const inputTokens = usage.total_input_tokens;
5809
+ const audioInputTokens = reportedGeminiAudioTokens(usage.input_tokens_by_modality, inputTokens);
5810
+ const modalities = foldGeminiModalities(usage.input_tokens_by_modality, inputTokens);
5811
+ const billedOutputTokens = safeTokenSum2(usage.total_output_tokens, thinking ?? 0);
5812
+ const reportedTotal = safeTokenSum2(
5813
+ inputTokens,
5814
+ usage.total_output_tokens,
5815
+ thinking ?? 0,
5816
+ toolUse ?? 0
5817
+ );
5818
+ const reconciles = reportedTotal !== void 0 && usage.total_tokens === reportedTotal;
5327
5819
  return {
5328
- prompt_tokens: usage.total_input_tokens,
5329
- completion_tokens: usage.total_output_tokens,
5330
- total_tokens: usage.total_tokens,
5331
- ...cached ? { cached_tokens: cached } : {},
5332
- ...thinking ? { reasoning_tokens: thinking } : {}
5820
+ usage: {
5821
+ prompt_tokens: inputTokens,
5822
+ completion_tokens: usage.total_output_tokens,
5823
+ total_tokens: usage.total_tokens,
5824
+ ...cached ? { cached_tokens: cached } : {},
5825
+ ...thinking ? { reasoning_tokens: thinking } : {},
5826
+ ...audioInputTokens !== void 0 ? { audio_input_tokens: audioInputTokens } : {}
5827
+ },
5828
+ ...modalities !== void 0 && billedOutputTokens !== void 0 && reconciles ? {
5829
+ pricingUsage: {
5830
+ inputTokens,
5831
+ outputTokens: billedOutputTokens,
5832
+ cachedTokens: cached ?? 0,
5833
+ modalities
5834
+ }
5835
+ } : {},
5836
+ hasUnmodeledBilledUsage: toolUse !== void 0 && toolUse !== 0
5333
5837
  };
5334
5838
  }
5335
5839
 
@@ -5349,6 +5853,14 @@ var OPENROUTER_PROFILE = {
5349
5853
  capture: "reasoning_details",
5350
5854
  roundTrip: "on-tool-call-turns"
5351
5855
  },
5856
+ capabilities: {
5857
+ // Images: `image_url` data URLs or remote URLs. Audio: `input_audio`, which
5858
+ // OpenRouter documents as base64-only — bytes are encoded by the engine.
5859
+ inputModalities: {
5860
+ image: { sources: ["url", "bytes", "base64"] },
5861
+ audio: { sources: ["bytes", "base64"], formats: OPENROUTER_AUDIO_FORMATS }
5862
+ }
5863
+ },
5352
5864
  // OpenRouter normalizes to max_tokens across its backends.
5353
5865
  maxTokensField: "max_tokens"
5354
5866
  };
@@ -6844,87 +7356,98 @@ function validBase64(value) {
6844
7356
  function decodedBase64Bytes(value) {
6845
7357
  return Math.floor(value.length * 3 / 4) - (value.endsWith("==") ? 2 : value.endsWith("=") ? 1 : 0);
6846
7358
  }
7359
+ var INLINE_MEDIA_LIMIT_MESSAGE = "Inline media data must not exceed 25 MiB total; use a provider-file source, or a URL for images, where supported";
7360
+ var IMAGE_SOURCE_TYPES = ["url", "bytes", "base64", "provider-file"];
7361
+ var AUDIO_SOURCE_TYPES = ["bytes", "base64", "provider-file"];
7362
+ function cloneMediaSource(source, index, allowed, reserveInlineBytes) {
7363
+ if (!source || typeof source !== "object") invalid(`part ${index}.source must be an object`);
7364
+ const raw = source;
7365
+ const kind = raw.type;
7366
+ if (typeof kind !== "string" || !allowed.includes(kind))
7367
+ invalid(`part ${index}.source.type is unsupported`);
7368
+ let clone;
7369
+ switch (kind) {
7370
+ case "url": {
7371
+ const url = nonEmptyString2(raw.url, `part ${index}.source.url`);
7372
+ let parsed;
7373
+ try {
7374
+ parsed = new URL(url);
7375
+ } catch {
7376
+ invalid(`part ${index}.source.url must be an http(s) URL`);
7377
+ }
7378
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:")
7379
+ invalid(`part ${index}.source.url must be an http(s) URL`);
7380
+ clone = {
7381
+ type: "url",
7382
+ url,
7383
+ ...raw.mediaType === void 0 ? {} : { mediaType: mediaType(raw.mediaType, `part ${index}.source.mediaType`) }
7384
+ };
7385
+ break;
7386
+ }
7387
+ case "bytes": {
7388
+ if (!(raw.data instanceof Uint8Array) || raw.data.byteLength === 0)
7389
+ invalid(`part ${index}.source.data must be a non-empty Uint8Array`);
7390
+ reserveInlineBytes(raw.data.byteLength);
7391
+ clone = {
7392
+ type: "bytes",
7393
+ data: new Uint8Array(raw.data),
7394
+ mediaType: mediaType(raw.mediaType, `part ${index}.source.mediaType`)
7395
+ };
7396
+ break;
7397
+ }
7398
+ case "base64": {
7399
+ const data = nonEmptyString2(raw.data, `part ${index}.source.data`);
7400
+ if (data.length > 4 * Math.ceil(MAX_INLINE_MODEL_INPUT_BYTES / 3))
7401
+ invalid(INLINE_MEDIA_LIMIT_MESSAGE);
7402
+ if (!validBase64(data)) invalid(`part ${index}.source.data must be valid base64`);
7403
+ reserveInlineBytes(decodedBase64Bytes(data));
7404
+ clone = {
7405
+ type: "base64",
7406
+ data,
7407
+ mediaType: mediaType(raw.mediaType, `part ${index}.source.mediaType`)
7408
+ };
7409
+ break;
7410
+ }
7411
+ default:
7412
+ clone = {
7413
+ type: "provider-file",
7414
+ provider: nonEmptyString2(raw.provider, `part ${index}.source.provider`),
7415
+ reference: nonEmptyString2(raw.reference, `part ${index}.source.reference`),
7416
+ ...raw.mediaType === void 0 ? {} : { mediaType: mediaType(raw.mediaType, `part ${index}.source.mediaType`) }
7417
+ };
7418
+ }
7419
+ return clone;
7420
+ }
6847
7421
  function normalizeModelInput(input) {
6848
7422
  if (typeof input === "string") return input;
6849
7423
  if (!Array.isArray(input) || input.length === 0)
6850
7424
  invalid("ModelInput parts must be a non-empty array");
6851
7425
  let inlineBytes = 0;
6852
7426
  const reserveInlineBytes = (bytes) => {
6853
- if (bytes > MAX_INLINE_MODEL_INPUT_BYTES - inlineBytes) {
6854
- invalid(
6855
- "Inline image data must not exceed 25 MiB total; use a URL or provider-file source where supported"
6856
- );
6857
- }
7427
+ if (bytes > MAX_INLINE_MODEL_INPUT_BYTES - inlineBytes) invalid(INLINE_MEDIA_LIMIT_MESSAGE);
6858
7428
  inlineBytes += bytes;
6859
7429
  };
6860
7430
  return input.map((part, index) => {
6861
7431
  if (!part || typeof part !== "object") invalid(`ModelInput part ${index} must be an object`);
6862
- if (part.type === "text")
6863
- return { type: "text", text: nonEmptyString2(part.text, `part ${index}.text`) };
6864
- if (part.type !== "image") invalid(`part ${index}.type must be 'text' or 'image'`);
6865
- const source = part.source;
6866
- if (!source || typeof source !== "object") invalid(`part ${index}.source must be an object`);
6867
- let clone;
6868
- switch (source.type) {
6869
- case "url": {
6870
- const url = nonEmptyString2(source.url, `part ${index}.source.url`);
6871
- let parsed;
6872
- try {
6873
- parsed = new URL(url);
6874
- } catch {
6875
- invalid(`part ${index}.source.url must be an http(s) URL`);
6876
- }
6877
- if (parsed.protocol !== "http:" && parsed.protocol !== "https:")
6878
- invalid(`part ${index}.source.url must be an http(s) URL`);
6879
- clone = {
6880
- type: "url",
6881
- url,
6882
- ...source.mediaType === void 0 ? {} : { mediaType: mediaType(source.mediaType, `part ${index}.source.mediaType`) }
6883
- };
6884
- break;
6885
- }
6886
- case "bytes":
6887
- if (!(source.data instanceof Uint8Array) || source.data.byteLength === 0)
6888
- invalid(`part ${index}.source.data must be a non-empty Uint8Array`);
6889
- reserveInlineBytes(source.data.byteLength);
6890
- clone = {
6891
- type: "bytes",
6892
- data: source.data.slice(),
6893
- mediaType: mediaType(source.mediaType, `part ${index}.source.mediaType`)
6894
- };
6895
- break;
6896
- case "base64": {
6897
- const data = nonEmptyString2(source.data, `part ${index}.source.data`);
6898
- if (data.length > 4 * Math.ceil(MAX_INLINE_MODEL_INPUT_BYTES / 3)) {
6899
- invalid(
6900
- "Inline image data must not exceed 25 MiB total; use a URL or provider-file source where supported"
6901
- );
6902
- }
6903
- if (!validBase64(data)) invalid(`part ${index}.source.data must be valid base64`);
6904
- reserveInlineBytes(decodedBase64Bytes(data));
6905
- clone = {
6906
- type: "base64",
6907
- data,
6908
- mediaType: mediaType(source.mediaType, `part ${index}.source.mediaType`)
7432
+ const mediaLabel = () => part.label === void 0 ? {} : { label: nonEmptyString2(part.label, `part ${index}.label`) };
7433
+ switch (part.type) {
7434
+ case "text":
7435
+ return { type: "text", text: nonEmptyString2(part.text, `part ${index}.text`) };
7436
+ case "image":
7437
+ return {
7438
+ type: "image",
7439
+ source: cloneMediaSource(part.source, index, IMAGE_SOURCE_TYPES, reserveInlineBytes),
7440
+ ...mediaLabel()
6909
7441
  };
6910
- break;
6911
- }
6912
- case "provider-file":
6913
- clone = {
6914
- type: "provider-file",
6915
- provider: nonEmptyString2(source.provider, `part ${index}.source.provider`),
6916
- reference: nonEmptyString2(source.reference, `part ${index}.source.reference`),
6917
- ...source.mediaType === void 0 ? {} : { mediaType: mediaType(source.mediaType, `part ${index}.source.mediaType`) }
7442
+ case "audio":
7443
+ return {
7444
+ type: "audio",
7445
+ source: cloneMediaSource(part.source, index, AUDIO_SOURCE_TYPES, reserveInlineBytes),
7446
+ ...mediaLabel()
6918
7447
  };
6919
- break;
6920
7448
  default:
6921
- invalid(`part ${index}.source.type is unsupported`);
7449
+ invalid(`part ${index}.type must be 'text', 'image', or 'audio'`);
6922
7450
  }
6923
- return {
6924
- type: "image",
6925
- source: clone,
6926
- ...part.label === void 0 ? {} : { label: nonEmptyString2(part.label, `part ${index}.label`) }
6927
- };
6928
7451
  });
6929
7452
  }
6930
7453
  function inputText(input) {
@@ -6933,33 +7456,54 @@ function inputText(input) {
6933
7456
  function cloneModelInput(input) {
6934
7457
  return typeof input === "string" ? input : normalizeModelInput(input);
6935
7458
  }
7459
+ function unsupportedPart(part) {
7460
+ return invalid(
7461
+ `Unsupported model input part type '${String(part.type)}'`
7462
+ );
7463
+ }
7464
+ function describeSource(source, label) {
7465
+ const media = source;
7466
+ const locator = media.type === "url" ? media.url : media.type === "provider-file" ? media.reference : void 0;
7467
+ return {
7468
+ source: source.type,
7469
+ ...media.mediaType ? { mediaType: media.mediaType } : {},
7470
+ ...media.type === "bytes" ? { bytes: media.data.byteLength } : {},
7471
+ ...media.type === "base64" ? { bytes: decodedBase64Bytes(media.data) } : {},
7472
+ ...locator ? { locator } : {},
7473
+ ...label ? { label } : {}
7474
+ };
7475
+ }
6936
7476
  function describeModelInput(input) {
6937
7477
  if (typeof input === "string") return void 0;
6938
7478
  return {
6939
7479
  parts: input.map((part) => {
6940
- if (part.type === "text") return { type: "text", characters: part.text.length };
6941
- const { source } = part;
6942
- return {
6943
- type: "image",
6944
- source: source.type,
6945
- ...source.mediaType ? { mediaType: source.mediaType } : {},
6946
- ...source.type === "bytes" ? { bytes: source.data.byteLength } : {},
6947
- ...source.type === "base64" ? {
6948
- bytes: decodedBase64Bytes(source.data)
6949
- } : {},
6950
- ...(source.type === "url" ? source.url : source.type === "provider-file" ? source.reference : void 0) ? {
6951
- locator: source.type === "url" ? source.url : source.type === "provider-file" ? source.reference : void 0
6952
- } : {},
6953
- ...part.label ? { label: part.label } : {}
6954
- };
7480
+ switch (part.type) {
7481
+ case "text":
7482
+ return { type: "text", characters: part.text.length };
7483
+ case "image":
7484
+ return { type: "image", ...describeSource(part.source, part.label) };
7485
+ case "audio":
7486
+ return { type: "audio", ...describeSource(part.source, part.label) };
7487
+ default:
7488
+ return unsupportedPart(part);
7489
+ }
6955
7490
  })
6956
7491
  };
6957
7492
  }
6958
7493
  function summarizeModelInput(input) {
6959
7494
  if (typeof input === "string") return input;
6960
- return input.map(
6961
- (part) => part.type === "text" ? part.text : `[image ${part.source.mediaType ?? "media"}]`
6962
- ).join("\n");
7495
+ return input.map((part) => {
7496
+ switch (part.type) {
7497
+ case "text":
7498
+ return part.text;
7499
+ case "image":
7500
+ return `[image ${part.source.mediaType ?? "media"}]`;
7501
+ case "audio":
7502
+ return `[audio ${part.source.mediaType ?? "media"}]`;
7503
+ default:
7504
+ return unsupportedPart(part);
7505
+ }
7506
+ }).join("\n");
6963
7507
  }
6964
7508
  function sanitizeModelInputForTrace(input) {
6965
7509
  return inputText(input);
@@ -6973,15 +7517,86 @@ function normalizePersistedSessionHistory(history) {
6973
7517
  );
6974
7518
  }
6975
7519
  const content = normalizeModelInput(message.content);
6976
- if (typeof content !== "string" && content.some((part) => part.type === "image" && part.source.type === "bytes")) {
7520
+ if (typeof content !== "string" && content.some((part) => part.type !== "text" && part.source.type === "bytes")) {
6977
7521
  throw new InvalidModelInputError(
6978
- "Uint8Array image input cannot be persisted in session history"
7522
+ "Uint8Array media input cannot be persisted in session history"
6979
7523
  );
6980
7524
  }
6981
7525
  return { ...message, content };
6982
7526
  });
6983
7527
  }
6984
7528
 
7529
+ // src/session-input.ts
7530
+ var MODEL_INPUT_PART_TYPES = /* @__PURE__ */ new Set(["text", "image", "audio"]);
7531
+ var sessionInputMarkers = /* @__PURE__ */ new WeakMap();
7532
+ function isModelInputParts(input) {
7533
+ return Array.isArray(input) && input.length > 0 && input.every(
7534
+ (part) => typeof part === "object" && part !== null && MODEL_INPUT_PART_TYPES.has(part.type)
7535
+ );
7536
+ }
7537
+ function prepareSessionInput(input) {
7538
+ if (typeof input === "string") return { content: input, normalizedInput: input };
7539
+ if (isModelInputParts(input)) {
7540
+ const normalizedInput = normalizeModelInput(input);
7541
+ return { content: summarizeModelInput(normalizedInput), normalizedInput };
7542
+ }
7543
+ const content = JSON.stringify(input);
7544
+ return {
7545
+ content,
7546
+ ...typeof content === "string" ? { normalizedInput: content } : {}
7547
+ };
7548
+ }
7549
+ function registerSessionInput(history, message, normalizedInput) {
7550
+ sessionInputMarkers.set(history, {
7551
+ message,
7552
+ content: message.content,
7553
+ normalizedInput: cloneModelInput(normalizedInput)
7554
+ });
7555
+ }
7556
+ function clearSessionInput(history) {
7557
+ sessionInputMarkers.delete(history);
7558
+ }
7559
+ function equalBytes(left, right) {
7560
+ if (left.byteLength !== right.byteLength) return false;
7561
+ for (let index = 0; index < left.byteLength; index++) {
7562
+ if (left[index] !== right[index]) return false;
7563
+ }
7564
+ return true;
7565
+ }
7566
+ function equalSource(left, right) {
7567
+ if (left.type !== right.type) return false;
7568
+ switch (left.type) {
7569
+ case "url":
7570
+ return right.type === "url" && left.url === right.url && left.mediaType === right.mediaType;
7571
+ case "bytes":
7572
+ return right.type === "bytes" && left.mediaType === right.mediaType && equalBytes(left.data, right.data);
7573
+ case "base64":
7574
+ return right.type === "base64" && left.data === right.data && left.mediaType === right.mediaType;
7575
+ case "provider-file":
7576
+ return right.type === "provider-file" && left.provider === right.provider && left.reference === right.reference && left.mediaType === right.mediaType;
7577
+ }
7578
+ }
7579
+ function equalPart(left, right) {
7580
+ if (left.type !== right.type) return false;
7581
+ if (left.type === "text") return right.type === "text" && left.text === right.text;
7582
+ return right.type === left.type && left.label === right.label && equalSource(left.source, right.source);
7583
+ }
7584
+ function equalModelInput(left, right) {
7585
+ if (typeof left === "string" || typeof right === "string") return left === right;
7586
+ return left.length === right.length && left.every((part, index) => equalPart(part, right[index]));
7587
+ }
7588
+ function sessionHistoryForAsk(history, normalizedInput) {
7589
+ const snapshot = history.slice();
7590
+ const marker = sessionInputMarkers.get(history);
7591
+ if (!marker) return snapshot;
7592
+ const last = snapshot[snapshot.length - 1];
7593
+ if (last !== marker.message || last.role !== "user" || last.content !== marker.content || !equalModelInput(marker.normalizedInput, normalizedInput)) {
7594
+ return snapshot;
7595
+ }
7596
+ snapshot.pop();
7597
+ return snapshot;
7598
+ }
7599
+
6985
7600
  // src/partial-json.ts
6986
7601
  var MAX_DEPTH = 256;
6987
7602
  function parsePartialJson(input) {
@@ -9780,7 +10395,7 @@ function estimateMessagesTokens(messages) {
9780
10395
  for (const msg of messages) {
9781
10396
  total += estimateTokens(inputText(msg.content));
9782
10397
  if (typeof msg.content !== "string") {
9783
- unmeasured ||= msg.content.some((part) => part.type === "image");
10398
+ unmeasured ||= msg.content.some((part) => part.type !== "text");
9784
10399
  }
9785
10400
  if (msg.tool_calls) {
9786
10401
  for (const tc of msg.tool_calls) {
@@ -9826,10 +10441,6 @@ function normalizeSessionHistory(messages) {
9826
10441
  function hasRichMessage(messages) {
9827
10442
  return messages.some((message) => typeof message.content !== "string");
9828
10443
  }
9829
- function firstImageSource(input, history) {
9830
- const find = (value) => typeof value === "string" ? void 0 : value.find((part) => part.type === "image")?.source.type;
9831
- return find(input) ?? history.map((message) => find(message.content)).find(Boolean);
9832
- }
9833
10444
  function appendHandoffInstruction(input, originalText, instruction) {
9834
10445
  if (typeof input === "string" || instruction.length === 0 || instruction === originalText)
9835
10446
  return input;
@@ -10387,7 +10998,8 @@ var WorkflowContext = class _WorkflowContext {
10387
10998
  // ── ctx.ask() ─────────────────────────────────────────────────────────
10388
10999
  async ask(agent2, prompt, options) {
10389
11000
  const normalizedInput = normalizeModelInput(prompt);
10390
- const normalizedHistory = normalizeSessionHistory(this.sessionHistory);
11001
+ const sessionHistorySnapshot = sessionHistoryForAsk(this.sessionHistory, normalizedInput);
11002
+ const normalizedHistory = normalizeSessionHistory(sessionHistorySnapshot);
10391
11003
  const delegateInputHolder = agent2;
10392
11004
  if (delegateInputHolder._delegateOriginalInput !== void 0) {
10393
11005
  delegateInputHolder._delegateOriginalInput = normalizeModelInput(
@@ -10597,20 +11209,31 @@ var WorkflowContext = class _WorkflowContext {
10597
11209
  const providerOptions = options?.providerOptions ?? agent2._config.providerOptions;
10598
11210
  const richRequest = typeof input !== "string" || hasRichMessage(sessionHistory);
10599
11211
  if (richRequest) {
11212
+ const richPart = firstRichPart(input, sessionHistory);
11213
+ const modality = richPart?.type ?? "image";
11214
+ const source = richPart?.source.type;
10600
11215
  if (providerOptions && ("messages" in providerOptions || "input" in providerOptions)) {
10601
11216
  throw new UnsupportedModelInputError({
10602
11217
  provider: provider.name ?? modelUri.split(":", 1)[0],
10603
11218
  model,
10604
- modality: "image",
11219
+ modality,
10605
11220
  feature: "raw input-container providerOptions"
10606
11221
  });
10607
11222
  }
10608
- const source = firstImageSource(input, sessionHistory);
11223
+ const audioPart = firstRichPart(input, sessionHistory, "audio");
11224
+ if (audioPart && !provider.inputCapabilities?.(model)?.audio) {
11225
+ throw new UnsupportedModelInputError({
11226
+ provider: provider.name ?? modelUri.split(":", 1)[0],
11227
+ model,
11228
+ modality: "audio",
11229
+ source: audioPart.source.type
11230
+ });
11231
+ }
10609
11232
  if (!provider.validateInput) {
10610
11233
  throw new UnsupportedModelInputError({
10611
11234
  provider: provider.name ?? modelUri.split(":", 1)[0],
10612
11235
  model,
10613
- modality: "image",
11236
+ modality,
10614
11237
  ...source ? { source } : {}
10615
11238
  });
10616
11239
  }
@@ -10629,10 +11252,20 @@ var WorkflowContext = class _WorkflowContext {
10629
11252
  throw new UnsupportedModelInputError({
10630
11253
  provider: provider.name ?? modelUri.split(":", 1)[0],
10631
11254
  model,
10632
- modality: "image",
11255
+ modality,
10633
11256
  ...source ? { source } : {}
10634
11257
  });
10635
11258
  }
11259
+ if (audioPart && validation.effectiveModel !== model) {
11260
+ if (!provider.inputCapabilities?.(validation.effectiveModel)?.audio) {
11261
+ throw new UnsupportedModelInputError({
11262
+ provider: provider.name ?? modelUri.split(":", 1)[0],
11263
+ model: validation.effectiveModel,
11264
+ modality: "audio",
11265
+ source: audioPart.source.type
11266
+ });
11267
+ }
11268
+ }
10636
11269
  model = validation.effectiveModel;
10637
11270
  }
10638
11271
  const effectiveModelUri = !richRequest ? modelUri : `${modelUri.slice(0, modelUri.indexOf(":") + 1)}${model}`;
@@ -10645,25 +11278,17 @@ var WorkflowContext = class _WorkflowContext {
10645
11278
  (descriptor) => descriptor !== void 0
10646
11279
  );
10647
11280
  const parts = descriptors.flatMap((descriptor) => descriptor.parts);
11281
+ const media = parts.filter((part) => part.type !== "text");
11282
+ const withSource = (source) => media.filter((part) => part.source === source).length;
10648
11283
  this.spanManager?.addEventToActiveSpan("axl.model_input", {
10649
11284
  "axl.input.parts": parts.length,
10650
11285
  "axl.input.images": parts.filter((part) => part.type === "image").length,
10651
- "axl.input.source.bytes": parts.filter(
10652
- (part) => part.type === "image" && part.source === "bytes"
10653
- ).length,
10654
- "axl.input.source.base64": parts.filter(
10655
- (part) => part.type === "image" && part.source === "base64"
10656
- ).length,
10657
- "axl.input.source.url": parts.filter(
10658
- (part) => part.type === "image" && part.source === "url"
10659
- ).length,
10660
- "axl.input.source.provider_file": parts.filter(
10661
- (part) => part.type === "image" && part.source === "provider-file"
10662
- ).length,
10663
- "axl.input.inline_bytes": parts.reduce(
10664
- (sum, part) => sum + (part.type === "image" ? part.bytes ?? 0 : 0),
10665
- 0
10666
- )
11286
+ "axl.input.audio": parts.filter((part) => part.type === "audio").length,
11287
+ "axl.input.source.bytes": withSource("bytes"),
11288
+ "axl.input.source.base64": withSource("base64"),
11289
+ "axl.input.source.url": withSource("url"),
11290
+ "axl.input.source.provider_file": withSource("provider-file"),
11291
+ "axl.input.inline_bytes": media.reduce((sum, part) => sum + (part.bytes ?? 0), 0)
10667
11292
  });
10668
11293
  }
10669
11294
  assertRichMessagesAreUserOnly(sessionHistory);
@@ -13230,6 +13855,11 @@ ${summaryResponse.content}`
13230
13855
  }
13231
13856
  });
13232
13857
  const routerInput = options?.routerInput === "text" ? inputText(prompt) : prompt;
13858
+ if (routerInput === "") {
13859
+ throw new InvalidModelInputError(
13860
+ "delegate routerInput 'text' requires at least one text part; the input is media-only \u2014 route on the full input or add a text part"
13861
+ );
13862
+ }
13233
13863
  routerAgent._delegateOriginalInput = prompt;
13234
13864
  return this.ask(routerAgent, routerInput, {
13235
13865
  schema: options?.schema,
@@ -14326,9 +14956,13 @@ var Session = class _Session {
14326
14956
  }
14327
14957
  async sendImpl(workflowName, input, signal, events) {
14328
14958
  const { history, metadata } = await this.prepareHistory(input);
14329
- const result = await this.runtime.execute(workflowName, input, { metadata, signal, events });
14330
- await this.commitHistory(history, result);
14331
- return result;
14959
+ try {
14960
+ const result = await this.runtime.execute(workflowName, input, { metadata, signal, events });
14961
+ await this.commitHistory(history, result);
14962
+ return result;
14963
+ } finally {
14964
+ clearSessionInput(history);
14965
+ }
14332
14966
  }
14333
14967
  /** Read history + summary, apply maxMessages limit (with optional
14334
14968
  * summarization), and push the new user message. Returns the mutable
@@ -14338,6 +14972,7 @@ var Session = class _Session {
14338
14972
  * `streamImpl` so future fixes (e.g., to summarization wiring) land in
14339
14973
  * one place. */
14340
14974
  async prepareHistory(input) {
14975
+ const preparedInput = prepareSessionInput(input);
14341
14976
  const history = await this.store.getSession(this.sessionId);
14342
14977
  let cachedSummary = await this.store.getSessionMeta(this.sessionId, "summaryCache");
14343
14978
  const maxMessages = this.options.history?.maxMessages;
@@ -14360,10 +14995,11 @@ var Session = class _Session {
14360
14995
  history.length = 0;
14361
14996
  history.push(...trimmed);
14362
14997
  }
14363
- history.push({
14364
- role: "user",
14365
- content: typeof input === "string" ? input : JSON.stringify(input)
14366
- });
14998
+ const userMessage = { role: "user", content: preparedInput.content };
14999
+ history.push(userMessage);
15000
+ if (this.options.deduplicateInput !== false && preparedInput.normalizedInput !== void 0) {
15001
+ registerSessionInput(history, userMessage, preparedInput.normalizedInput);
15002
+ }
14367
15003
  const metadata = {
14368
15004
  sessionId: this.sessionId,
14369
15005
  sessionHistory: history,
@@ -14422,21 +15058,26 @@ var Session = class _Session {
14422
15058
  events
14423
15059
  });
14424
15060
  } catch (err) {
15061
+ if (history) clearSessionInput(history);
14425
15062
  rejectReady(err);
14426
15063
  throw err;
14427
15064
  }
14428
15065
  resolveReady(axlStream);
14429
- const completion = axlStream.promise.then(
14430
- (result) => this.commitHistory(history, result).catch((err) => {
14431
- this.runtime.emit("error", {
14432
- type: "session_history_save_failed",
14433
- sessionId: this.sessionId,
14434
- error: err instanceof Error ? err.message : String(err)
14435
- });
14436
- }),
14437
- () => void 0
14438
- );
14439
- await completion;
15066
+ try {
15067
+ const completion = axlStream.promise.then(
15068
+ (result) => this.commitHistory(history, result).catch((err) => {
15069
+ this.runtime.emit("error", {
15070
+ type: "session_history_save_failed",
15071
+ sessionId: this.sessionId,
15072
+ error: err instanceof Error ? err.message : String(err)
15073
+ });
15074
+ }),
15075
+ () => void 0
15076
+ );
15077
+ await completion;
15078
+ } finally {
15079
+ clearSessionInput(history);
15080
+ }
14440
15081
  }
14441
15082
  async history() {
14442
15083
  return this.store.getSession(this.sessionId);
@@ -18050,6 +18691,7 @@ export {
18050
18691
  resolveRuntime,
18051
18692
  resolveThinkingOptions,
18052
18693
  stringStreamFromEvents,
18694
+ summarizeModelInput,
18053
18695
  tool,
18054
18696
  workflow,
18055
18697
  zodToJsonSchema