@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.cjs CHANGED
@@ -219,6 +219,7 @@ __export(index_exports, {
219
219
  resolveRuntime: () => resolveRuntime,
220
220
  resolveThinkingOptions: () => resolveThinkingOptions,
221
221
  stringStreamFromEvents: () => stringStreamFromEvents,
222
+ summarizeModelInput: () => summarizeModelInput,
222
223
  tool: () => tool,
223
224
  workflow: () => workflow,
224
225
  zodToJsonSchema: () => zodToJsonSchema
@@ -1416,38 +1417,112 @@ function unsafeTransportError(surface, remediation, exposedData) {
1416
1417
  );
1417
1418
  }
1418
1419
 
1420
+ // src/providers/rich-input.ts
1421
+ function richInputParts(input) {
1422
+ return typeof input === "string" ? [] : input.filter((part) => part.type !== "text");
1423
+ }
1424
+ function firstRichPart(input, history, modality) {
1425
+ const find = (value) => richInputParts(value).find((part) => modality === void 0 || part.type === modality);
1426
+ return find(input) ?? history.map((message) => find(message.content)).find((part) => part !== void 0);
1427
+ }
1428
+
1429
+ // src/providers/audio-format.ts
1430
+ var OPENAI_CHAT_AUDIO_FORMATS = Object.freeze({
1431
+ "audio/wav": "wav",
1432
+ "audio/x-wav": "wav",
1433
+ "audio/wave": "wav",
1434
+ "audio/mpeg": "mp3",
1435
+ "audio/mp3": "mp3"
1436
+ });
1437
+ var OPENROUTER_AUDIO_FORMATS = Object.freeze({
1438
+ ...OPENAI_CHAT_AUDIO_FORMATS,
1439
+ "audio/aiff": "aiff",
1440
+ "audio/x-aiff": "aiff",
1441
+ "audio/aac": "aac",
1442
+ "audio/ogg": "ogg",
1443
+ "audio/flac": "flac",
1444
+ "audio/x-flac": "flac",
1445
+ "audio/m4a": "m4a",
1446
+ "audio/x-m4a": "m4a",
1447
+ "audio/mp4": "m4a",
1448
+ "audio/l16": "pcm16"
1449
+ });
1450
+ function resolveAudioFormat(formats, mediaType2) {
1451
+ if (typeof mediaType2 !== "string") return void 0;
1452
+ const essence = mediaType2.split(";", 1)[0].trim().toLowerCase();
1453
+ return Object.prototype.hasOwnProperty.call(formats, essence) ? formats[essence] : void 0;
1454
+ }
1455
+
1419
1456
  // src/providers/openai-compatible.ts
1420
- function compatibleImageBase64(source) {
1457
+ function unmappableMediaTypeFeature(mediaType2) {
1458
+ return mediaType2 === void 0 ? "audio media type (missing)" : `audio media type '${mediaType2}'`;
1459
+ }
1460
+ function compatibleInlineBase64(source) {
1421
1461
  return source.type === "base64" ? source.data : Buffer.from(source.data.buffer, source.data.byteOffset, source.data.byteLength).toString(
1422
1462
  "base64"
1423
1463
  );
1424
1464
  }
1425
- function openRouterImageParts(parts, model) {
1465
+ function compatibleRichParts(parts, model, modalities, provider) {
1466
+ const reject = (modality, source, feature) => new UnsupportedModelInputError({
1467
+ provider,
1468
+ model,
1469
+ modality,
1470
+ source,
1471
+ ...feature ? { feature } : {}
1472
+ });
1426
1473
  const content = [];
1427
1474
  for (const part of parts) {
1428
1475
  if (part.type === "text") {
1429
1476
  content.push({ type: "text", text: part.text });
1430
1477
  continue;
1431
1478
  }
1432
- const { source } = part;
1433
- if (source.type === "provider-file") {
1479
+ if (part.type === "audio") {
1480
+ const { source: source2 } = part;
1481
+ const audio = modalities.audio;
1482
+ if (!audio) throw reject("audio", source2.type, "audio input");
1483
+ if (source2.type === "provider-file" || !audio.sources.includes(source2.type)) {
1484
+ throw reject("audio", source2.type);
1485
+ }
1486
+ const format = resolveAudioFormat(audio.formats, source2.mediaType);
1487
+ if (format === void 0) {
1488
+ throw reject("audio", source2.type, unmappableMediaTypeFeature(source2.mediaType));
1489
+ }
1490
+ content.push({
1491
+ type: "input_audio",
1492
+ // Caller base64 passes through verbatim — never decoded and re-encoded.
1493
+ input_audio: { data: compatibleInlineBase64(source2), format }
1494
+ });
1495
+ if (part.label) content.push({ type: "text", text: `[Audio: ${part.label}]` });
1496
+ continue;
1497
+ }
1498
+ if (part.type !== "image") {
1499
+ const unmapped = part;
1434
1500
  throw new UnsupportedModelInputError({
1435
- provider: "openrouter",
1501
+ provider,
1436
1502
  model,
1437
- modality: "image",
1438
- source: "provider-file"
1503
+ modality: unmapped.type,
1504
+ feature: "this input modality"
1439
1505
  });
1440
1506
  }
1507
+ const { source } = part;
1508
+ const image = modalities.image;
1509
+ if (!image) throw reject("image", source.type, "image input for this model");
1510
+ if (source.type === "provider-file" || !image.sources.includes(source.type)) {
1511
+ throw reject("image", source.type);
1512
+ }
1441
1513
  content.push({
1442
1514
  type: "image_url",
1443
1515
  image_url: {
1444
- url: source.type === "url" ? source.url : `data:${source.mediaType};base64,${compatibleImageBase64(source)}`
1516
+ url: source.type === "url" ? source.url : `data:${source.mediaType};base64,${compatibleInlineBase64(source)}`
1445
1517
  }
1446
1518
  });
1447
1519
  if (part.label) content.push({ type: "text", text: `[Image: ${part.label}]` });
1448
1520
  }
1449
1521
  return content;
1450
1522
  }
1523
+ function requestAudioParts(input, history) {
1524
+ return [input, ...history.map((message) => message.content)].flatMap((content) => richInputParts(content)).filter((part) => part.type === "audio");
1525
+ }
1451
1526
  function resolvePerModel(value, model, fallback) {
1452
1527
  if (value === void 0) return fallback;
1453
1528
  return typeof value === "function" ? value(model) : value;
@@ -1461,6 +1536,9 @@ function sortedKeys(table) {
1461
1536
  }
1462
1537
  return keys;
1463
1538
  }
1539
+ function reportedTokenCount(value) {
1540
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : void 0;
1541
+ }
1464
1542
  function priceFromTable(table, model, promptTokens, completionTokens, cachedTokens, match = "prefix") {
1465
1543
  const cached = cachedTokens ?? 0;
1466
1544
  if (!Number.isSafeInteger(promptTokens) || !Number.isSafeInteger(completionTokens) || !Number.isSafeInteger(cached) || promptTokens < 0 || completionTokens < 0 || cached < 0 || cached > promptTokens) {
@@ -1564,26 +1642,47 @@ function normalizeMessageContent(content) {
1564
1642
  var OpenAICompatibleProvider = class {
1565
1643
  name;
1566
1644
  reportsRequestLifecycle = true;
1645
+ /**
1646
+ * Modalities the effective profile declares for this model. A blank model
1647
+ * declares nothing, so a rich request against one fails validation exactly
1648
+ * as it did before any modality existed.
1649
+ */
1650
+ inputModalities(model) {
1651
+ return model.trim().length > 0 ? this.profile.capabilities?.inputModalities ?? {} : {};
1652
+ }
1567
1653
  inputCapabilities(model) {
1568
- return this.name === "openrouter" && model.trim().length > 0 ? { image: { sources: ["url", "bytes", "base64"] } } : {};
1654
+ const { image, audio } = this.inputModalities(model);
1655
+ return {
1656
+ ...image ? { image: { sources: image.sources } } : {},
1657
+ // `formats` is engine-internal wire detail; the runtime contract is the
1658
+ // accepted source kinds only.
1659
+ ...audio ? { audio: { sources: audio.sources } } : {}
1660
+ };
1569
1661
  }
1570
1662
  validateInput(request) {
1571
1663
  const modelOverride = request.providerOptions?.model;
1572
1664
  const effectiveModel = typeof modelOverride === "string" ? modelOverride : request.model;
1573
- const fail = (source, feature) => {
1665
+ const failWith = (modality, source, feature) => {
1574
1666
  throw new UnsupportedModelInputError({
1575
1667
  provider: this.name,
1576
1668
  model: effectiveModel || request.model,
1577
- modality: "image",
1669
+ modality,
1578
1670
  ...source ? { source } : {},
1579
1671
  ...feature ? { feature } : {}
1580
1672
  });
1581
1673
  };
1674
+ const fail = (source, feature) => failWith(firstRichPart(request.input, request.history)?.type ?? "image", source, feature);
1675
+ const modalities = this.inputModalities(effectiveModel);
1676
+ const audioPart = firstRichPart(request.input, request.history, "audio");
1677
+ if (audioPart && !modalities.audio) {
1678
+ failWith("audio", audioPart.source.type, "audio input");
1679
+ }
1582
1680
  if (request.providerOptions && "model" in request.providerOptions && (typeof modelOverride !== "string" || modelOverride.trim().length === 0)) {
1583
1681
  fail(void 0, "invalid model providerOptions");
1584
1682
  }
1585
- if (this.name !== "openrouter" || effectiveModel.trim().length === 0) {
1586
- fail(void 0, "image input for this model");
1683
+ const imagePart = firstRichPart(request.input, request.history, "image");
1684
+ if (!modalities.image && (!audioPart || imagePart)) {
1685
+ failWith("image", void 0, "image input for this model");
1587
1686
  }
1588
1687
  if (request.providerOptions && "messages" in request.providerOptions) {
1589
1688
  fail(void 0, "raw messages providerOptions");
@@ -1594,11 +1693,21 @@ var OpenAICompatibleProvider = class {
1594
1693
  if (message.content.some(
1595
1694
  (part) => part.type === "image" && part.source.type === "provider-file"
1596
1695
  )) {
1597
- fail("provider-file");
1696
+ failWith("image", "provider-file");
1598
1697
  }
1599
1698
  }
1600
1699
  if (Array.isArray(request.input) && request.input.some((part) => part.type === "image" && part.source.type === "provider-file")) {
1601
- fail("provider-file");
1700
+ failWith("image", "provider-file");
1701
+ }
1702
+ const audio = modalities.audio;
1703
+ if (audio) {
1704
+ for (const part of requestAudioParts(request.input, request.history)) {
1705
+ const source = part.source;
1706
+ if (!audio.sources.includes(source.type)) failWith("audio", source.type);
1707
+ if (resolveAudioFormat(audio.formats, source.mediaType) === void 0) {
1708
+ failWith("audio", source.type, unmappableMediaTypeFeature(source.mediaType));
1709
+ }
1710
+ }
1602
1711
  }
1603
1712
  return { effectiveModel };
1604
1713
  }
@@ -1783,9 +1892,15 @@ var OpenAICompatibleProvider = class {
1783
1892
  return body;
1784
1893
  }
1785
1894
  formatMessage(msg, model) {
1895
+ const modalities = this.inputModalities(model);
1786
1896
  const out = {
1787
1897
  role: this.profile.roleFor ? this.profile.roleFor(msg.role, model) : msg.role,
1788
- content: Array.isArray(msg.content) && this.name === "openrouter" ? openRouterImageParts(msg.content, model) : msg.content
1898
+ // History user turns go through the same builder as the caller's input,
1899
+ // so a rich part is reproduced identically in every continuation request.
1900
+ // Array content is routed UNCONDITIONALLY: a profile that declares no
1901
+ // modality has the builder reject every rich part (and pass text through)
1902
+ // rather than emitting raw `InputContentPart` objects on the wire.
1903
+ content: Array.isArray(msg.content) ? compatibleRichParts(msg.content, model, modalities, this.name) : msg.content
1789
1904
  };
1790
1905
  if (msg.name && (this.profile.capabilities?.emitsMessageName ?? true)) out.name = msg.name;
1791
1906
  if (msg.tool_calls) out.tool_calls = msg.tool_calls;
@@ -1816,6 +1931,8 @@ var OpenAICompatibleProvider = class {
1816
1931
  }
1817
1932
  toUsage(raw) {
1818
1933
  if (!raw) return void 0;
1934
+ const audioInput = reportedTokenCount(raw.prompt_tokens_details?.audio_tokens);
1935
+ const audioOutput = reportedTokenCount(raw.completion_tokens_details?.audio_tokens);
1819
1936
  return {
1820
1937
  prompt_tokens: raw.prompt_tokens,
1821
1938
  completion_tokens: raw.completion_tokens,
@@ -1825,7 +1942,9 @@ var OpenAICompatibleProvider = class {
1825
1942
  // prompt_tokens_details. Preserve the normalized hit count for the
1826
1943
  // generic table estimator; its split is validated before pricing.
1827
1944
  cached_tokens: raw.prompt_cache_hit_tokens ?? raw.prompt_tokens_details?.cached_tokens,
1828
- cache_write_tokens: raw.prompt_tokens_details?.cache_write_tokens
1945
+ cache_write_tokens: raw.prompt_tokens_details?.cache_write_tokens,
1946
+ ...audioInput !== void 0 ? { audio_input_tokens: audioInput } : {},
1947
+ ...audioOutput !== void 0 ? { audio_output_tokens: audioOutput } : {}
1829
1948
  };
1830
1949
  }
1831
1950
  reportedCost(raw) {
@@ -1851,6 +1970,9 @@ var OpenAICompatibleProvider = class {
1851
1970
  case "from-response":
1852
1971
  return typeof reportedCost === "number" && Number.isFinite(reportedCost) && reportedCost >= 0 ? reportedCost : void 0;
1853
1972
  case "table": {
1973
+ if (typeof usage.audio_input_tokens === "number" && usage.audio_input_tokens > 0 || typeof usage.audio_output_tokens === "number" && usage.audio_output_tokens > 0) {
1974
+ return void 0;
1975
+ }
1854
1976
  if (!isBuiltinTablePricingEligible(this.profile, {
1855
1977
  baseUrl: this.baseUrl,
1856
1978
  request: context?.request,
@@ -2198,6 +2320,26 @@ var DIRECT_OPENAI_CATALOG = [
2198
2320
  }),
2199
2321
  contextBoundary: LONG_CONTEXT_BOUNDARY
2200
2322
  },
2323
+ // Audio-capable models. Audio rates reviewed 2026-09-08 against
2324
+ // https://developers.openai.com/api/docs/pricing (no version stamp on the
2325
+ // page): `gpt-audio-1.5` and `gpt-audio` both list Text $2.50 in / $10.00
2326
+ // out and Audio $32.00 in / $64.00 out per 1M tokens, with no cached-input
2327
+ // row — hence no `cachedInput` here, which makes a cache-hit report unpriced
2328
+ // rather than silently billed at the full input rate. Only these exact ids
2329
+ // are listed: no snapshot id is published for either, and inventing one
2330
+ // would price an unknown model.
2331
+ {
2332
+ aliases: ["gpt-audio-1.5"],
2333
+ snapshotBase: "gpt-audio-1.5",
2334
+ short: { input: 2.5 / M, output: 10 / M },
2335
+ audio: { input: 32 / M, output: 64 / M }
2336
+ },
2337
+ {
2338
+ aliases: ["gpt-audio"],
2339
+ snapshotBase: "gpt-audio",
2340
+ short: { input: 2.5 / M, output: 10 / M },
2341
+ audio: { input: 32 / M, output: 64 / M }
2342
+ },
2201
2343
  // Existing direct models retain literal current Standard rows. These flat
2202
2344
  // rows keep native pricing aligned with the compatibility wrapper while the
2203
2345
  // private estimator still validates categories and billing mode strictly.
@@ -2348,32 +2490,45 @@ var CANONICAL_OPENAI_BASE_URL = "https://api.openai.com/v1";
2348
2490
  function isTextContentPart(value, type) {
2349
2491
  return value !== null && typeof value === "object" && value.type === type && typeof value.text === "string";
2350
2492
  }
2351
- function hasUnmodeledDirectOpenAIContent(request) {
2493
+ function isChatAudioContentPart(value) {
2494
+ if (value === null || typeof value !== "object") return false;
2495
+ const part = value;
2496
+ return part.type === "input_audio" && part.input_audio !== null && typeof part.input_audio === "object";
2497
+ }
2498
+ function classifyDirectOpenAIContent(request) {
2499
+ if (!request) return "text";
2352
2500
  if ("messages" in request) {
2353
- if (!Array.isArray(request.messages)) return true;
2501
+ if (!Array.isArray(request.messages)) return "unmodeled";
2502
+ let carriesAudio = false;
2354
2503
  for (const message of request.messages) {
2355
- if (message === null || typeof message !== "object") return true;
2504
+ if (message === null || typeof message !== "object") return "unmodeled";
2356
2505
  const content = message.content;
2357
2506
  if (content === void 0 || content === null || typeof content === "string") continue;
2358
- if (!Array.isArray(content) || content.some((part) => !isTextContentPart(part, "text"))) {
2359
- return true;
2507
+ if (!Array.isArray(content)) return "unmodeled";
2508
+ for (const part of content) {
2509
+ if (isTextContentPart(part, "text")) continue;
2510
+ if (isChatAudioContentPart(part)) {
2511
+ carriesAudio = true;
2512
+ continue;
2513
+ }
2514
+ return "unmodeled";
2360
2515
  }
2361
2516
  }
2362
- return false;
2517
+ return carriesAudio ? "audio" : "text";
2363
2518
  }
2364
2519
  if ("input" in request) {
2365
2520
  const input = request.input;
2366
- if (typeof input === "string") return false;
2367
- if (!Array.isArray(input)) return true;
2521
+ if (typeof input === "string") return "text";
2522
+ if (!Array.isArray(input)) return "unmodeled";
2368
2523
  for (const item of input) {
2369
- if (item === null || typeof item !== "object") return true;
2524
+ if (item === null || typeof item !== "object") return "unmodeled";
2370
2525
  const typed = item;
2371
2526
  switch (typed.type) {
2372
2527
  case "message": {
2373
2528
  const content = typed.content;
2374
2529
  if (typeof content === "string") break;
2375
2530
  if (!Array.isArray(content) || content.some((part) => !isTextContentPart(part, "input_text"))) {
2376
- return true;
2531
+ return "unmodeled";
2377
2532
  }
2378
2533
  break;
2379
2534
  }
@@ -2383,31 +2538,30 @@ function hasUnmodeledDirectOpenAIContent(request) {
2383
2538
  const output = typed.output;
2384
2539
  if (typeof output === "string") break;
2385
2540
  if (!Array.isArray(output) || output.some((part) => !isTextContentPart(part, "input_text"))) {
2386
- return true;
2541
+ return "unmodeled";
2387
2542
  }
2388
2543
  break;
2389
2544
  }
2390
2545
  case "custom_tool_call":
2391
- if (typeof typed.input !== "string") return true;
2546
+ if (typeof typed.input !== "string") return "unmodeled";
2392
2547
  break;
2393
2548
  case "custom_tool_call_output":
2394
- if (typeof typed.output !== "string") return true;
2549
+ if (typeof typed.output !== "string") return "unmodeled";
2395
2550
  break;
2396
2551
  case "reasoning":
2397
2552
  break;
2398
2553
  default:
2399
- return true;
2554
+ return "unmodeled";
2400
2555
  }
2401
2556
  }
2402
- return false;
2557
+ return "text";
2403
2558
  }
2404
- return false;
2559
+ return "text";
2405
2560
  }
2406
- function isEligibleDirectOpenAIContext(context) {
2561
+ function isEligibleDirectOpenAIContext(context, entry) {
2407
2562
  const request = context?.request;
2408
2563
  const response = context?.response;
2409
2564
  if (context?.baseUrl !== void 0 && context.baseUrl !== CANONICAL_OPENAI_BASE_URL) return false;
2410
- if (request && hasUnmodeledDirectOpenAIContent(request)) return false;
2411
2565
  const tier = response?.service_tier ?? response?.serviceTier ?? request?.service_tier ?? request?.serviceTier;
2412
2566
  if (tier !== void 0 && (typeof tier !== "string" || !["standard", "default"].includes(tier.toLowerCase()))) {
2413
2567
  return false;
@@ -2422,7 +2576,14 @@ function isEligibleDirectOpenAIContext(context) {
2422
2576
  if (reasoning !== null && typeof reasoning === "object" && "mode" in reasoning && reasoning.mode !== void 0) {
2423
2577
  return false;
2424
2578
  }
2425
- 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)) {
2579
+ const audioOutputRated = entry.audio?.output !== void 0;
2580
+ if (request?.modalities !== void 0 && (!Array.isArray(request.modalities) || request.modalities.length === 0 || request.modalities.some(
2581
+ (modality) => modality !== "text" && !(modality === "audio" && audioOutputRated)
2582
+ ))) {
2583
+ return false;
2584
+ }
2585
+ if (request?.audio !== void 0 && !audioOutputRated) return false;
2586
+ if (["image_generation", "web_search_options"].some((key) => request?.[key] !== void 0)) {
2426
2587
  return false;
2427
2588
  }
2428
2589
  if (Array.isArray(request?.tools) && request.tools.some(
@@ -2433,23 +2594,42 @@ function isEligibleDirectOpenAIContext(context) {
2433
2594
  return true;
2434
2595
  }
2435
2596
  function estimateDirectOpenAICost(model, usage, context) {
2436
- if (!isEligibleDirectOpenAIContext(context)) return void 0;
2437
2597
  const entry = directOpenAIModel(model);
2438
2598
  if (!entry) return void 0;
2439
- const { prompt_tokens, completion_tokens, cached_tokens, cache_write_tokens } = usage;
2599
+ const content = classifyDirectOpenAIContent(context?.request);
2600
+ if (content === "unmodeled") return void 0;
2601
+ if (!isEligibleDirectOpenAIContext(context, entry)) return void 0;
2602
+ const {
2603
+ prompt_tokens,
2604
+ completion_tokens,
2605
+ cached_tokens,
2606
+ cache_write_tokens,
2607
+ audio_input_tokens,
2608
+ audio_output_tokens
2609
+ } = usage;
2610
+ if (content === "audio" && audio_input_tokens === void 0) return void 0;
2440
2611
  const cached = cached_tokens ?? 0;
2441
2612
  const cacheWrite = cache_write_tokens ?? 0;
2442
- if (![prompt_tokens, completion_tokens, cached, cacheWrite].every(
2613
+ const audioInput = audio_input_tokens ?? 0;
2614
+ const audioOutput = audio_output_tokens ?? 0;
2615
+ if (![prompt_tokens, completion_tokens, cached, cacheWrite, audioInput, audioOutput].every(
2443
2616
  (count) => Number.isSafeInteger(count) && count >= 0
2444
- ) || cached + cacheWrite > prompt_tokens) {
2617
+ ) || cached + cacheWrite + audioInput > prompt_tokens || audioOutput > completion_tokens) {
2445
2618
  return void 0;
2446
2619
  }
2447
- const rates = entry.long !== void 0 && entry.contextBoundary !== void 0 && prompt_tokens > entry.contextBoundary ? entry.long : entry.short;
2620
+ if (audioInput > 0 && entry.audio === void 0 || audioOutput > 0 && entry.audio?.output === void 0) {
2621
+ return void 0;
2622
+ }
2623
+ if (audioInput > 0 && cached + cacheWrite > 0) return void 0;
2624
+ const crossesLongContext = entry.long !== void 0 && entry.contextBoundary !== void 0 && prompt_tokens > entry.contextBoundary;
2625
+ if (crossesLongContext && (audioInput > 0 || audioOutput > 0)) return void 0;
2626
+ const rates = crossesLongContext ? entry.long : entry.short;
2448
2627
  if (cached > 0 && rates.cachedInput === void 0 || cacheWrite > 0 && rates.cacheWrite === void 0) {
2449
2628
  return void 0;
2450
2629
  }
2451
- const ordinary = prompt_tokens - cached - cacheWrite;
2452
- return ordinary * rates.input + cached * (rates.cachedInput ?? 0) + cacheWrite * (rates.cacheWrite ?? 0) + completion_tokens * rates.output;
2630
+ const ordinary = prompt_tokens - cached - cacheWrite - audioInput;
2631
+ const textOutput = completion_tokens - audioOutput;
2632
+ 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);
2453
2633
  }
2454
2634
  function estimateOpenAICost(model, promptTokens, completionTokens, cachedTokens) {
2455
2635
  return estimateDirectOpenAICost(model, {
@@ -2539,6 +2719,14 @@ var OPENAI_PROFILE = {
2539
2719
  envBaseUrl: "OPENAI_BASE_URL",
2540
2720
  pricing: { kind: "table", table: OPENAI_PRICING, match: "exact" },
2541
2721
  reasoning: { emit: openaiReasoningEmit, capture: "none" },
2722
+ capabilities: {
2723
+ // Chat Completions carries recorded audio via `input_audio` (wav/mp3 only).
2724
+ // Images are deliberately NOT declared: the Responses API serves images, and
2725
+ // Chat-Completions image pricing is unmodeled here (fork F4).
2726
+ inputModalities: {
2727
+ audio: { sources: ["bytes", "base64"], formats: OPENAI_CHAT_AUDIO_FORMATS }
2728
+ }
2729
+ },
2542
2730
  roleFor: (role, model) => role === "system" && isOSeriesModel(model) ? "developer" : role,
2543
2731
  maxTokensField: "max_completion_tokens",
2544
2732
  parallelToolCalls: (model) => !isOSeriesModel(model)
@@ -2567,19 +2755,37 @@ function base64FromSource(source) {
2567
2755
  "base64"
2568
2756
  );
2569
2757
  }
2570
- function responseImageParts(parts) {
2758
+ function responseImageParts(parts, model) {
2571
2759
  const mapped = [];
2572
2760
  for (const part of parts) {
2573
2761
  if (part.type === "text") {
2574
2762
  mapped.push({ type: "input_text", text: part.text });
2575
2763
  continue;
2576
2764
  }
2765
+ if (part.type === "audio") {
2766
+ throw new UnsupportedModelInputError({
2767
+ provider: "openai-responses",
2768
+ model,
2769
+ modality: "audio",
2770
+ source: part.source.type,
2771
+ feature: "audio input"
2772
+ });
2773
+ }
2774
+ if (part.type !== "image") {
2775
+ const unmapped = part;
2776
+ throw new UnsupportedModelInputError({
2777
+ provider: "openai-responses",
2778
+ model,
2779
+ modality: unmapped.type,
2780
+ feature: "this input modality"
2781
+ });
2782
+ }
2577
2783
  const { source } = part;
2578
2784
  if (source.type === "provider-file") {
2579
2785
  if (source.provider !== "openai-responses") {
2580
2786
  throw new UnsupportedModelInputError({
2581
2787
  provider: "openai-responses",
2582
- model: "unknown",
2788
+ model,
2583
2789
  modality: "image",
2584
2790
  source: "provider-file"
2585
2791
  });
@@ -2597,6 +2803,21 @@ function responseImageParts(parts) {
2597
2803
  }
2598
2804
  return mapped;
2599
2805
  }
2806
+ function toResponsesUsage(raw) {
2807
+ if (!raw) return void 0;
2808
+ const audioInput = reportedTokenCount(raw.input_tokens_details?.audio_tokens);
2809
+ const audioOutput = reportedTokenCount(raw.output_tokens_details?.audio_tokens);
2810
+ return {
2811
+ prompt_tokens: raw.input_tokens,
2812
+ completion_tokens: raw.output_tokens,
2813
+ total_tokens: raw.total_tokens,
2814
+ reasoning_tokens: raw.output_tokens_details?.reasoning_tokens,
2815
+ cached_tokens: raw.input_tokens_details?.cached_tokens,
2816
+ cache_write_tokens: raw.input_tokens_details?.cache_write_tokens,
2817
+ ...audioInput !== void 0 ? { audio_input_tokens: audioInput } : {},
2818
+ ...audioOutput !== void 0 ? { audio_output_tokens: audioOutput } : {}
2819
+ };
2820
+ }
2600
2821
  var OpenAIResponsesProvider = class {
2601
2822
  name = "openai-responses";
2602
2823
  reportsRequestLifecycle = true;
@@ -2612,15 +2833,18 @@ var OpenAIResponsesProvider = class {
2612
2833
  validateInput(request) {
2613
2834
  const modelOverride = request.providerOptions?.model;
2614
2835
  const effectiveModel = typeof modelOverride === "string" ? modelOverride : request.model;
2615
- const fail = (source, feature) => {
2836
+ const failWith = (modality, source, feature) => {
2616
2837
  throw new UnsupportedModelInputError({
2617
2838
  provider: this.name,
2618
2839
  model: effectiveModel || request.model,
2619
- modality: "image",
2840
+ modality,
2620
2841
  ...source ? { source } : {},
2621
2842
  ...feature ? { feature } : {}
2622
2843
  });
2623
2844
  };
2845
+ const fail = (source, feature) => failWith("image", source, feature);
2846
+ const audioPart = firstRichPart(request.input, request.history, "audio");
2847
+ if (audioPart) failWith("audio", audioPart.source.type, "audio input");
2624
2848
  if (request.providerOptions && "model" in request.providerOptions && (typeof modelOverride !== "string" || modelOverride.trim().length === 0)) {
2625
2849
  fail(void 0, "invalid model providerOptions");
2626
2850
  }
@@ -2767,7 +2991,7 @@ var OpenAIResponsesProvider = class {
2767
2991
  const nonSystemMessages = messages.filter((m) => m.role !== "system");
2768
2992
  const body = {
2769
2993
  model: effectiveModel,
2770
- input: this.buildInput(nonSystemMessages),
2994
+ input: this.buildInput(nonSystemMessages, effectiveModel),
2771
2995
  store: false,
2772
2996
  stream
2773
2997
  };
@@ -2819,7 +3043,7 @@ var OpenAIResponsesProvider = class {
2819
3043
  // ---------------------------------------------------------------------------
2820
3044
  // Internal: message → input mapping
2821
3045
  // ---------------------------------------------------------------------------
2822
- buildInput(messages) {
3046
+ buildInput(messages, model) {
2823
3047
  const input = [];
2824
3048
  for (const msg of messages) {
2825
3049
  const text = typeof msg.content === "string" ? msg.content : "";
@@ -2857,7 +3081,7 @@ var OpenAIResponsesProvider = class {
2857
3081
  input.push({
2858
3082
  type: "message",
2859
3083
  role: msg.role,
2860
- content: typeof msg.content === "string" ? msg.content : responseImageParts(msg.content)
3084
+ content: typeof msg.content === "string" ? msg.content : responseImageParts(msg.content, model)
2861
3085
  });
2862
3086
  }
2863
3087
  }
@@ -2916,14 +3140,7 @@ var OpenAIResponsesProvider = class {
2916
3140
  }
2917
3141
  }
2918
3142
  }
2919
- const usage = json.usage ? {
2920
- prompt_tokens: json.usage.input_tokens,
2921
- completion_tokens: json.usage.output_tokens,
2922
- total_tokens: json.usage.total_tokens,
2923
- reasoning_tokens: json.usage.output_tokens_details?.reasoning_tokens,
2924
- cached_tokens: json.usage.input_tokens_details?.cached_tokens,
2925
- cache_write_tokens: json.usage.input_tokens_details?.cache_write_tokens
2926
- } : void 0;
3143
+ const usage = toResponsesUsage(json.usage);
2927
3144
  const cost = usage && !this.requestContainsImages(request) ? estimateDirectOpenAICost(json.model ?? model, usage, {
2928
3145
  baseUrl: this.baseUrl,
2929
3146
  request,
@@ -3030,14 +3247,7 @@ var OpenAIResponsesProvider = class {
3030
3247
  }
3031
3248
  case "response.completed": {
3032
3249
  const response = data.response;
3033
- const usage = response?.usage ? {
3034
- prompt_tokens: response.usage.input_tokens,
3035
- completion_tokens: response.usage.output_tokens,
3036
- total_tokens: response.usage.total_tokens,
3037
- reasoning_tokens: response.usage.output_tokens_details?.reasoning_tokens,
3038
- cached_tokens: response.usage.input_tokens_details?.cached_tokens,
3039
- cache_write_tokens: response.usage.input_tokens_details?.cache_write_tokens
3040
- } : void 0;
3250
+ const usage = toResponsesUsage(response?.usage);
3041
3251
  const reasoningItems = response?.output?.filter((item) => item.type === "reasoning") ?? [];
3042
3252
  const providerMetadata = reasoningItems.length > 0 ? { openaiReasoningItems: reasoningItems } : void 0;
3043
3253
  return {
@@ -3086,19 +3296,37 @@ function anthropicBase64(source) {
3086
3296
  "base64"
3087
3297
  );
3088
3298
  }
3089
- function anthropicImageBlocks(parts) {
3299
+ function anthropicImageBlocks(parts, model) {
3090
3300
  const blocks = [];
3091
3301
  for (const part of parts) {
3092
3302
  if (part.type === "text") {
3093
3303
  blocks.push({ type: "text", text: part.text });
3094
3304
  continue;
3095
3305
  }
3306
+ if (part.type === "audio") {
3307
+ throw new UnsupportedModelInputError({
3308
+ provider: "anthropic",
3309
+ model,
3310
+ modality: "audio",
3311
+ source: part.source.type,
3312
+ feature: "audio input"
3313
+ });
3314
+ }
3315
+ if (part.type !== "image") {
3316
+ const unmapped = part;
3317
+ throw new UnsupportedModelInputError({
3318
+ provider: "anthropic",
3319
+ model,
3320
+ modality: unmapped.type,
3321
+ feature: "this input modality"
3322
+ });
3323
+ }
3096
3324
  const { source } = part;
3097
3325
  if (source.type === "provider-file") {
3098
3326
  if (source.provider !== "anthropic") {
3099
3327
  throw new UnsupportedModelInputError({
3100
3328
  provider: "anthropic",
3101
- model: "unknown",
3329
+ model,
3102
3330
  modality: "image",
3103
3331
  source: "provider-file"
3104
3332
  });
@@ -3560,15 +3788,18 @@ var AnthropicProvider = class {
3560
3788
  validateInput(request) {
3561
3789
  const modelOverride = request.providerOptions?.model;
3562
3790
  const effectiveModel = typeof modelOverride === "string" ? modelOverride : request.model;
3563
- const fail = (source, feature) => {
3791
+ const failWith = (modality, source, feature) => {
3564
3792
  throw new UnsupportedModelInputError({
3565
3793
  provider: this.name,
3566
3794
  model: effectiveModel || request.model,
3567
- modality: "image",
3795
+ modality,
3568
3796
  ...source ? { source } : {},
3569
3797
  ...feature ? { feature } : {}
3570
3798
  });
3571
3799
  };
3800
+ const fail = (source, feature) => failWith("image", source, feature);
3801
+ const audioPart = firstRichPart(request.input, request.history, "audio");
3802
+ if (audioPart) failWith("audio", audioPart.source.type, "audio input");
3572
3803
  if (request.providerOptions && "model" in request.providerOptions && (typeof modelOverride !== "string" || modelOverride.trim().length === 0)) {
3573
3804
  fail(void 0, "invalid model providerOptions");
3574
3805
  }
@@ -3739,7 +3970,7 @@ var AnthropicProvider = class {
3739
3970
  const systemText = systemMessages.map((m) => m.content).join("\n\n");
3740
3971
  const body = {
3741
3972
  model: effectiveModel,
3742
- messages: this.mapMessages(nonSystemMessages),
3973
+ messages: this.mapMessages(nonSystemMessages, effectiveModel),
3743
3974
  max_tokens: options.maxTokens ?? 4096,
3744
3975
  stream
3745
3976
  };
@@ -3806,7 +4037,7 @@ ${jsonInstruction}` : jsonInstruction;
3806
4037
  * - assistant messages with tool_calls -> assistant with tool_use content blocks
3807
4038
  * - tool messages (tool results) -> user messages with tool_result content blocks
3808
4039
  */
3809
- mapMessages(messages) {
4040
+ mapMessages(messages, model) {
3810
4041
  const result = [];
3811
4042
  for (const msg of messages) {
3812
4043
  const text = typeof msg.content === "string" ? msg.content : "";
@@ -3853,7 +4084,7 @@ ${jsonInstruction}` : jsonInstruction;
3853
4084
  } else if (msg.role === "user") {
3854
4085
  result.push({
3855
4086
  role: "user",
3856
- content: typeof msg.content === "string" ? msg.content : anthropicImageBlocks(msg.content)
4087
+ content: typeof msg.content === "string" ? msg.content : anthropicImageBlocks(msg.content, model)
3857
4088
  });
3858
4089
  }
3859
4090
  }
@@ -4157,7 +4388,7 @@ function lastFallbackIndex(blocks) {
4157
4388
  function hasRichGeminiMessages(messages) {
4158
4389
  return messages.some((message) => Array.isArray(message.content));
4159
4390
  }
4160
- function geminiImageBase64(source) {
4391
+ function geminiInlineBase64(source) {
4161
4392
  return source.type === "base64" ? source.data : Buffer.from(source.data.buffer, source.data.byteOffset, source.data.byteLength).toString(
4162
4393
  "base64"
4163
4394
  );
@@ -4169,6 +4400,50 @@ function geminiInteractionContent(parts, model) {
4169
4400
  content.push({ type: "text", text: part.text });
4170
4401
  continue;
4171
4402
  }
4403
+ if (part.type === "audio") {
4404
+ const { source: audioSource } = part;
4405
+ if (audioSource.type === "provider-file") {
4406
+ if (audioSource.provider !== "google") {
4407
+ throw new UnsupportedModelInputError({
4408
+ provider: "google",
4409
+ model,
4410
+ modality: "audio",
4411
+ source: "provider-file"
4412
+ });
4413
+ }
4414
+ if (!audioSource.mediaType) {
4415
+ throw new UnsupportedModelInputError({
4416
+ provider: "google",
4417
+ model,
4418
+ modality: "audio",
4419
+ source: "provider-file",
4420
+ feature: "Interactions URI audio mediaType"
4421
+ });
4422
+ }
4423
+ content.push({
4424
+ type: "audio",
4425
+ uri: audioSource.reference,
4426
+ mime_type: audioSource.mediaType
4427
+ });
4428
+ } else {
4429
+ content.push({
4430
+ type: "audio",
4431
+ data: geminiInlineBase64(audioSource),
4432
+ mime_type: audioSource.mediaType
4433
+ });
4434
+ }
4435
+ if (part.label) content.push({ type: "text", text: `[Audio: ${part.label}]` });
4436
+ continue;
4437
+ }
4438
+ if (part.type !== "image") {
4439
+ const unmapped = part;
4440
+ throw new UnsupportedModelInputError({
4441
+ provider: "google",
4442
+ model,
4443
+ modality: unmapped.type,
4444
+ feature: "this input modality"
4445
+ });
4446
+ }
4172
4447
  const { source } = part;
4173
4448
  if (source.type === "provider-file") {
4174
4449
  if (source.provider !== "google") {
@@ -4202,7 +4477,11 @@ function geminiInteractionContent(parts, model) {
4202
4477
  feature: "direct URL image input; pass bytes/base64 or a Gemini provider-file"
4203
4478
  });
4204
4479
  } else {
4205
- content.push({ type: "image", data: geminiImageBase64(source), mime_type: source.mediaType });
4480
+ content.push({
4481
+ type: "image",
4482
+ data: geminiInlineBase64(source),
4483
+ mime_type: source.mediaType
4484
+ });
4206
4485
  }
4207
4486
  if (part.label) content.push({ type: "text", text: `[Image: ${part.label}]` });
4208
4487
  }
@@ -4259,7 +4538,7 @@ var GEMINI_PRICING = {
4259
4538
  output: 1e-5,
4260
4539
  longContext: { input: 25e-7, cached: 25e-8, output: 15e-6 }
4261
4540
  },
4262
- "gemini-2.5-flash": { input: 3e-7, cached: 3e-8, output: 25e-7 },
4541
+ "gemini-2.5-flash": { input: 3e-7, cached: 3e-8, output: 25e-7, audioInput: 1e-6 },
4263
4542
  "gemini-2.5-flash-lite": { input: 1e-7, cached: 1e-8, output: 4e-7 },
4264
4543
  "gemini-3.1-pro-preview": {
4265
4544
  input: 2e-6,
@@ -4277,18 +4556,43 @@ var GEMINI_PRICING = {
4277
4556
  "gemini-3.5-flash": { input: 15e-7, cached: 15e-8, output: 9e-6 },
4278
4557
  "gemini-3.5-flash-lite": { input: 3e-7, cached: 3e-8, output: 25e-7 },
4279
4558
  "gemini-3.6-flash": { input: 75e-8, cached: 75e-9, output: 375e-8 },
4280
- "gemini-3.7-flash": { input: 75e-8, cached: 75e-9, output: 375e-8 },
4559
+ "gemini-3.7-flash": {
4560
+ input: 75e-8,
4561
+ cached: 75e-9,
4562
+ output: 375e-8,
4563
+ audioInput: 75e-8
4564
+ },
4281
4565
  "gemini-3.8-flash": { input: 75e-8, cached: 75e-9, output: 375e-8 }
4282
4566
  };
4567
+ var CANONICAL_GEMINI_BASE_URL = "https://generativelanguage.googleapis.com/v1beta";
4283
4568
  function isValidTokenCount2(value) {
4284
4569
  return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
4285
4570
  }
4286
4571
  function isStandardGeminiRequestTier(value) {
4287
- return value === void 0 || value === "SERVICE_TIER_UNSPECIFIED" || value === "SERVICE_TIER_STANDARD";
4572
+ return value === void 0 || value === null || isDefinitiveStandardGeminiResponseTier(value);
4288
4573
  }
4289
4574
  function isDefinitiveStandardGeminiResponseTier(value) {
4290
4575
  return value === "SERVICE_TIER_UNSPECIFIED" || value === "SERVICE_TIER_STANDARD" || value === "standard";
4291
4576
  }
4577
+ function recordGeminiResponseTier(evidence, value) {
4578
+ if (value === void 0) return;
4579
+ evidence.reportsTier = true;
4580
+ if (isDefinitiveStandardGeminiResponseTier(value)) {
4581
+ evidence.hasDefinitiveStandardTier = true;
4582
+ } else {
4583
+ evidence.hasInvalidTier = true;
4584
+ }
4585
+ }
4586
+ function geminiResponseTierEvidence(responseHeaders, ...values) {
4587
+ const evidence = {
4588
+ reportsTier: false,
4589
+ hasDefinitiveStandardTier: false,
4590
+ hasInvalidTier: false
4591
+ };
4592
+ recordGeminiResponseTier(evidence, responseHeaders?.get("x-gemini-service-tier") ?? void 0);
4593
+ for (const value of values) recordGeminiResponseTier(evidence, value);
4594
+ return evidence;
4595
+ }
4292
4596
  function geminiInteractionErrorStatus(code) {
4293
4597
  if (typeof code === "number" && Number.isInteger(code) && code >= 100 && code <= 599) {
4294
4598
  return code;
@@ -4347,8 +4651,19 @@ function estimateGeminiCost(model, usage) {
4347
4651
  if (!pricing || !isValidTokenCount2(usage.inputTokens) || !isValidTokenCount2(usage.outputTokens) || !isValidTokenCount2(cached) || cached > usage.inputTokens) {
4348
4652
  return void 0;
4349
4653
  }
4350
- const rate = usage.inputTokens > 2e5 && pricing.longContext ? pricing.longContext : pricing;
4351
- return (usage.inputTokens - cached) * rate.input + cached * rate.cached + usage.outputTokens * rate.output;
4654
+ const split = usage.modalities;
4655
+ const audio = split?.audioTokens ?? 0;
4656
+ if (split !== void 0) {
4657
+ if (audio > 0 && pricing.audioInput === void 0) return void 0;
4658
+ if (!isValidTokenCount2(audio) || !isValidTokenCount2(split.nonAudioTokens) || audio + split.nonAudioTokens !== usage.inputTokens || cached + audio > usage.inputTokens) {
4659
+ return void 0;
4660
+ }
4661
+ if (audio > 0 && cached > 0) return void 0;
4662
+ }
4663
+ const crossesLongContext = usage.inputTokens > 2e5 && pricing.longContext !== void 0;
4664
+ if (crossesLongContext && audio > 0) return void 0;
4665
+ const rate = crossesLongContext ? pricing.longContext : pricing;
4666
+ return (usage.inputTokens - cached - audio) * rate.input + cached * rate.cached + audio * (pricing.audioInput ?? 0) + usage.outputTokens * rate.output;
4352
4667
  }
4353
4668
  function safeTokenSum2(...values) {
4354
4669
  const total = values.reduce((sum, value) => sum + value, 0);
@@ -4508,20 +4823,44 @@ var GeminiProvider = class {
4508
4823
  name = "google";
4509
4824
  reportsRequestLifecycle = true;
4510
4825
  inputCapabilities(model) {
4511
- return model.trim().length > 0 ? { image: { sources: ["bytes", "base64", "provider-file"] } } : {};
4826
+ return model.trim().length > 0 ? {
4827
+ image: { sources: ["bytes", "base64", "provider-file"] },
4828
+ audio: { sources: ["bytes", "base64", "provider-file"] }
4829
+ } : {};
4512
4830
  }
4513
4831
  validateInput(request) {
4514
4832
  const modelOverride = request.providerOptions?.model;
4515
4833
  const effectiveModel = typeof modelOverride === "string" ? modelOverride : request.model;
4516
- const fail = (source, feature) => {
4834
+ const failWith = (modality, source, feature) => {
4517
4835
  throw new UnsupportedModelInputError({
4518
4836
  provider: this.name,
4519
4837
  model: effectiveModel || request.model,
4520
- modality: "image",
4838
+ modality,
4521
4839
  ...source ? { source } : {},
4522
4840
  ...feature ? { feature } : {}
4523
4841
  });
4524
4842
  };
4843
+ const fail = (source, feature) => failWith(firstRichPart(request.input, request.history)?.type ?? "image", source, feature);
4844
+ const checkPart = (part) => {
4845
+ if (part.type === "audio") {
4846
+ if (part.source.type === "provider-file" && part.source.provider !== this.name)
4847
+ failWith("audio", "provider-file");
4848
+ if (part.source.type === "provider-file" && !part.source.mediaType)
4849
+ failWith("audio", "provider-file", "Interactions URI audio mediaType");
4850
+ return;
4851
+ }
4852
+ if (part.type !== "image") return;
4853
+ if (part.source.type === "url")
4854
+ failWith(
4855
+ "image",
4856
+ "url",
4857
+ "direct URL image input; pass bytes/base64 or a Gemini provider-file"
4858
+ );
4859
+ if (part.source.type === "provider-file" && part.source.provider !== this.name)
4860
+ failWith("image", "provider-file");
4861
+ if (part.source.type === "provider-file" && !part.source.mediaType)
4862
+ failWith("image", "provider-file", "Interactions URI image mediaType");
4863
+ };
4525
4864
  if (request.providerOptions && "model" in request.providerOptions && (typeof modelOverride !== "string" || modelOverride.trim().length === 0)) {
4526
4865
  fail(void 0, "invalid model providerOptions");
4527
4866
  }
@@ -4543,26 +4882,10 @@ var GeminiProvider = class {
4543
4882
  for (const message of request.history) {
4544
4883
  if (!Array.isArray(message.content)) continue;
4545
4884
  if (message.role !== "user") fail(void 0, "rich non-user history");
4546
- for (const part of message.content) {
4547
- if (part.type !== "image") continue;
4548
- if (part.source.type === "url")
4549
- fail("url", "direct URL image input; pass bytes/base64 or a Gemini provider-file");
4550
- if (part.source.type === "provider-file" && part.source.provider !== this.name)
4551
- fail("provider-file");
4552
- if (part.source.type === "provider-file" && !part.source.mediaType)
4553
- fail("provider-file", "Interactions URI image mediaType");
4554
- }
4885
+ for (const part of message.content) checkPart(part);
4555
4886
  }
4556
4887
  if (Array.isArray(request.input)) {
4557
- for (const part of request.input) {
4558
- if (part.type !== "image") continue;
4559
- if (part.source.type === "url")
4560
- fail("url", "direct URL image input; pass bytes/base64 or a Gemini provider-file");
4561
- if (part.source.type === "provider-file" && part.source.provider !== this.name)
4562
- fail("provider-file");
4563
- if (part.source.type === "provider-file" && !part.source.mediaType)
4564
- fail("provider-file", "Interactions URI image mediaType");
4565
- }
4888
+ for (const part of request.input) checkPart(part);
4566
4889
  }
4567
4890
  return { effectiveModel };
4568
4891
  }
@@ -4698,6 +5021,7 @@ var GeminiProvider = class {
4698
5021
  async chatInteraction(messages, options) {
4699
5022
  this.assertSafeInteractionOptions(options);
4700
5023
  const body = this.buildInteractionRequestBody(messages, options, false);
5024
+ const pricingContext = this.interactionPricingContext(this.requestModel(options), body);
4701
5025
  const headers = this.buildHeaders(await this.resolveKey());
4702
5026
  const recorder = new CallTimingRecorder(options.requestLifecycle);
4703
5027
  const res = await fetchWithRetry(
@@ -4718,12 +5042,17 @@ var GeminiProvider = class {
4718
5042
  }
4719
5043
  return withChatTiming(
4720
5044
  recorder,
4721
- this.parseInteractionResponse(await res.json())
5045
+ this.parseInteractionResponse(
5046
+ await res.json(),
5047
+ pricingContext,
5048
+ res.headers
5049
+ )
4722
5050
  );
4723
5051
  }
4724
5052
  async *streamInteraction(messages, options) {
4725
5053
  this.assertSafeInteractionOptions(options);
4726
5054
  const body = this.buildInteractionRequestBody(messages, options, true);
5055
+ const pricingContext = this.interactionPricingContext(this.requestModel(options), body);
4727
5056
  const headers = this.buildHeaders(await this.resolveKey());
4728
5057
  const recorder = new CallTimingRecorder(options.requestLifecycle);
4729
5058
  const res = await fetchWithRetry(
@@ -4745,7 +5074,7 @@ var GeminiProvider = class {
4745
5074
  if (!res.body) throw new Error("Gemini Interactions stream has no body");
4746
5075
  yield* withCallTiming(
4747
5076
  recorder,
4748
- this.parseInteractionSSEStream(res.body, res.headers, recorder)
5077
+ this.parseInteractionSSEStream(res.body, res.headers, recorder, pricingContext)
4749
5078
  );
4750
5079
  }
4751
5080
  buildInteractionRequestBody(messages, options, stream) {
@@ -4840,6 +5169,7 @@ var GeminiProvider = class {
4840
5169
  }
4841
5170
  mapInteractionInput(messages, model) {
4842
5171
  const steps = [];
5172
+ const functionNames = /* @__PURE__ */ new Map();
4843
5173
  for (const message of messages) {
4844
5174
  if (message.role === "user") {
4845
5175
  steps.push({
@@ -4849,6 +5179,10 @@ var GeminiProvider = class {
4849
5179
  } else if (message.role === "assistant") {
4850
5180
  const nativeSteps = message.providerMetadata?.geminiInteractionSteps;
4851
5181
  if (Array.isArray(nativeSteps) && nativeSteps.every(isGeminiInteractionStep)) {
5182
+ for (const step of nativeSteps) {
5183
+ if (step.type === "function_call" && typeof step.id === "string" && step.name)
5184
+ functionNames.set(step.id, step.name);
5185
+ }
4852
5186
  steps.push(...nativeSteps);
4853
5187
  continue;
4854
5188
  }
@@ -4856,6 +5190,7 @@ var GeminiProvider = class {
4856
5190
  steps.push({ type: "model_output", content: [{ type: "text", text: message.content }] });
4857
5191
  }
4858
5192
  for (const call of message.tool_calls ?? []) {
5193
+ functionNames.set(call.id, call.function.name);
4859
5194
  steps.push({
4860
5195
  type: "function_call",
4861
5196
  id: call.id,
@@ -4864,9 +5199,17 @@ var GeminiProvider = class {
4864
5199
  });
4865
5200
  }
4866
5201
  } else if (message.role === "tool") {
5202
+ const callId = message.tool_call_id ?? "";
5203
+ const name = functionNames.get(callId) ?? message.name;
5204
+ if (!name) {
5205
+ throw new InvalidModelInputError(
5206
+ `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'`
5207
+ );
5208
+ }
4867
5209
  steps.push({
4868
5210
  type: "function_result",
4869
- call_id: message.tool_call_id ?? "",
5211
+ name,
5212
+ call_id: callId,
4870
5213
  result: [
4871
5214
  { type: "text", text: typeof message.content === "string" ? message.content : "" }
4872
5215
  ]
@@ -4875,7 +5218,7 @@ var GeminiProvider = class {
4875
5218
  }
4876
5219
  return steps;
4877
5220
  }
4878
- parseInteractionResponse(json) {
5221
+ parseInteractionResponse(json, pricingContext, responseHeaders) {
4879
5222
  let content = "";
4880
5223
  let thinkingContent = "";
4881
5224
  const toolCalls = [];
@@ -4891,18 +5234,63 @@ var GeminiProvider = class {
4891
5234
  }
4892
5235
  }
4893
5236
  this.assertInteractionTerminalStatus(json.status, toolCalls.length);
4894
- const usage = normalizeInteractionUsage(json.usage);
5237
+ const normalized = normalizeInteractionUsage(json.usage);
4895
5238
  return {
4896
5239
  content,
4897
5240
  thinking_content: thinkingContent || void 0,
4898
5241
  tool_calls: toolCalls.length ? toolCalls : void 0,
4899
- usage,
4900
- // Interactions returns modality usage but no authoritative monetary cost.
4901
- cost: void 0,
5242
+ usage: normalized?.usage,
5243
+ cost: this.interactionCost({
5244
+ normalized,
5245
+ steps: json.steps,
5246
+ responseTierEvidence: geminiResponseTierEvidence(
5247
+ responseHeaders,
5248
+ json.service_tier,
5249
+ json.usage?.service_tier
5250
+ ),
5251
+ effectiveModel: typeof json.model === "string" ? json.model : pricingContext.model,
5252
+ pricingContext,
5253
+ // The whole body is in hand: every part is on `json.steps`, so there
5254
+ // is nothing this path could have failed to materialize.
5255
+ unmodeledOutput: false
5256
+ }),
4902
5257
  providerMetadata: json.steps?.length ? { geminiInteractionSteps: json.steps.filter(isGeminiInteractionStep) } : void 0
4903
5258
  };
4904
5259
  }
4905
- async *parseInteractionSSEStream(body, responseHeaders, timing) {
5260
+ /**
5261
+ * Cost for one Interactions call, or `undefined` when any precondition
5262
+ * fails. Streaming and non-streaming share this one body, so identical usage
5263
+ * always produces an identical number on both paths.
5264
+ */
5265
+ interactionCost(call) {
5266
+ const { normalized, steps, responseTierEvidence, effectiveModel, pricingContext } = call;
5267
+ if (!normalized?.pricingUsage || normalized.hasUnmodeledBilledUsage) return void 0;
5268
+ if (call.unmodeledOutput || !this.isTextOnlyInteractionOutput(steps)) return void 0;
5269
+ if (!isEligibleGeminiPricing(
5270
+ pricingContext,
5271
+ !responseTierEvidence.reportsTier || responseTierEvidence.hasDefinitiveStandardTier,
5272
+ responseTierEvidence.hasInvalidTier
5273
+ )) {
5274
+ return void 0;
5275
+ }
5276
+ return estimateGeminiCost(effectiveModel, normalized.pricingUsage);
5277
+ }
5278
+ /**
5279
+ * Whether every model-produced step is text. A non-text reply part (image,
5280
+ * audio, inline data) is billed on an output track this adapter carries no
5281
+ * rate for, so the call stays unpriced. Echoed `user_input` and
5282
+ * Axl-authored `function_result` steps are not model output and are ignored.
5283
+ */
5284
+ isTextOnlyInteractionOutput(steps) {
5285
+ return (steps ?? []).every((step) => {
5286
+ if (step.type !== "model_output" && step.type !== "thought") return true;
5287
+ const parts = [...step.content ?? [], ...step.summary ?? []];
5288
+ return parts.every(
5289
+ (part) => part !== null && typeof part === "object" && part.type === "text"
5290
+ );
5291
+ });
5292
+ }
5293
+ async *parseInteractionSSEStream(body, responseHeaders, timing, pricingContext) {
4906
5294
  const reader = body.getReader();
4907
5295
  const decoder = new TextDecoder();
4908
5296
  let buffer = "";
@@ -4910,6 +5298,8 @@ var GeminiProvider = class {
4910
5298
  const argumentBuffers = /* @__PURE__ */ new Map();
4911
5299
  const stopped = /* @__PURE__ */ new Set();
4912
5300
  let completed = false;
5301
+ const responseTierEvidence = geminiResponseTierEvidence(responseHeaders);
5302
+ let unmodeledOutput = false;
4913
5303
  try {
4914
5304
  while (true) {
4915
5305
  const { done, value } = await timing.read(reader);
@@ -4927,6 +5317,8 @@ var GeminiProvider = class {
4927
5317
  } catch {
4928
5318
  continue;
4929
5319
  }
5320
+ recordGeminiResponseTier(responseTierEvidence, event.interaction?.service_tier);
5321
+ recordGeminiResponseTier(responseTierEvidence, event.interaction?.usage?.service_tier);
4930
5322
  if (event.event_type === "error") {
4931
5323
  const status = geminiInteractionErrorStatus(event.error?.code);
4932
5324
  const message = event.error?.message ?? "Gemini Interactions stream failed";
@@ -4954,6 +5346,7 @@ var GeminiProvider = class {
4954
5346
  }
4955
5347
  const step = cloneInteractionStep(event.step);
4956
5348
  steps.set(index, step);
5349
+ if (!this.isTextOnlyInteractionOutput([step])) unmodeledOutput = true;
4957
5350
  if (step.type === "model_output") {
4958
5351
  for (const text of interactionTextChunks(step.content)) {
4959
5352
  yield { type: "text_delta", content: text };
@@ -4976,6 +5369,12 @@ var GeminiProvider = class {
4976
5369
  const step = steps.get(index);
4977
5370
  if (!step)
4978
5371
  throw new Error("Gemini Interactions stream delta arrived before step.start");
5372
+ if (step.type === "model_output" && !(delta.type === "text" && typeof delta.text === "string")) {
5373
+ unmodeledOutput = true;
5374
+ }
5375
+ if (step.type === "thought" && !(delta.type === "thought_summary" && !!delta.content) && !(delta.type === "thought_signature" && typeof delta.signature === "string")) {
5376
+ unmodeledOutput = true;
5377
+ }
4979
5378
  if (delta.type === "text" && typeof delta.text === "string" && step.type === "model_output") {
4980
5379
  appendInteractionContent(step, "content", { type: "text", text: delta.text });
4981
5380
  yield { type: "text_delta", content: delta.text };
@@ -5023,12 +5422,20 @@ var GeminiProvider = class {
5023
5422
  ).length;
5024
5423
  this.assertInteractionTerminalStatus(event.interaction?.status, toolCallCount);
5025
5424
  completed = true;
5425
+ const orderedSteps = [...steps.entries()].sort(([a], [b]) => a - b).map(([, step]) => step);
5426
+ const normalized = normalizeInteractionUsage(event.interaction?.usage);
5026
5427
  yield {
5027
5428
  type: "done",
5028
- usage: normalizeInteractionUsage(event.interaction?.usage),
5029
- providerMetadata: steps.size ? {
5030
- geminiInteractionSteps: [...steps.entries()].sort(([a], [b]) => a - b).map(([, step]) => step)
5031
- } : void 0
5429
+ usage: normalized?.usage,
5430
+ cost: this.interactionCost({
5431
+ normalized,
5432
+ steps: orderedSteps,
5433
+ responseTierEvidence,
5434
+ effectiveModel: typeof event.interaction?.model === "string" ? event.interaction.model : pricingContext.model,
5435
+ pricingContext,
5436
+ unmodeledOutput
5437
+ }),
5438
+ providerMetadata: steps.size ? { geminiInteractionSteps: orderedSteps } : void 0
5032
5439
  };
5033
5440
  return;
5034
5441
  }
@@ -5070,6 +5477,51 @@ var GeminiProvider = class {
5070
5477
  }
5071
5478
  return `Gemini API error (${status}): ${body}`;
5072
5479
  }
5480
+ /**
5481
+ * Pricing context for the stateless Interactions transport.
5482
+ *
5483
+ * The endpoint reports a per-modality input split, so a rich call IS
5484
+ * priceable — but only from the canonical endpoint, on a standard tier, with
5485
+ * a request whose billing is fully represented by the token totals.
5486
+ */
5487
+ interactionPricingContext(fallbackModel, body) {
5488
+ return {
5489
+ model: typeof body.model === "string" ? body.model : fallbackModel,
5490
+ serviceTier: body.service_tier ?? body.serviceTier,
5491
+ eligibleRequest: this.baseUrl === CANONICAL_GEMINI_BASE_URL && isStandardGeminiRequestTier(body.serviceTier) && this.isModeledInteractionRequest(body)
5492
+ };
5493
+ }
5494
+ /**
5495
+ * Whether an Interactions request body's billing is fully represented by the
5496
+ * reported token totals. `providerOptions` merges arbitrary keys into the
5497
+ * body, so anything that can move billing off the local totals — a
5498
+ * server-side cache, a server-executed tool, a residency/region override, a
5499
+ * non-text response modality — makes the call unpriced.
5500
+ *
5501
+ * Input CONTENT is deliberately not inspected: `input_tokens_by_modality` is
5502
+ * the authority on what the input billed as, and it unprices any modality
5503
+ * without a billing category (video, documents, anything new).
5504
+ */
5505
+ isModeledInteractionRequest(body) {
5506
+ if ([
5507
+ "cached_content",
5508
+ "cachedContent",
5509
+ "previous_interaction_id",
5510
+ "region",
5511
+ "inference_geo",
5512
+ "data_residency"
5513
+ ].some((key) => body[key] !== void 0)) {
5514
+ return false;
5515
+ }
5516
+ if (body.tools !== void 0 && (!Array.isArray(body.tools) || body.tools.some(
5517
+ (tool2) => tool2 === null || typeof tool2 !== "object" || tool2.type !== "function"
5518
+ ))) {
5519
+ return false;
5520
+ }
5521
+ const generationConfig = body.generation_config;
5522
+ const modalities = generationConfig !== null && typeof generationConfig === "object" ? generationConfig.response_modalities : void 0;
5523
+ return modalities === void 0 || modalities === "TEXT" || Array.isArray(modalities) && modalities.length === 1 && modalities[0] === "TEXT";
5524
+ }
5073
5525
  pricingContext(fallbackModel, body) {
5074
5526
  return {
5075
5527
  // Gemini's model is normally selected in the URL. providerOptions is an
@@ -5077,7 +5529,7 @@ var GeminiProvider = class {
5077
5529
  // the URL and pricing rather than pricing a different request.
5078
5530
  model: typeof body.model === "string" ? body.model : fallbackModel,
5079
5531
  serviceTier: body.serviceTier,
5080
- eligibleRequest: this.baseUrl === "https://generativelanguage.googleapis.com/v1beta" && this.isTextOnlyClientRequest(body)
5532
+ eligibleRequest: this.baseUrl === CANONICAL_GEMINI_BASE_URL && this.isTextOnlyClientRequest(body)
5081
5533
  };
5082
5534
  }
5083
5535
  requestModel(options) {
@@ -5400,7 +5852,7 @@ var GeminiProvider = class {
5400
5852
  }
5401
5853
  const normalized = json.usageMetadata ? normalizeGeminiUsage(json.usageMetadata) : void 0;
5402
5854
  const effectiveModel = typeof json.modelVersion === "string" ? json.modelVersion : pricingContext.model;
5403
- const cost = normalized && !normalized.hasUnmodeledBilledUsage && this.isTextOnlyGeminiResponse(candidate) && isEligibleGeminiPricing(
5855
+ const cost = normalized?.pricingUsage && !normalized.hasUnmodeledBilledUsage && this.isTextOnlyGeminiResponse(candidate) && isEligibleGeminiPricing(
5404
5856
  pricingContext,
5405
5857
  isDefinitiveStandardGeminiResponseTier(json.usageMetadata?.serviceTier),
5406
5858
  json.usageMetadata?.serviceTier !== void 0 && !isDefinitiveStandardGeminiResponseTier(json.usageMetadata.serviceTier)
@@ -5493,7 +5945,7 @@ var GeminiProvider = class {
5493
5945
  yield {
5494
5946
  type: "done",
5495
5947
  usage: normalizedUsage?.usage,
5496
- cost: normalizedUsage && !normalizedUsage.hasUnmodeledBilledUsage && responseIsTextOnly && isEligibleGeminiPricing(
5948
+ cost: normalizedUsage?.pricingUsage && !normalizedUsage.hasUnmodeledBilledUsage && responseIsTextOnly && isEligibleGeminiPricing(
5497
5949
  pricingContext,
5498
5950
  hasDefinitiveStandardResponseTier,
5499
5951
  hasInvalidResponseTier
@@ -5539,20 +5991,73 @@ function safeJsonObject(value) {
5539
5991
  return {};
5540
5992
  }
5541
5993
  }
5994
+ var KNOWN_GEMINI_MODALITIES = /* @__PURE__ */ new Set(["text", "image", "audio"]);
5995
+ function foldGeminiModalities(raw, totalInputTokens) {
5996
+ if (!Array.isArray(raw)) return void 0;
5997
+ let audioTokens = 0;
5998
+ let nonAudioTokens = 0;
5999
+ for (const entry of raw) {
6000
+ if (entry === null || typeof entry !== "object") return void 0;
6001
+ const { modality, tokens } = entry;
6002
+ if (typeof modality !== "string" || !isValidTokenCount2(tokens)) return void 0;
6003
+ const normalized = modality.toLowerCase();
6004
+ if (!KNOWN_GEMINI_MODALITIES.has(normalized)) return void 0;
6005
+ if (normalized === "audio") audioTokens += tokens;
6006
+ else nonAudioTokens += tokens;
6007
+ }
6008
+ const total = safeTokenSum2(audioTokens, nonAudioTokens);
6009
+ if (total === void 0 || total !== totalInputTokens) return void 0;
6010
+ return { audioTokens, nonAudioTokens };
6011
+ }
6012
+ function reportedGeminiAudioTokens(raw, totalInputTokens) {
6013
+ if (!Array.isArray(raw)) return void 0;
6014
+ let audioTokens;
6015
+ for (const entry of raw) {
6016
+ if (entry === null || typeof entry !== "object") return void 0;
6017
+ const { modality, tokens } = entry;
6018
+ if (typeof modality !== "string" || !isValidTokenCount2(tokens)) return void 0;
6019
+ if (modality.toLowerCase() === "audio") audioTokens = (audioTokens ?? 0) + tokens;
6020
+ }
6021
+ return audioTokens !== void 0 && audioTokens <= totalInputTokens ? audioTokens : void 0;
6022
+ }
5542
6023
  function normalizeInteractionUsage(usage) {
5543
6024
  if (!usage || !isValidTokenCount2(usage.total_input_tokens) || !isValidTokenCount2(usage.total_output_tokens) || !isValidTokenCount2(usage.total_tokens)) {
5544
6025
  return void 0;
5545
6026
  }
5546
6027
  const cached = usage.total_cached_tokens;
5547
6028
  const thinking = usage.total_thought_tokens;
5548
- if (cached !== void 0 && (!isValidTokenCount2(cached) || cached > usage.total_input_tokens) || thinking !== void 0 && !isValidTokenCount2(thinking))
6029
+ const toolUse = usage.total_tool_use_tokens;
6030
+ if (cached !== void 0 && (!isValidTokenCount2(cached) || cached > usage.total_input_tokens) || thinking !== void 0 && !isValidTokenCount2(thinking) || toolUse !== void 0 && !isValidTokenCount2(toolUse))
5549
6031
  return void 0;
6032
+ const inputTokens = usage.total_input_tokens;
6033
+ const audioInputTokens = reportedGeminiAudioTokens(usage.input_tokens_by_modality, inputTokens);
6034
+ const modalities = foldGeminiModalities(usage.input_tokens_by_modality, inputTokens);
6035
+ const billedOutputTokens = safeTokenSum2(usage.total_output_tokens, thinking ?? 0);
6036
+ const reportedTotal = safeTokenSum2(
6037
+ inputTokens,
6038
+ usage.total_output_tokens,
6039
+ thinking ?? 0,
6040
+ toolUse ?? 0
6041
+ );
6042
+ const reconciles = reportedTotal !== void 0 && usage.total_tokens === reportedTotal;
5550
6043
  return {
5551
- prompt_tokens: usage.total_input_tokens,
5552
- completion_tokens: usage.total_output_tokens,
5553
- total_tokens: usage.total_tokens,
5554
- ...cached ? { cached_tokens: cached } : {},
5555
- ...thinking ? { reasoning_tokens: thinking } : {}
6044
+ usage: {
6045
+ prompt_tokens: inputTokens,
6046
+ completion_tokens: usage.total_output_tokens,
6047
+ total_tokens: usage.total_tokens,
6048
+ ...cached ? { cached_tokens: cached } : {},
6049
+ ...thinking ? { reasoning_tokens: thinking } : {},
6050
+ ...audioInputTokens !== void 0 ? { audio_input_tokens: audioInputTokens } : {}
6051
+ },
6052
+ ...modalities !== void 0 && billedOutputTokens !== void 0 && reconciles ? {
6053
+ pricingUsage: {
6054
+ inputTokens,
6055
+ outputTokens: billedOutputTokens,
6056
+ cachedTokens: cached ?? 0,
6057
+ modalities
6058
+ }
6059
+ } : {},
6060
+ hasUnmodeledBilledUsage: toolUse !== void 0 && toolUse !== 0
5556
6061
  };
5557
6062
  }
5558
6063
 
@@ -5572,6 +6077,14 @@ var OPENROUTER_PROFILE = {
5572
6077
  capture: "reasoning_details",
5573
6078
  roundTrip: "on-tool-call-turns"
5574
6079
  },
6080
+ capabilities: {
6081
+ // Images: `image_url` data URLs or remote URLs. Audio: `input_audio`, which
6082
+ // OpenRouter documents as base64-only — bytes are encoded by the engine.
6083
+ inputModalities: {
6084
+ image: { sources: ["url", "bytes", "base64"] },
6085
+ audio: { sources: ["bytes", "base64"], formats: OPENROUTER_AUDIO_FORMATS }
6086
+ }
6087
+ },
5575
6088
  // OpenRouter normalizes to max_tokens across its backends.
5576
6089
  maxTokensField: "max_tokens"
5577
6090
  };
@@ -7067,87 +7580,98 @@ function validBase64(value) {
7067
7580
  function decodedBase64Bytes(value) {
7068
7581
  return Math.floor(value.length * 3 / 4) - (value.endsWith("==") ? 2 : value.endsWith("=") ? 1 : 0);
7069
7582
  }
7583
+ 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";
7584
+ var IMAGE_SOURCE_TYPES = ["url", "bytes", "base64", "provider-file"];
7585
+ var AUDIO_SOURCE_TYPES = ["bytes", "base64", "provider-file"];
7586
+ function cloneMediaSource(source, index, allowed, reserveInlineBytes) {
7587
+ if (!source || typeof source !== "object") invalid(`part ${index}.source must be an object`);
7588
+ const raw = source;
7589
+ const kind = raw.type;
7590
+ if (typeof kind !== "string" || !allowed.includes(kind))
7591
+ invalid(`part ${index}.source.type is unsupported`);
7592
+ let clone;
7593
+ switch (kind) {
7594
+ case "url": {
7595
+ const url = nonEmptyString2(raw.url, `part ${index}.source.url`);
7596
+ let parsed;
7597
+ try {
7598
+ parsed = new URL(url);
7599
+ } catch {
7600
+ invalid(`part ${index}.source.url must be an http(s) URL`);
7601
+ }
7602
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:")
7603
+ invalid(`part ${index}.source.url must be an http(s) URL`);
7604
+ clone = {
7605
+ type: "url",
7606
+ url,
7607
+ ...raw.mediaType === void 0 ? {} : { mediaType: mediaType(raw.mediaType, `part ${index}.source.mediaType`) }
7608
+ };
7609
+ break;
7610
+ }
7611
+ case "bytes": {
7612
+ if (!(raw.data instanceof Uint8Array) || raw.data.byteLength === 0)
7613
+ invalid(`part ${index}.source.data must be a non-empty Uint8Array`);
7614
+ reserveInlineBytes(raw.data.byteLength);
7615
+ clone = {
7616
+ type: "bytes",
7617
+ data: new Uint8Array(raw.data),
7618
+ mediaType: mediaType(raw.mediaType, `part ${index}.source.mediaType`)
7619
+ };
7620
+ break;
7621
+ }
7622
+ case "base64": {
7623
+ const data = nonEmptyString2(raw.data, `part ${index}.source.data`);
7624
+ if (data.length > 4 * Math.ceil(MAX_INLINE_MODEL_INPUT_BYTES / 3))
7625
+ invalid(INLINE_MEDIA_LIMIT_MESSAGE);
7626
+ if (!validBase64(data)) invalid(`part ${index}.source.data must be valid base64`);
7627
+ reserveInlineBytes(decodedBase64Bytes(data));
7628
+ clone = {
7629
+ type: "base64",
7630
+ data,
7631
+ mediaType: mediaType(raw.mediaType, `part ${index}.source.mediaType`)
7632
+ };
7633
+ break;
7634
+ }
7635
+ default:
7636
+ clone = {
7637
+ type: "provider-file",
7638
+ provider: nonEmptyString2(raw.provider, `part ${index}.source.provider`),
7639
+ reference: nonEmptyString2(raw.reference, `part ${index}.source.reference`),
7640
+ ...raw.mediaType === void 0 ? {} : { mediaType: mediaType(raw.mediaType, `part ${index}.source.mediaType`) }
7641
+ };
7642
+ }
7643
+ return clone;
7644
+ }
7070
7645
  function normalizeModelInput(input) {
7071
7646
  if (typeof input === "string") return input;
7072
7647
  if (!Array.isArray(input) || input.length === 0)
7073
7648
  invalid("ModelInput parts must be a non-empty array");
7074
7649
  let inlineBytes = 0;
7075
7650
  const reserveInlineBytes = (bytes) => {
7076
- if (bytes > MAX_INLINE_MODEL_INPUT_BYTES - inlineBytes) {
7077
- invalid(
7078
- "Inline image data must not exceed 25 MiB total; use a URL or provider-file source where supported"
7079
- );
7080
- }
7651
+ if (bytes > MAX_INLINE_MODEL_INPUT_BYTES - inlineBytes) invalid(INLINE_MEDIA_LIMIT_MESSAGE);
7081
7652
  inlineBytes += bytes;
7082
7653
  };
7083
7654
  return input.map((part, index) => {
7084
7655
  if (!part || typeof part !== "object") invalid(`ModelInput part ${index} must be an object`);
7085
- if (part.type === "text")
7086
- return { type: "text", text: nonEmptyString2(part.text, `part ${index}.text`) };
7087
- if (part.type !== "image") invalid(`part ${index}.type must be 'text' or 'image'`);
7088
- const source = part.source;
7089
- if (!source || typeof source !== "object") invalid(`part ${index}.source must be an object`);
7090
- let clone;
7091
- switch (source.type) {
7092
- case "url": {
7093
- const url = nonEmptyString2(source.url, `part ${index}.source.url`);
7094
- let parsed;
7095
- try {
7096
- parsed = new URL(url);
7097
- } catch {
7098
- invalid(`part ${index}.source.url must be an http(s) URL`);
7099
- }
7100
- if (parsed.protocol !== "http:" && parsed.protocol !== "https:")
7101
- invalid(`part ${index}.source.url must be an http(s) URL`);
7102
- clone = {
7103
- type: "url",
7104
- url,
7105
- ...source.mediaType === void 0 ? {} : { mediaType: mediaType(source.mediaType, `part ${index}.source.mediaType`) }
7106
- };
7107
- break;
7108
- }
7109
- case "bytes":
7110
- if (!(source.data instanceof Uint8Array) || source.data.byteLength === 0)
7111
- invalid(`part ${index}.source.data must be a non-empty Uint8Array`);
7112
- reserveInlineBytes(source.data.byteLength);
7113
- clone = {
7114
- type: "bytes",
7115
- data: source.data.slice(),
7116
- mediaType: mediaType(source.mediaType, `part ${index}.source.mediaType`)
7117
- };
7118
- break;
7119
- case "base64": {
7120
- const data = nonEmptyString2(source.data, `part ${index}.source.data`);
7121
- if (data.length > 4 * Math.ceil(MAX_INLINE_MODEL_INPUT_BYTES / 3)) {
7122
- invalid(
7123
- "Inline image data must not exceed 25 MiB total; use a URL or provider-file source where supported"
7124
- );
7125
- }
7126
- if (!validBase64(data)) invalid(`part ${index}.source.data must be valid base64`);
7127
- reserveInlineBytes(decodedBase64Bytes(data));
7128
- clone = {
7129
- type: "base64",
7130
- data,
7131
- mediaType: mediaType(source.mediaType, `part ${index}.source.mediaType`)
7656
+ const mediaLabel = () => part.label === void 0 ? {} : { label: nonEmptyString2(part.label, `part ${index}.label`) };
7657
+ switch (part.type) {
7658
+ case "text":
7659
+ return { type: "text", text: nonEmptyString2(part.text, `part ${index}.text`) };
7660
+ case "image":
7661
+ return {
7662
+ type: "image",
7663
+ source: cloneMediaSource(part.source, index, IMAGE_SOURCE_TYPES, reserveInlineBytes),
7664
+ ...mediaLabel()
7132
7665
  };
7133
- break;
7134
- }
7135
- case "provider-file":
7136
- clone = {
7137
- type: "provider-file",
7138
- provider: nonEmptyString2(source.provider, `part ${index}.source.provider`),
7139
- reference: nonEmptyString2(source.reference, `part ${index}.source.reference`),
7140
- ...source.mediaType === void 0 ? {} : { mediaType: mediaType(source.mediaType, `part ${index}.source.mediaType`) }
7666
+ case "audio":
7667
+ return {
7668
+ type: "audio",
7669
+ source: cloneMediaSource(part.source, index, AUDIO_SOURCE_TYPES, reserveInlineBytes),
7670
+ ...mediaLabel()
7141
7671
  };
7142
- break;
7143
7672
  default:
7144
- invalid(`part ${index}.source.type is unsupported`);
7673
+ invalid(`part ${index}.type must be 'text', 'image', or 'audio'`);
7145
7674
  }
7146
- return {
7147
- type: "image",
7148
- source: clone,
7149
- ...part.label === void 0 ? {} : { label: nonEmptyString2(part.label, `part ${index}.label`) }
7150
- };
7151
7675
  });
7152
7676
  }
7153
7677
  function inputText(input) {
@@ -7156,33 +7680,54 @@ function inputText(input) {
7156
7680
  function cloneModelInput(input) {
7157
7681
  return typeof input === "string" ? input : normalizeModelInput(input);
7158
7682
  }
7683
+ function unsupportedPart(part) {
7684
+ return invalid(
7685
+ `Unsupported model input part type '${String(part.type)}'`
7686
+ );
7687
+ }
7688
+ function describeSource(source, label) {
7689
+ const media = source;
7690
+ const locator = media.type === "url" ? media.url : media.type === "provider-file" ? media.reference : void 0;
7691
+ return {
7692
+ source: source.type,
7693
+ ...media.mediaType ? { mediaType: media.mediaType } : {},
7694
+ ...media.type === "bytes" ? { bytes: media.data.byteLength } : {},
7695
+ ...media.type === "base64" ? { bytes: decodedBase64Bytes(media.data) } : {},
7696
+ ...locator ? { locator } : {},
7697
+ ...label ? { label } : {}
7698
+ };
7699
+ }
7159
7700
  function describeModelInput(input) {
7160
7701
  if (typeof input === "string") return void 0;
7161
7702
  return {
7162
7703
  parts: input.map((part) => {
7163
- if (part.type === "text") return { type: "text", characters: part.text.length };
7164
- const { source } = part;
7165
- return {
7166
- type: "image",
7167
- source: source.type,
7168
- ...source.mediaType ? { mediaType: source.mediaType } : {},
7169
- ...source.type === "bytes" ? { bytes: source.data.byteLength } : {},
7170
- ...source.type === "base64" ? {
7171
- bytes: decodedBase64Bytes(source.data)
7172
- } : {},
7173
- ...(source.type === "url" ? source.url : source.type === "provider-file" ? source.reference : void 0) ? {
7174
- locator: source.type === "url" ? source.url : source.type === "provider-file" ? source.reference : void 0
7175
- } : {},
7176
- ...part.label ? { label: part.label } : {}
7177
- };
7704
+ switch (part.type) {
7705
+ case "text":
7706
+ return { type: "text", characters: part.text.length };
7707
+ case "image":
7708
+ return { type: "image", ...describeSource(part.source, part.label) };
7709
+ case "audio":
7710
+ return { type: "audio", ...describeSource(part.source, part.label) };
7711
+ default:
7712
+ return unsupportedPart(part);
7713
+ }
7178
7714
  })
7179
7715
  };
7180
7716
  }
7181
7717
  function summarizeModelInput(input) {
7182
7718
  if (typeof input === "string") return input;
7183
- return input.map(
7184
- (part) => part.type === "text" ? part.text : `[image ${part.source.mediaType ?? "media"}]`
7185
- ).join("\n");
7719
+ return input.map((part) => {
7720
+ switch (part.type) {
7721
+ case "text":
7722
+ return part.text;
7723
+ case "image":
7724
+ return `[image ${part.source.mediaType ?? "media"}]`;
7725
+ case "audio":
7726
+ return `[audio ${part.source.mediaType ?? "media"}]`;
7727
+ default:
7728
+ return unsupportedPart(part);
7729
+ }
7730
+ }).join("\n");
7186
7731
  }
7187
7732
  function sanitizeModelInputForTrace(input) {
7188
7733
  return inputText(input);
@@ -7196,15 +7741,86 @@ function normalizePersistedSessionHistory(history) {
7196
7741
  );
7197
7742
  }
7198
7743
  const content = normalizeModelInput(message.content);
7199
- if (typeof content !== "string" && content.some((part) => part.type === "image" && part.source.type === "bytes")) {
7744
+ if (typeof content !== "string" && content.some((part) => part.type !== "text" && part.source.type === "bytes")) {
7200
7745
  throw new InvalidModelInputError(
7201
- "Uint8Array image input cannot be persisted in session history"
7746
+ "Uint8Array media input cannot be persisted in session history"
7202
7747
  );
7203
7748
  }
7204
7749
  return { ...message, content };
7205
7750
  });
7206
7751
  }
7207
7752
 
7753
+ // src/session-input.ts
7754
+ var MODEL_INPUT_PART_TYPES = /* @__PURE__ */ new Set(["text", "image", "audio"]);
7755
+ var sessionInputMarkers = /* @__PURE__ */ new WeakMap();
7756
+ function isModelInputParts(input) {
7757
+ return Array.isArray(input) && input.length > 0 && input.every(
7758
+ (part) => typeof part === "object" && part !== null && MODEL_INPUT_PART_TYPES.has(part.type)
7759
+ );
7760
+ }
7761
+ function prepareSessionInput(input) {
7762
+ if (typeof input === "string") return { content: input, normalizedInput: input };
7763
+ if (isModelInputParts(input)) {
7764
+ const normalizedInput = normalizeModelInput(input);
7765
+ return { content: summarizeModelInput(normalizedInput), normalizedInput };
7766
+ }
7767
+ const content = JSON.stringify(input);
7768
+ return {
7769
+ content,
7770
+ ...typeof content === "string" ? { normalizedInput: content } : {}
7771
+ };
7772
+ }
7773
+ function registerSessionInput(history, message, normalizedInput) {
7774
+ sessionInputMarkers.set(history, {
7775
+ message,
7776
+ content: message.content,
7777
+ normalizedInput: cloneModelInput(normalizedInput)
7778
+ });
7779
+ }
7780
+ function clearSessionInput(history) {
7781
+ sessionInputMarkers.delete(history);
7782
+ }
7783
+ function equalBytes(left, right) {
7784
+ if (left.byteLength !== right.byteLength) return false;
7785
+ for (let index = 0; index < left.byteLength; index++) {
7786
+ if (left[index] !== right[index]) return false;
7787
+ }
7788
+ return true;
7789
+ }
7790
+ function equalSource(left, right) {
7791
+ if (left.type !== right.type) return false;
7792
+ switch (left.type) {
7793
+ case "url":
7794
+ return right.type === "url" && left.url === right.url && left.mediaType === right.mediaType;
7795
+ case "bytes":
7796
+ return right.type === "bytes" && left.mediaType === right.mediaType && equalBytes(left.data, right.data);
7797
+ case "base64":
7798
+ return right.type === "base64" && left.data === right.data && left.mediaType === right.mediaType;
7799
+ case "provider-file":
7800
+ return right.type === "provider-file" && left.provider === right.provider && left.reference === right.reference && left.mediaType === right.mediaType;
7801
+ }
7802
+ }
7803
+ function equalPart(left, right) {
7804
+ if (left.type !== right.type) return false;
7805
+ if (left.type === "text") return right.type === "text" && left.text === right.text;
7806
+ return right.type === left.type && left.label === right.label && equalSource(left.source, right.source);
7807
+ }
7808
+ function equalModelInput(left, right) {
7809
+ if (typeof left === "string" || typeof right === "string") return left === right;
7810
+ return left.length === right.length && left.every((part, index) => equalPart(part, right[index]));
7811
+ }
7812
+ function sessionHistoryForAsk(history, normalizedInput) {
7813
+ const snapshot = history.slice();
7814
+ const marker = sessionInputMarkers.get(history);
7815
+ if (!marker) return snapshot;
7816
+ const last = snapshot[snapshot.length - 1];
7817
+ if (last !== marker.message || last.role !== "user" || last.content !== marker.content || !equalModelInput(marker.normalizedInput, normalizedInput)) {
7818
+ return snapshot;
7819
+ }
7820
+ snapshot.pop();
7821
+ return snapshot;
7822
+ }
7823
+
7208
7824
  // src/partial-json.ts
7209
7825
  var MAX_DEPTH = 256;
7210
7826
  function parsePartialJson(input) {
@@ -10003,7 +10619,7 @@ function estimateMessagesTokens(messages) {
10003
10619
  for (const msg of messages) {
10004
10620
  total += estimateTokens(inputText(msg.content));
10005
10621
  if (typeof msg.content !== "string") {
10006
- unmeasured ||= msg.content.some((part) => part.type === "image");
10622
+ unmeasured ||= msg.content.some((part) => part.type !== "text");
10007
10623
  }
10008
10624
  if (msg.tool_calls) {
10009
10625
  for (const tc of msg.tool_calls) {
@@ -10049,10 +10665,6 @@ function normalizeSessionHistory(messages) {
10049
10665
  function hasRichMessage(messages) {
10050
10666
  return messages.some((message) => typeof message.content !== "string");
10051
10667
  }
10052
- function firstImageSource(input, history) {
10053
- const find = (value) => typeof value === "string" ? void 0 : value.find((part) => part.type === "image")?.source.type;
10054
- return find(input) ?? history.map((message) => find(message.content)).find(Boolean);
10055
- }
10056
10668
  function appendHandoffInstruction(input, originalText, instruction) {
10057
10669
  if (typeof input === "string" || instruction.length === 0 || instruction === originalText)
10058
10670
  return input;
@@ -10610,7 +11222,8 @@ var WorkflowContext = class _WorkflowContext {
10610
11222
  // ── ctx.ask() ─────────────────────────────────────────────────────────
10611
11223
  async ask(agent2, prompt, options) {
10612
11224
  const normalizedInput = normalizeModelInput(prompt);
10613
- const normalizedHistory = normalizeSessionHistory(this.sessionHistory);
11225
+ const sessionHistorySnapshot = sessionHistoryForAsk(this.sessionHistory, normalizedInput);
11226
+ const normalizedHistory = normalizeSessionHistory(sessionHistorySnapshot);
10614
11227
  const delegateInputHolder = agent2;
10615
11228
  if (delegateInputHolder._delegateOriginalInput !== void 0) {
10616
11229
  delegateInputHolder._delegateOriginalInput = normalizeModelInput(
@@ -10820,20 +11433,31 @@ var WorkflowContext = class _WorkflowContext {
10820
11433
  const providerOptions = options?.providerOptions ?? agent2._config.providerOptions;
10821
11434
  const richRequest = typeof input !== "string" || hasRichMessage(sessionHistory);
10822
11435
  if (richRequest) {
11436
+ const richPart = firstRichPart(input, sessionHistory);
11437
+ const modality = richPart?.type ?? "image";
11438
+ const source = richPart?.source.type;
10823
11439
  if (providerOptions && ("messages" in providerOptions || "input" in providerOptions)) {
10824
11440
  throw new UnsupportedModelInputError({
10825
11441
  provider: provider.name ?? modelUri.split(":", 1)[0],
10826
11442
  model,
10827
- modality: "image",
11443
+ modality,
10828
11444
  feature: "raw input-container providerOptions"
10829
11445
  });
10830
11446
  }
10831
- const source = firstImageSource(input, sessionHistory);
11447
+ const audioPart = firstRichPart(input, sessionHistory, "audio");
11448
+ if (audioPart && !provider.inputCapabilities?.(model)?.audio) {
11449
+ throw new UnsupportedModelInputError({
11450
+ provider: provider.name ?? modelUri.split(":", 1)[0],
11451
+ model,
11452
+ modality: "audio",
11453
+ source: audioPart.source.type
11454
+ });
11455
+ }
10832
11456
  if (!provider.validateInput) {
10833
11457
  throw new UnsupportedModelInputError({
10834
11458
  provider: provider.name ?? modelUri.split(":", 1)[0],
10835
11459
  model,
10836
- modality: "image",
11460
+ modality,
10837
11461
  ...source ? { source } : {}
10838
11462
  });
10839
11463
  }
@@ -10852,10 +11476,20 @@ var WorkflowContext = class _WorkflowContext {
10852
11476
  throw new UnsupportedModelInputError({
10853
11477
  provider: provider.name ?? modelUri.split(":", 1)[0],
10854
11478
  model,
10855
- modality: "image",
11479
+ modality,
10856
11480
  ...source ? { source } : {}
10857
11481
  });
10858
11482
  }
11483
+ if (audioPart && validation.effectiveModel !== model) {
11484
+ if (!provider.inputCapabilities?.(validation.effectiveModel)?.audio) {
11485
+ throw new UnsupportedModelInputError({
11486
+ provider: provider.name ?? modelUri.split(":", 1)[0],
11487
+ model: validation.effectiveModel,
11488
+ modality: "audio",
11489
+ source: audioPart.source.type
11490
+ });
11491
+ }
11492
+ }
10859
11493
  model = validation.effectiveModel;
10860
11494
  }
10861
11495
  const effectiveModelUri = !richRequest ? modelUri : `${modelUri.slice(0, modelUri.indexOf(":") + 1)}${model}`;
@@ -10868,25 +11502,17 @@ var WorkflowContext = class _WorkflowContext {
10868
11502
  (descriptor) => descriptor !== void 0
10869
11503
  );
10870
11504
  const parts = descriptors.flatMap((descriptor) => descriptor.parts);
11505
+ const media = parts.filter((part) => part.type !== "text");
11506
+ const withSource = (source) => media.filter((part) => part.source === source).length;
10871
11507
  this.spanManager?.addEventToActiveSpan("axl.model_input", {
10872
11508
  "axl.input.parts": parts.length,
10873
11509
  "axl.input.images": parts.filter((part) => part.type === "image").length,
10874
- "axl.input.source.bytes": parts.filter(
10875
- (part) => part.type === "image" && part.source === "bytes"
10876
- ).length,
10877
- "axl.input.source.base64": parts.filter(
10878
- (part) => part.type === "image" && part.source === "base64"
10879
- ).length,
10880
- "axl.input.source.url": parts.filter(
10881
- (part) => part.type === "image" && part.source === "url"
10882
- ).length,
10883
- "axl.input.source.provider_file": parts.filter(
10884
- (part) => part.type === "image" && part.source === "provider-file"
10885
- ).length,
10886
- "axl.input.inline_bytes": parts.reduce(
10887
- (sum, part) => sum + (part.type === "image" ? part.bytes ?? 0 : 0),
10888
- 0
10889
- )
11510
+ "axl.input.audio": parts.filter((part) => part.type === "audio").length,
11511
+ "axl.input.source.bytes": withSource("bytes"),
11512
+ "axl.input.source.base64": withSource("base64"),
11513
+ "axl.input.source.url": withSource("url"),
11514
+ "axl.input.source.provider_file": withSource("provider-file"),
11515
+ "axl.input.inline_bytes": media.reduce((sum, part) => sum + (part.bytes ?? 0), 0)
10890
11516
  });
10891
11517
  }
10892
11518
  assertRichMessagesAreUserOnly(sessionHistory);
@@ -13453,6 +14079,11 @@ ${summaryResponse.content}`
13453
14079
  }
13454
14080
  });
13455
14081
  const routerInput = options?.routerInput === "text" ? inputText(prompt) : prompt;
14082
+ if (routerInput === "") {
14083
+ throw new InvalidModelInputError(
14084
+ "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"
14085
+ );
14086
+ }
13456
14087
  routerAgent._delegateOriginalInput = prompt;
13457
14088
  return this.ask(routerAgent, routerInput, {
13458
14089
  schema: options?.schema,
@@ -14549,9 +15180,13 @@ var Session = class _Session {
14549
15180
  }
14550
15181
  async sendImpl(workflowName, input, signal, events) {
14551
15182
  const { history, metadata } = await this.prepareHistory(input);
14552
- const result = await this.runtime.execute(workflowName, input, { metadata, signal, events });
14553
- await this.commitHistory(history, result);
14554
- return result;
15183
+ try {
15184
+ const result = await this.runtime.execute(workflowName, input, { metadata, signal, events });
15185
+ await this.commitHistory(history, result);
15186
+ return result;
15187
+ } finally {
15188
+ clearSessionInput(history);
15189
+ }
14555
15190
  }
14556
15191
  /** Read history + summary, apply maxMessages limit (with optional
14557
15192
  * summarization), and push the new user message. Returns the mutable
@@ -14561,6 +15196,7 @@ var Session = class _Session {
14561
15196
  * `streamImpl` so future fixes (e.g., to summarization wiring) land in
14562
15197
  * one place. */
14563
15198
  async prepareHistory(input) {
15199
+ const preparedInput = prepareSessionInput(input);
14564
15200
  const history = await this.store.getSession(this.sessionId);
14565
15201
  let cachedSummary = await this.store.getSessionMeta(this.sessionId, "summaryCache");
14566
15202
  const maxMessages = this.options.history?.maxMessages;
@@ -14583,10 +15219,11 @@ var Session = class _Session {
14583
15219
  history.length = 0;
14584
15220
  history.push(...trimmed);
14585
15221
  }
14586
- history.push({
14587
- role: "user",
14588
- content: typeof input === "string" ? input : JSON.stringify(input)
14589
- });
15222
+ const userMessage = { role: "user", content: preparedInput.content };
15223
+ history.push(userMessage);
15224
+ if (this.options.deduplicateInput !== false && preparedInput.normalizedInput !== void 0) {
15225
+ registerSessionInput(history, userMessage, preparedInput.normalizedInput);
15226
+ }
14590
15227
  const metadata = {
14591
15228
  sessionId: this.sessionId,
14592
15229
  sessionHistory: history,
@@ -14645,21 +15282,26 @@ var Session = class _Session {
14645
15282
  events
14646
15283
  });
14647
15284
  } catch (err) {
15285
+ if (history) clearSessionInput(history);
14648
15286
  rejectReady(err);
14649
15287
  throw err;
14650
15288
  }
14651
15289
  resolveReady(axlStream);
14652
- const completion = axlStream.promise.then(
14653
- (result) => this.commitHistory(history, result).catch((err) => {
14654
- this.runtime.emit("error", {
14655
- type: "session_history_save_failed",
14656
- sessionId: this.sessionId,
14657
- error: err instanceof Error ? err.message : String(err)
14658
- });
14659
- }),
14660
- () => void 0
14661
- );
14662
- await completion;
15290
+ try {
15291
+ const completion = axlStream.promise.then(
15292
+ (result) => this.commitHistory(history, result).catch((err) => {
15293
+ this.runtime.emit("error", {
15294
+ type: "session_history_save_failed",
15295
+ sessionId: this.sessionId,
15296
+ error: err instanceof Error ? err.message : String(err)
15297
+ });
15298
+ }),
15299
+ () => void 0
15300
+ );
15301
+ await completion;
15302
+ } finally {
15303
+ clearSessionInput(history);
15304
+ }
14663
15305
  }
14664
15306
  async history() {
14665
15307
  return this.store.getSession(this.sessionId);
@@ -18274,6 +18916,7 @@ function cosineSimilarity2(a, b) {
18274
18916
  resolveRuntime,
18275
18917
  resolveThinkingOptions,
18276
18918
  stringStreamFromEvents,
18919
+ summarizeModelInput,
18277
18920
  tool,
18278
18921
  workflow,
18279
18922
  zodToJsonSchema