@axlsdk/axl 0.21.0 → 0.22.0

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
@@ -130,10 +130,15 @@ __export(index_exports, {
130
130
  EventStreamOverflowError: () => EventStreamOverflowError,
131
131
  GROQ_PROFILE: () => GROQ_PROFILE,
132
132
  GeminiProvider: () => GeminiProvider,
133
+ GeminiTranscriptionProvider: () => GeminiTranscriptionProvider,
133
134
  GuardrailError: () => GuardrailError,
134
135
  InMemoryVectorStore: () => InMemoryVectorStore,
136
+ InvalidModelInputError: () => InvalidModelInputError,
137
+ InvalidTranscriptionInputError: () => InvalidTranscriptionInputError,
135
138
  LLAMACPP_PROFILE: () => LLAMACPP_PROFILE,
136
139
  LMSTUDIO_PROFILE: () => LMSTUDIO_PROFILE,
140
+ MAX_INLINE_MODEL_INPUT_BYTES: () => MAX_INLINE_MODEL_INPUT_BYTES,
141
+ MAX_INLINE_TRANSCRIPTION_BYTES: () => MAX_INLINE_TRANSCRIPTION_BYTES,
137
142
  MISTRAL_PROFILE: () => MISTRAL_PROFILE,
138
143
  MaxTurnsError: () => MaxTurnsError,
139
144
  McpManager: () => McpManager,
@@ -149,6 +154,8 @@ __export(index_exports, {
149
154
  OpenAIEmbedder: () => OpenAIEmbedder,
150
155
  OpenAIProvider: () => OpenAIProvider,
151
156
  OpenAIResponsesProvider: () => OpenAIResponsesProvider,
157
+ OpenAITranscriptionProvider: () => OpenAITranscriptionProvider,
158
+ OpenRouterTranscriptionProvider: () => OpenRouterTranscriptionProvider,
152
159
  ProviderError: () => ProviderError,
153
160
  ProviderRegistry: () => ProviderRegistry,
154
161
  QuorumNotMet: () => QuorumNotMet,
@@ -164,7 +171,11 @@ __export(index_exports, {
164
171
  TimeoutError: () => TimeoutError,
165
172
  ToolFailure: () => ToolFailure,
166
173
  ToolModelOutputError: () => ToolModelOutputError,
174
+ TranscriptionOperationError: () => TranscriptionOperationError,
175
+ TranscriptionProviderRegistry: () => TranscriptionProviderRegistry,
167
176
  UnsupportedEventSchemaVersionError: () => UnsupportedEventSchemaVersionError,
177
+ UnsupportedModelInputError: () => UnsupportedModelInputError,
178
+ UnsupportedTranscriptionInputError: () => UnsupportedTranscriptionInputError,
168
179
  VLLM_PROFILE: () => VLLM_PROFILE,
169
180
  ValidationError: () => ValidationError,
170
181
  VerifyError: () => VerifyError,
@@ -181,8 +192,10 @@ __export(index_exports, {
181
192
  findConfig: () => findConfig,
182
193
  getEventSchemaVersion: () => getEventSchemaVersion,
183
194
  getExecutionEventSchemaVersion: () => getExecutionEventSchemaVersion,
195
+ hasPositiveBillableWork: () => hasPositiveBillableWork,
184
196
  hasPositiveTokens: () => hasPositiveTokens,
185
197
  importModule: () => importModule,
198
+ inputText: () => inputText,
186
199
  isCostBearingLeaf: () => isCostBearingLeaf,
187
200
  isRetryableStatus: () => isRetryableStatus,
188
201
  isRootLevel: () => isRootLevel,
@@ -223,6 +236,75 @@ var AxlError = class extends Error {
223
236
  this.code = code;
224
237
  }
225
238
  };
239
+ var InvalidModelInputError = class extends AxlError {
240
+ constructor(message) {
241
+ super("INVALID_MODEL_INPUT", message);
242
+ this.name = "InvalidModelInputError";
243
+ }
244
+ };
245
+ var UnsupportedModelInputError = class extends AxlError {
246
+ provider;
247
+ model;
248
+ modality;
249
+ source;
250
+ constructor(options) {
251
+ super(
252
+ "UNSUPPORTED_MODEL_INPUT",
253
+ `Provider '${options.provider}' model '${options.model}' does not support ${options.feature ?? options.modality}${options.source ? ` from ${options.source}` : ""}`
254
+ );
255
+ this.name = "UnsupportedModelInputError";
256
+ this.provider = options.provider;
257
+ this.model = options.model;
258
+ this.modality = options.modality;
259
+ this.source = options.source;
260
+ }
261
+ };
262
+ var InvalidTranscriptionInputError = class extends AxlError {
263
+ constructor(message) {
264
+ super("INVALID_TRANSCRIPTION_INPUT", message);
265
+ this.name = "InvalidTranscriptionInputError";
266
+ }
267
+ };
268
+ var UnsupportedTranscriptionInputError = class extends AxlError {
269
+ provider;
270
+ model;
271
+ constructor(options) {
272
+ super(
273
+ "UNSUPPORTED_TRANSCRIPTION_INPUT",
274
+ `Transcription provider '${options.provider}'${options.model ? ` model '${options.model}'` : ""} does not support ${options.feature ?? "this request"}`
275
+ );
276
+ this.name = "UnsupportedTranscriptionInputError";
277
+ this.provider = options.provider;
278
+ this.model = options.model;
279
+ }
280
+ };
281
+ var TranscriptionOperationError = class extends AxlError {
282
+ provider;
283
+ model;
284
+ usage;
285
+ pricingStatus;
286
+ cleanupStatus;
287
+ /** Safe HTTP diagnostics projected from ProviderError when available. */
288
+ status;
289
+ retryable;
290
+ retryAfterMs;
291
+ requestId;
292
+ constructor(options) {
293
+ super("TRANSCRIPTION_PROVIDER_ERROR", "Transcription provider operation failed");
294
+ this.name = "TranscriptionOperationError";
295
+ this.provider = options.provider;
296
+ this.model = options.model;
297
+ this.usage = options.usage;
298
+ this.pricingStatus = options.pricingStatus;
299
+ this.cleanupStatus = options.cleanupStatus;
300
+ this.status = options.status;
301
+ this.retryable = options.retryable;
302
+ this.retryAfterMs = options.retryAfterMs;
303
+ this.requestId = options.requestId;
304
+ if (options.cause !== void 0)
305
+ Object.defineProperty(this, "cause", { enumerable: false, value: options.cause });
306
+ }
307
+ };
226
308
  var EventStreamOverflowError = class extends Error {
227
309
  maxQueued;
228
310
  eventType;
@@ -380,6 +462,221 @@ var ValidationError = class extends AxlError {
380
462
 
381
463
  // src/tool.ts
382
464
  var DEFAULT_MAX_STRING_LENGTH = 1e4;
465
+ var MAX_MODEL_ARGUMENT_ISSUES = 8;
466
+ var MAX_MODEL_ARGUMENT_MESSAGE_LENGTH = 2e3;
467
+ var MAX_MODEL_ARGUMENT_LINE_LENGTH = 220;
468
+ var MAX_MODEL_ARGUMENT_PATH_LENGTH = 160;
469
+ var MAX_UNION_DEPTH = 3;
470
+ var MAX_UNION_BRANCHES = 4;
471
+ var MAX_LITERAL_ALTERNATIVES = 10;
472
+ var SAFE_SCHEMA_TYPES = /* @__PURE__ */ new Set([
473
+ "string",
474
+ "number",
475
+ "boolean",
476
+ "null",
477
+ "array",
478
+ "object",
479
+ "integer"
480
+ ]);
481
+ var SAFE_STRING_FORMATS = /* @__PURE__ */ new Set([
482
+ "email",
483
+ "url",
484
+ "emoji",
485
+ "uuid",
486
+ "guid",
487
+ "nanoid",
488
+ "cuid",
489
+ "cuid2",
490
+ "ulid",
491
+ "xid",
492
+ "ksuid",
493
+ "datetime",
494
+ "date",
495
+ "time",
496
+ "duration",
497
+ "ipv4",
498
+ "ipv6",
499
+ "cidrv4",
500
+ "cidrv6",
501
+ "base64",
502
+ "base64url",
503
+ "json_string",
504
+ "e164",
505
+ "lowercase",
506
+ "uppercase",
507
+ "jwt"
508
+ ]);
509
+ function truncateModelFeedback(value, maximum) {
510
+ return value.length <= maximum ? value : `${value.slice(0, Math.max(0, maximum - 1))}\u2026`;
511
+ }
512
+ var DISPLAY_CONTROL_CHARACTERS = /[\u061C\u200B-\u200F\u202A-\u202E\u2066-\u2069\u2028\u2029]/g;
513
+ function isJsonSchema(value) {
514
+ return value !== null && typeof value === "object" && !Array.isArray(value);
515
+ }
516
+ function escapeDisplayControls(value) {
517
+ return value.replace(
518
+ DISPLAY_CONTROL_CHARACTERS,
519
+ (character) => `\\u${character.codePointAt(0).toString(16).padStart(4, "0")}`
520
+ );
521
+ }
522
+ function quoteModelText(value) {
523
+ return escapeDisplayControls(JSON.stringify(value));
524
+ }
525
+ function schemaNumber(value) {
526
+ return typeof value === "number" && Number.isFinite(value) ? String(value) : void 0;
527
+ }
528
+ function schemaLiteral(value) {
529
+ if (typeof value === "string") {
530
+ const rendered = quoteModelText(value);
531
+ return rendered.length <= 120 ? rendered : '"<literal omitted>"';
532
+ }
533
+ if (typeof value === "number") return Number.isFinite(value) ? String(value) : void 0;
534
+ if (typeof value === "boolean") return String(value);
535
+ if (value === null) return "null";
536
+ return void 0;
537
+ }
538
+ function schemaType(schema) {
539
+ const type = schema.type;
540
+ return typeof type === "string" && SAFE_SCHEMA_TYPES.has(type) ? type : void 0;
541
+ }
542
+ function compositionBranches(schema) {
543
+ const branches = [];
544
+ for (const key of ["anyOf", "oneOf", "allOf"]) {
545
+ const value = schema[key];
546
+ if (!Array.isArray(value)) continue;
547
+ for (const branch of value.slice(0, MAX_UNION_BRANCHES)) {
548
+ if (isJsonSchema(branch)) branches.push(branch);
549
+ }
550
+ }
551
+ return branches;
552
+ }
553
+ function unionBranches(schema) {
554
+ const branches = [];
555
+ for (const key of ["anyOf", "oneOf"]) {
556
+ const value = schema[key];
557
+ if (!Array.isArray(value)) continue;
558
+ for (const branch of value.slice(0, MAX_UNION_BRANCHES)) {
559
+ if (isJsonSchema(branch)) branches.push(branch);
560
+ }
561
+ }
562
+ return branches;
563
+ }
564
+ function resolvePathSegment(schema, segment, depth = 0) {
565
+ if (depth >= MAX_UNION_DEPTH) return void 0;
566
+ if (typeof segment === "string") {
567
+ const properties = schema.properties;
568
+ if (isJsonSchema(properties) && Object.hasOwn(properties, segment)) {
569
+ const child = properties[segment];
570
+ if (isJsonSchema(child)) return { node: child, renderedSegment: segment };
571
+ }
572
+ if (isJsonSchema(schema.additionalProperties)) {
573
+ return { node: schema.additionalProperties, renderedSegment: "<key>" };
574
+ }
575
+ } else if (typeof segment === "number" && Number.isSafeInteger(segment) && segment >= 0) {
576
+ const prefixItems = schema.prefixItems;
577
+ if (Array.isArray(prefixItems) && isJsonSchema(prefixItems[segment])) {
578
+ return { node: prefixItems[segment], renderedSegment: String(segment) };
579
+ }
580
+ if (isJsonSchema(schema.items)) {
581
+ return { node: schema.items, renderedSegment: "<index>" };
582
+ }
583
+ }
584
+ for (const branch of compositionBranches(schema)) {
585
+ const resolved = resolvePathSegment(branch, segment, depth + 1);
586
+ if (resolved !== void 0) return resolved;
587
+ }
588
+ return void 0;
589
+ }
590
+ function resolveSchemaPath(providerVisibleSchema, issuePath) {
591
+ if (!isJsonSchema(providerVisibleSchema)) return void 0;
592
+ let node = providerVisibleSchema;
593
+ const segments = [];
594
+ for (const segment of issuePath) {
595
+ const resolved = resolvePathSegment(node, segment);
596
+ if (resolved === void 0) {
597
+ segments.push("<path omitted>");
598
+ return { node, segments };
599
+ }
600
+ node = resolved.node;
601
+ segments.push(resolved.renderedSegment);
602
+ }
603
+ return { node, segments };
604
+ }
605
+ function jsonPointerPath(segments) {
606
+ if (segments.length === 0) return "<root>";
607
+ const pointer = `/${segments.map((segment) => segment.replaceAll("~", "~0").replaceAll("/", "~1")).join("/")}`;
608
+ const rendered = quoteModelText(pointer);
609
+ return rendered.length <= MAX_MODEL_ARGUMENT_PATH_LENGTH ? rendered : '"<path omitted>"';
610
+ }
611
+ function schemaAlternatives(schema, depth = 0) {
612
+ if (depth >= MAX_UNION_DEPTH) return [];
613
+ const constant = "const" in schema ? schemaLiteral(schema.const) : void 0;
614
+ if (constant !== void 0) return [constant];
615
+ if (Array.isArray(schema.enum)) {
616
+ const values = schema.enum.slice(0, MAX_LITERAL_ALTERNATIVES).map(schemaLiteral);
617
+ if (values.every((value) => value !== void 0)) return values;
618
+ }
619
+ const type = schemaType(schema);
620
+ if (type !== void 0) return [type];
621
+ const alternatives = unionBranches(schema).flatMap(
622
+ (branch) => schemaAlternatives(branch, depth + 1)
623
+ );
624
+ return alternatives.slice(0, MAX_LITERAL_ALTERNATIVES);
625
+ }
626
+ function schemaEnumExpectation(schema) {
627
+ if ("const" in schema) {
628
+ const constant = schemaLiteral(schema.const);
629
+ return constant === void 0 ? void 0 : `expected ${constant}`;
630
+ }
631
+ if (!Array.isArray(schema.enum)) return void 0;
632
+ const values = schema.enum.slice(0, MAX_LITERAL_ALTERNATIVES).map(schemaLiteral);
633
+ if (values.some((value) => value === void 0)) return void 0;
634
+ const rendered = values;
635
+ if (rendered.length === 0) return void 0;
636
+ const omission = schema.enum.length > rendered.length ? ", \u2026" : "";
637
+ return `expected ${rendered.length === 1 ? rendered[0] : `one of ${rendered.join(", ")}`}${omission}`;
638
+ }
639
+ function schemaTypeExpectation(schema) {
640
+ const type = schemaType(schema);
641
+ if (type !== void 0) return `expected ${type}`;
642
+ const alternatives = schemaAlternatives(schema);
643
+ return alternatives.length === 0 ? void 0 : `expected one of: ${alternatives.join(", ")}`;
644
+ }
645
+ function schemaSizeExpectation(schema, direction) {
646
+ const type = schemaType(schema);
647
+ const keyword = direction === "small" ? type === "string" ? "minLength" : type === "array" ? "minItems" : "minimum" : type === "string" ? "maxLength" : type === "array" ? "maxItems" : "maximum";
648
+ const value = schemaNumber(schema[keyword]);
649
+ if (value === void 0) return void 0;
650
+ const label = type === "string" ? "string length" : type === "array" ? "array length" : type;
651
+ if (label === void 0) return void 0;
652
+ return `expected ${label} ${direction === "small" ? "at least" : "at most"} ${value}`;
653
+ }
654
+ function schemaExpectation(schema, issueCode) {
655
+ switch (issueCode) {
656
+ case "invalid_type":
657
+ return schemaTypeExpectation(schema) ?? "failed structural validation";
658
+ case "invalid_value":
659
+ return schemaEnumExpectation(schema) ?? schemaTypeExpectation(schema) ?? "failed structural validation";
660
+ case "too_small":
661
+ return schemaSizeExpectation(schema, "small") ?? schemaTypeExpectation(schema) ?? "failed structural validation";
662
+ case "too_big":
663
+ return schemaSizeExpectation(schema, "big") ?? schemaTypeExpectation(schema) ?? "failed structural validation";
664
+ case "not_multiple_of": {
665
+ const multipleOf = schemaNumber(schema.multipleOf);
666
+ return multipleOf === void 0 ? schemaTypeExpectation(schema) ?? "failed structural validation" : `expected a multiple of ${multipleOf}`;
667
+ }
668
+ case "invalid_format": {
669
+ const format = schema.format;
670
+ return typeof format === "string" && SAFE_STRING_FORMATS.has(format) ? `expected valid ${format} format` : schemaTypeExpectation(schema) ?? "failed structural validation";
671
+ }
672
+ case "invalid_union":
673
+ return schemaTypeExpectation(schema) ?? "failed structural validation";
674
+ default: {
675
+ const typeExpectation = schemaTypeExpectation(schema);
676
+ return typeExpectation === void 0 ? "failed structural validation" : `failed structural validation; ${typeExpectation}`;
677
+ }
678
+ }
679
+ }
383
680
  var ToolStringLengthError = class extends Error {
384
681
  constructor(path, actual, maximum) {
385
682
  const renderedPath = path.reduce(
@@ -395,6 +692,42 @@ var ToolStringLengthError = class extends Error {
395
692
  this.name = "ToolStringLengthError";
396
693
  }
397
694
  };
695
+ function toolArgumentModelMessage(error, providerVisibleSchema) {
696
+ try {
697
+ let lines;
698
+ let omitted = false;
699
+ if (error instanceof import_zod.ZodError) {
700
+ if (error.issues.length === 0) return void 0;
701
+ lines = [];
702
+ for (const issue of error.issues.slice(0, MAX_MODEL_ARGUMENT_ISSUES)) {
703
+ const resolved = resolveSchemaPath(providerVisibleSchema, issue.path);
704
+ if (resolved === void 0) return void 0;
705
+ const path = jsonPointerPath(resolved.segments);
706
+ const expectation = schemaExpectation(resolved.node, issue.code);
707
+ lines.push(
708
+ truncateModelFeedback(`- ${path}: ${expectation}`, MAX_MODEL_ARGUMENT_LINE_LENGTH)
709
+ );
710
+ }
711
+ omitted = error.issues.length > lines.length;
712
+ } else if (error instanceof ToolStringLengthError) {
713
+ const resolved = resolveSchemaPath(providerVisibleSchema, error.path);
714
+ const maximum = schemaNumber(error.maximum);
715
+ if (resolved === void 0 || maximum === void 0) return void 0;
716
+ const path = jsonPointerPath(resolved.segments);
717
+ lines = [`- ${path}: maximum string length is ${maximum}`];
718
+ } else {
719
+ return void 0;
720
+ }
721
+ const header = "Error: Invalid tool arguments:";
722
+ const footer = "Correct the arguments and try again.";
723
+ const omission = omitted ? "Additional validation issues were omitted." : void 0;
724
+ const parts = [header, ...lines, ...omission ? [omission] : [], footer];
725
+ const message = parts.join("\n");
726
+ return message.length <= MAX_MODEL_ARGUMENT_MESSAGE_LENGTH ? message : void 0;
727
+ } catch {
728
+ return void 0;
729
+ }
730
+ }
398
731
  function validateStringLengths(value, maxLen, path = []) {
399
732
  if (typeof value === "string") {
400
733
  if (value.length > maxLen) {
@@ -944,6 +1277,38 @@ function unsafeTransportError(surface, remediation, exposedData) {
944
1277
  }
945
1278
 
946
1279
  // src/providers/openai-compatible.ts
1280
+ var OPENROUTER_IMAGE_MODEL = "openai/gpt-4o-mini";
1281
+ function compatibleImageBase64(source) {
1282
+ return source.type === "base64" ? source.data : Buffer.from(source.data.buffer, source.data.byteOffset, source.data.byteLength).toString(
1283
+ "base64"
1284
+ );
1285
+ }
1286
+ function openRouterImageParts(parts) {
1287
+ const content = [];
1288
+ for (const part of parts) {
1289
+ if (part.type === "text") {
1290
+ content.push({ type: "text", text: part.text });
1291
+ continue;
1292
+ }
1293
+ const { source } = part;
1294
+ if (source.type === "provider-file") {
1295
+ throw new UnsupportedModelInputError({
1296
+ provider: "openrouter",
1297
+ model: OPENROUTER_IMAGE_MODEL,
1298
+ modality: "image",
1299
+ source: "provider-file"
1300
+ });
1301
+ }
1302
+ content.push({
1303
+ type: "image_url",
1304
+ image_url: {
1305
+ url: source.type === "url" ? source.url : `data:${source.mediaType};base64,${compatibleImageBase64(source)}`
1306
+ }
1307
+ });
1308
+ if (part.label) content.push({ type: "text", text: `[Image: ${part.label}]` });
1309
+ }
1310
+ return content;
1311
+ }
947
1312
  function resolvePerModel(value, model, fallback) {
948
1313
  if (value === void 0) return fallback;
949
1314
  return typeof value === "function" ? value(model) : value;
@@ -1059,6 +1424,41 @@ function normalizeMessageContent(content) {
1059
1424
  }
1060
1425
  var OpenAICompatibleProvider = class {
1061
1426
  name;
1427
+ inputCapabilities(model) {
1428
+ return this.name === "openrouter" && model === OPENROUTER_IMAGE_MODEL ? { image: { sources: ["url", "bytes", "base64"] } } : {};
1429
+ }
1430
+ validateInput(request) {
1431
+ const effectiveModel = typeof request.providerOptions?.model === "string" ? request.providerOptions.model : request.model;
1432
+ const fail = (source, feature) => {
1433
+ throw new UnsupportedModelInputError({
1434
+ provider: this.name,
1435
+ model: effectiveModel || request.model,
1436
+ modality: "image",
1437
+ ...source ? { source } : {},
1438
+ ...feature ? { feature } : {}
1439
+ });
1440
+ };
1441
+ if (this.name !== "openrouter" || effectiveModel !== OPENROUTER_IMAGE_MODEL) {
1442
+ fail(void 0, "image input for this model");
1443
+ }
1444
+ if (request.providerOptions && "messages" in request.providerOptions) {
1445
+ fail(void 0, "raw messages providerOptions");
1446
+ }
1447
+ if (request.hasTools) fail(void 0, "tools with image input");
1448
+ for (const message of request.history) {
1449
+ if (!Array.isArray(message.content)) continue;
1450
+ if (message.role !== "user") fail(void 0, "rich non-user history");
1451
+ if (message.content.some(
1452
+ (part) => part.type === "image" && part.source.type === "provider-file"
1453
+ )) {
1454
+ fail("provider-file");
1455
+ }
1456
+ }
1457
+ if (Array.isArray(request.input) && request.input.some((part) => part.type === "image" && part.source.type === "provider-file")) {
1458
+ fail("provider-file");
1459
+ }
1460
+ return { effectiveModel };
1461
+ }
1062
1462
  /** `json_schema` is honored natively when the profile's `supportsJsonSchema`
1063
1463
  * capability is true for this model (the default); otherwise the engine
1064
1464
  * downgrades the request to plain `json_object` (see `buildRequestBody`),
@@ -1232,7 +1632,7 @@ var OpenAICompatibleProvider = class {
1232
1632
  formatMessage(msg, model) {
1233
1633
  const out = {
1234
1634
  role: this.profile.roleFor ? this.profile.roleFor(msg.role, model) : msg.role,
1235
- content: msg.content
1635
+ content: Array.isArray(msg.content) && this.name === "openrouter" && model === OPENROUTER_IMAGE_MODEL ? openRouterImageParts(msg.content) : msg.content
1236
1636
  };
1237
1637
  if (msg.name && (this.profile.capabilities?.emitsMessageName ?? true)) out.name = msg.name;
1238
1638
  if (msg.tool_calls) out.tool_calls = msg.tool_calls;
@@ -1979,8 +2379,88 @@ var OpenAIProvider = class extends OpenAICompatibleProvider {
1979
2379
  };
1980
2380
 
1981
2381
  // src/providers/openai-responses.ts
2382
+ var OPENAI_RESPONSES_IMAGE_MODELS = /* @__PURE__ */ new Set([
2383
+ "gpt-4o",
2384
+ "gpt-4o-2024-08-06",
2385
+ "gpt-4o-2024-11-20",
2386
+ "gpt-4o-mini",
2387
+ "gpt-4o-mini-2024-07-18"
2388
+ ]);
2389
+ function base64FromSource(source) {
2390
+ return source.type === "base64" ? source.data : Buffer.from(source.data.buffer, source.data.byteOffset, source.data.byteLength).toString(
2391
+ "base64"
2392
+ );
2393
+ }
2394
+ function responseImageParts(parts) {
2395
+ const mapped = [];
2396
+ for (const part of parts) {
2397
+ if (part.type === "text") {
2398
+ mapped.push({ type: "input_text", text: part.text });
2399
+ continue;
2400
+ }
2401
+ const { source } = part;
2402
+ if (source.type === "provider-file") {
2403
+ if (source.provider !== "openai-responses") {
2404
+ throw new UnsupportedModelInputError({
2405
+ provider: "openai-responses",
2406
+ model: "unknown",
2407
+ modality: "image",
2408
+ source: "provider-file"
2409
+ });
2410
+ }
2411
+ mapped.push({ type: "input_image", file_id: source.reference });
2412
+ } else if (source.type === "url") {
2413
+ mapped.push({ type: "input_image", image_url: source.url });
2414
+ } else {
2415
+ mapped.push({
2416
+ type: "input_image",
2417
+ image_url: `data:${source.mediaType};base64,${base64FromSource(source)}`
2418
+ });
2419
+ }
2420
+ if (part.label) mapped.push({ type: "input_text", text: `[Image: ${part.label}]` });
2421
+ }
2422
+ return mapped;
2423
+ }
1982
2424
  var OpenAIResponsesProvider = class {
1983
2425
  name = "openai-responses";
2426
+ inputCapabilities(model) {
2427
+ return OPENAI_RESPONSES_IMAGE_MODELS.has(model) ? { image: { sources: ["url", "bytes", "base64", "provider-file"] } } : {};
2428
+ }
2429
+ validateInput(request) {
2430
+ const effectiveModel = typeof request.providerOptions?.model === "string" ? request.providerOptions.model : request.model;
2431
+ const fail = (source, feature) => {
2432
+ throw new UnsupportedModelInputError({
2433
+ provider: this.name,
2434
+ model: effectiveModel || request.model,
2435
+ modality: "image",
2436
+ ...source ? { source } : {},
2437
+ ...feature ? { feature } : {}
2438
+ });
2439
+ };
2440
+ if (!OPENAI_RESPONSES_IMAGE_MODELS.has(effectiveModel)) {
2441
+ fail(void 0, "image input for this model");
2442
+ }
2443
+ if (request.providerOptions && "input" in request.providerOptions) {
2444
+ fail(void 0, "raw input-container providerOptions");
2445
+ }
2446
+ for (const message of request.history) {
2447
+ if (!Array.isArray(message.content)) continue;
2448
+ if (message.role !== "user") fail(void 0, "rich non-user history");
2449
+ for (const part of message.content) {
2450
+ if (part.type === "image" && part.source.type === "provider-file" && part.source.provider !== this.name) {
2451
+ fail("provider-file");
2452
+ }
2453
+ }
2454
+ }
2455
+ if (Array.isArray(request.input)) {
2456
+ for (const part of request.input) {
2457
+ if (part.type === "image" && part.source.type === "provider-file" && part.source.provider !== this.name) {
2458
+ fail("provider-file");
2459
+ }
2460
+ }
2461
+ }
2462
+ return { effectiveModel };
2463
+ }
1984
2464
  /** The Responses API honors `json_schema` (structured outputs) natively. */
1985
2465
  nativeStructuredOutputSupport() {
1986
2466
  return "schema";
@@ -2145,11 +2625,12 @@ var OpenAIResponsesProvider = class {
2145
2625
  buildInput(messages) {
2146
2626
  const input = [];
2147
2627
  for (const msg of messages) {
2628
+ const text = typeof msg.content === "string" ? msg.content : "";
2148
2629
  if (msg.role === "tool") {
2149
2630
  input.push({
2150
2631
  type: "function_call_output",
2151
2632
  call_id: msg.tool_call_id ?? "",
2152
- output: msg.content
2633
+ output: text
2153
2634
  });
2154
2635
  } else if (msg.role === "assistant" && msg.tool_calls && msg.tool_calls.length > 0) {
2155
2636
  const reasoningItems = msg.providerMetadata?.openaiReasoningItems;
@@ -2158,8 +2639,8 @@ var OpenAIResponsesProvider = class {
2158
2639
  input.push(item);
2159
2640
  }
2160
2641
  }
2161
- if (msg.content) {
2162
- input.push({ type: "message", role: "assistant", content: msg.content });
2642
+ if (text) {
2643
+ input.push({ type: "message", role: "assistant", content: text });
2163
2644
  }
2164
2645
  for (const tc of msg.tool_calls) {
2165
2646
  input.push({
@@ -2179,7 +2660,7 @@ var OpenAIResponsesProvider = class {
2179
2660
  input.push({
2180
2661
  type: "message",
2181
2662
  role: msg.role,
2182
- content: msg.content
2663
+ content: typeof msg.content === "string" ? msg.content : responseImageParts(msg.content)
2183
2664
  });
2184
2665
  }
2185
2666
  }
@@ -2246,7 +2727,7 @@ var OpenAIResponsesProvider = class {
2246
2727
  cached_tokens: json.usage.input_tokens_details?.cached_tokens,
2247
2728
  cache_write_tokens: json.usage.input_tokens_details?.cache_write_tokens
2248
2729
  } : void 0;
2249
- const cost = usage ? estimateDirectOpenAICost(json.model ?? model, usage, {
2730
+ const cost = usage && !this.requestContainsImages(request) ? estimateDirectOpenAICost(json.model ?? model, usage, {
2250
2731
  baseUrl: this.baseUrl,
2251
2732
  request,
2252
2733
  response: json
@@ -2261,6 +2742,16 @@ var OpenAIResponsesProvider = class {
2261
2742
  providerMetadata
2262
2743
  };
2263
2744
  }
2745
+ /** Responses usage does not report a media-inclusive price, so rich calls stay unpriced. */
2746
+ requestContainsImages(request) {
2747
+ return Array.isArray(request?.input) && request.input.some((item) => {
2748
+ if (!item || typeof item !== "object") return false;
2749
+ const content = item.content;
2750
+ return Array.isArray(content) && content.some(
2751
+ (part) => part !== null && typeof part === "object" && part.type === "input_image"
2752
+ );
2753
+ });
2754
+ }
2264
2755
  // ---------------------------------------------------------------------------
2265
2756
  // Internal: parse SSE stream
2266
2757
  // ---------------------------------------------------------------------------
@@ -2348,7 +2839,7 @@ var OpenAIResponsesProvider = class {
2348
2839
  return {
2349
2840
  type: "done",
2350
2841
  usage,
2351
- cost: usage ? estimateDirectOpenAICost(response?.model ?? model, usage, {
2842
+ cost: usage && !this.requestContainsImages(request) ? estimateDirectOpenAICost(response?.model ?? model, usage, {
2352
2843
  baseUrl: this.baseUrl,
2353
2844
  request,
2354
2845
  response
@@ -2385,7 +2876,51 @@ var OpenAIResponsesProvider = class {
2385
2876
  };
2386
2877
 
2387
2878
  // src/providers/anthropic.ts
2879
+ function anthropicBase64(source) {
2880
+ return source.type === "base64" ? source.data : Buffer.from(source.data.buffer, source.data.byteOffset, source.data.byteLength).toString(
2881
+ "base64"
2882
+ );
2883
+ }
2884
+ function anthropicImageBlocks(parts) {
2885
+ const blocks = [];
2886
+ for (const part of parts) {
2887
+ if (part.type === "text") {
2888
+ blocks.push({ type: "text", text: part.text });
2889
+ continue;
2890
+ }
2891
+ const { source } = part;
2892
+ if (source.type === "provider-file") {
2893
+ if (source.provider !== "anthropic") {
2894
+ throw new UnsupportedModelInputError({
2895
+ provider: "anthropic",
2896
+ model: "unknown",
2897
+ modality: "image",
2898
+ source: "provider-file"
2899
+ });
2900
+ }
2901
+ blocks.push({ type: "image", source: { type: "file", file_id: source.reference } });
2902
+ } else if (source.type === "url") {
2903
+ blocks.push({ type: "image", source: { type: "url", url: source.url } });
2904
+ } else {
2905
+ blocks.push({
2906
+ type: "image",
2907
+ source: { type: "base64", media_type: source.mediaType, data: anthropicBase64(source) }
2908
+ });
2909
+ }
2910
+ if (part.label) blocks.push({ type: "text", text: `[Image: ${part.label}]` });
2911
+ }
2912
+ return blocks;
2913
+ }
2388
2914
  var ANTHROPIC_API_VERSION = "2023-06-01";
2915
+ var ANTHROPIC_FILES_BETA = "files-api-2025-04-14";
2916
+ var ANTHROPIC_IMAGE_MODELS = /* @__PURE__ */ new Set(["claude-sonnet-4-5", "claude-opus-4-8"]);
2917
+ function hasAnthropicProviderFileImage(messages) {
2918
+ return messages.some(
2919
+ (message) => Array.isArray(message.content) && message.content.some(
2920
+ (part) => part.type === "image" && part.source.type === "provider-file" && part.source.provider === "anthropic"
2921
+ )
2922
+ );
2923
+ }
2389
2924
  var CLAUDE_CAPABILITIES = {
2390
2925
  "claude-fable-5": {
2391
2926
  thinking: "adaptive-always-on",
@@ -2646,6 +3181,11 @@ function isEffortAtOrBelow(effort, ceiling) {
2646
3181
  function pricingContextFromBody(body) {
2647
3182
  return {
2648
3183
  model: typeof body.model === "string" ? body.model : void 0,
3184
+ hasRichInput: Array.isArray(body.messages) && body.messages.some(
3185
+ (message) => message !== null && typeof message === "object" && Array.isArray(message.content) && message.content.some(
3186
+ (part) => part !== null && typeof part === "object" && part.type === "image"
3187
+ )
3188
+ ),
2649
3189
  unpricedModifier: isAnthropicModifier("inference_geo", body.inference_geo) || isAnthropicModifier("speed", body.speed) || hasAnthropicServerTool(body.tools) || body.fallbacks !== void 0
2650
3190
  };
2651
3191
  }
@@ -2740,6 +3280,44 @@ function mergeAnthropicUsage(initial, update) {
2740
3280
  }
2741
3281
  var AnthropicProvider = class {
2742
3282
  name = "anthropic";
3283
+ inputCapabilities(model) {
3284
+ return ANTHROPIC_IMAGE_MODELS.has(model) ? { image: { sources: ["url", "bytes", "base64", "provider-file"] } } : {};
3285
+ }
3286
+ validateInput(request) {
3287
+ const effectiveModel = typeof request.providerOptions?.model === "string" ? request.providerOptions.model : request.model;
3288
+ const fail = (source, feature) => {
3289
+ throw new UnsupportedModelInputError({
3290
+ provider: this.name,
3291
+ model: effectiveModel || request.model,
3292
+ modality: "image",
3293
+ ...source ? { source } : {},
3294
+ ...feature ? { feature } : {}
3295
+ });
3296
+ };
3297
+ if (!ANTHROPIC_IMAGE_MODELS.has(effectiveModel)) {
3298
+ fail(void 0, "image input for this model");
3299
+ }
3300
+ if (request.providerOptions && "messages" in request.providerOptions) {
3301
+ fail(void 0, "raw messages providerOptions");
3302
+ }
3303
+ for (const message of request.history) {
3304
+ if (!Array.isArray(message.content)) continue;
3305
+ if (message.role !== "user") fail(void 0, "rich non-user history");
3306
+ for (const part of message.content) {
3307
+ if (part.type === "image" && part.source.type === "provider-file" && part.source.provider !== this.name) {
3308
+ fail("provider-file");
3309
+ }
3310
+ }
3311
+ }
3312
+ if (Array.isArray(request.input)) {
3313
+ for (const part of request.input) {
3314
+ if (part.type === "image" && part.source.type === "provider-file" && part.source.provider !== this.name) {
3315
+ fail("provider-file");
3316
+ }
3317
+ }
3318
+ }
3319
+ return { effectiveModel };
3320
+ }
2743
3321
  /** Anthropic ignores native `json_schema` structurally — Axl uses a system
2744
3322
  * prompt JSON instruction + client-side Zod validation (see buildRequest). */
2745
3323
  nativeStructuredOutputSupport() {
@@ -2777,7 +3355,10 @@ var AnthropicProvider = class {
2777
3355
  // chat - non-streaming completion
2778
3356
  // ---------------------------------------------------------------------------
2779
3357
  async chat(messages, options) {
2780
- const headers = this.buildHeaders(await this.resolveKey());
3358
+ const headers = this.buildHeaders(
3359
+ await this.resolveKey(),
3360
+ hasAnthropicProviderFileImage(messages)
3361
+ );
2781
3362
  const body = this.buildRequestBody(messages, options, false);
2782
3363
  const pricingContext = pricingContextFromBody(body);
2783
3364
  const res = await fetchWithRetry(
@@ -2808,7 +3389,10 @@ var AnthropicProvider = class {
2808
3389
  // stream - SSE streaming completion
2809
3390
  // ---------------------------------------------------------------------------
2810
3391
  async *stream(messages, options) {
2811
- const headers = this.buildHeaders(await this.resolveKey());
3392
+ const headers = this.buildHeaders(
3393
+ await this.resolveKey(),
3394
+ hasAnthropicProviderFileImage(messages)
3395
+ );
2812
3396
  const body = this.buildRequestBody(messages, options, true);
2813
3397
  const pricingContext = pricingContextFromBody(body);
2814
3398
  const res = await fetchWithRetry(
@@ -2840,11 +3424,12 @@ var AnthropicProvider = class {
2840
3424
  // ---------------------------------------------------------------------------
2841
3425
  // Internal: request building
2842
3426
  // ---------------------------------------------------------------------------
2843
- buildHeaders(apiKey) {
3427
+ buildHeaders(apiKey, requiresFilesBeta = false) {
2844
3428
  return {
2845
3429
  "Content-Type": "application/json",
2846
3430
  "x-api-key": apiKey,
2847
- "anthropic-version": ANTHROPIC_API_VERSION
3431
+ "anthropic-version": ANTHROPIC_API_VERSION,
3432
+ ...requiresFilesBeta ? { "anthropic-beta": ANTHROPIC_FILES_BETA } : {}
2848
3433
  };
2849
3434
  }
2850
3435
  /** Extract a human-readable message from an API error response body. */
@@ -2914,12 +3499,13 @@ ${jsonInstruction}` : jsonInstruction;
2914
3499
  mapMessages(messages) {
2915
3500
  const result = [];
2916
3501
  for (const msg of messages) {
3502
+ const text = typeof msg.content === "string" ? msg.content : "";
2917
3503
  if (msg.role === "assistant") {
2918
3504
  const replayedThinking = this.getReplayedThinkingBlocks(msg);
2919
3505
  if (msg.tool_calls && msg.tool_calls.length > 0) {
2920
3506
  const content = [...replayedThinking];
2921
- if (msg.content) {
2922
- content.push({ type: "text", text: msg.content });
3507
+ if (text) {
3508
+ content.push({ type: "text", text });
2923
3509
  }
2924
3510
  for (const tc of msg.tool_calls) {
2925
3511
  let parsedArgs;
@@ -2938,10 +3524,10 @@ ${jsonInstruction}` : jsonInstruction;
2938
3524
  result.push({ role: "assistant", content });
2939
3525
  } else if (replayedThinking.length > 0) {
2940
3526
  const content = [...replayedThinking];
2941
- if (msg.content) content.push({ type: "text", text: msg.content });
3527
+ if (text) content.push({ type: "text", text });
2942
3528
  result.push({ role: "assistant", content });
2943
3529
  } else {
2944
- result.push({ role: "assistant", content: msg.content });
3530
+ result.push({ role: "assistant", content: text });
2945
3531
  }
2946
3532
  } else if (msg.role === "tool") {
2947
3533
  result.push({
@@ -2950,12 +3536,15 @@ ${jsonInstruction}` : jsonInstruction;
2950
3536
  {
2951
3537
  type: "tool_result",
2952
3538
  tool_use_id: msg.tool_call_id,
2953
- content: msg.content
3539
+ content: text
2954
3540
  }
2955
3541
  ]
2956
3542
  });
2957
3543
  } else if (msg.role === "user") {
2958
- result.push({ role: "user", content: msg.content });
3544
+ result.push({
3545
+ role: "user",
3546
+ content: typeof msg.content === "string" ? msg.content : anthropicImageBlocks(msg.content)
3547
+ });
2959
3548
  }
2960
3549
  }
2961
3550
  return this.mergeConsecutiveRoles(result);
@@ -3060,7 +3649,7 @@ ${jsonInstruction}` : jsonInstruction;
3060
3649
  "Anthropic fallback responses cannot be continued safely; retry without fallbacks."
3061
3650
  );
3062
3651
  }
3063
- const cost = normalized && !pricingContext.unpricedModifier && !isModifiedAnthropicResponse(json, effectiveModel) && !hasFallbackBoundary && !refused ? estimateAnthropicCost(effectiveModel, normalized.pricingUsage) : void 0;
3652
+ const cost = normalized && !pricingContext.hasRichInput && !pricingContext.unpricedModifier && !isModifiedAnthropicResponse(json, effectiveModel) && !hasFallbackBoundary && !refused ? estimateAnthropicCost(effectiveModel, normalized.pricingUsage) : void 0;
3064
3653
  return {
3065
3654
  content,
3066
3655
  thinking_content: thinkingContent || void 0,
@@ -3097,7 +3686,7 @@ ${jsonInstruction}` : jsonInstruction;
3097
3686
  const doneChunk = () => ({
3098
3687
  type: "done",
3099
3688
  usage,
3100
- cost: pricingUsage && !unpricedModifier && !hasFallbackBoundary && !refused ? estimateAnthropicCost(effectiveModel, pricingUsage) : void 0,
3689
+ cost: pricingUsage && !pricingContext.hasRichInput && !unpricedModifier && !hasFallbackBoundary && !refused ? estimateAnthropicCost(effectiveModel, pricingUsage) : void 0,
3101
3690
  providerMetadata: thinkingBlocks.length > 0 && !hasFallbackBoundary && !hasFallbackIterationSignal ? { anthropicThinkingBlocks: thinkingBlocks } : void 0
3102
3691
  });
3103
3692
  try {
@@ -3245,6 +3834,60 @@ function lastFallbackIndex(blocks) {
3245
3834
  }
3246
3835
 
3247
3836
  // src/providers/gemini.ts
3837
+ function hasRichGeminiMessages(messages) {
3838
+ return messages.some((message) => Array.isArray(message.content));
3839
+ }
3840
+ function geminiImageBase64(source) {
3841
+ return source.type === "base64" ? source.data : Buffer.from(source.data.buffer, source.data.byteOffset, source.data.byteLength).toString(
3842
+ "base64"
3843
+ );
3844
+ }
3845
+ function geminiInteractionContent(parts, model) {
3846
+ const content = [];
3847
+ for (const part of parts) {
3848
+ if (part.type === "text") {
3849
+ content.push({ type: "text", text: part.text });
3850
+ continue;
3851
+ }
3852
+ const { source } = part;
3853
+ if (source.type === "provider-file") {
3854
+ if (source.provider !== "google") {
3855
+ throw new UnsupportedModelInputError({
3856
+ provider: "google",
3857
+ model,
3858
+ modality: "image",
3859
+ source: "provider-file"
3860
+ });
3861
+ }
3862
+ if (!source.mediaType) {
3863
+ throw new UnsupportedModelInputError({
3864
+ provider: "google",
3865
+ model,
3866
+ modality: "image",
3867
+ source: "provider-file",
3868
+ feature: "Interactions URI image mediaType"
3869
+ });
3870
+ }
3871
+ content.push({
3872
+ type: "image",
3873
+ uri: source.reference,
3874
+ mime_type: source.mediaType
3875
+ });
3876
+ } else if (source.type === "url") {
3877
+ throw new UnsupportedModelInputError({
3878
+ provider: "google",
3879
+ model,
3880
+ modality: "image",
3881
+ source: "url",
3882
+ feature: "direct URL image input; pass bytes/base64 or a Gemini provider-file"
3883
+ });
3884
+ } else {
3885
+ content.push({ type: "image", data: geminiImageBase64(source), mime_type: source.mediaType });
3886
+ }
3887
+ if (part.label) content.push({ type: "text", text: `[Image: ${part.label}]` });
3888
+ }
3889
+ return content;
3890
+ }
3248
3891
  var GEMINI_DISALLOWED_SCHEMA_KEYS = /* @__PURE__ */ new Set([
3249
3892
  "additionalProperties",
3250
3893
  "$schema",
@@ -3316,6 +3959,7 @@ var GEMINI_PRICING = {
3316
3959
  "gemini-3.5-flash-lite": { input: 3e-7, cached: 3e-8, output: 25e-7 },
3317
3960
  "gemini-3.6-flash": { input: 15e-7, cached: 15e-8, output: 75e-7 }
3318
3961
  };
3962
+ var GEMINI_INTERACTIONS_IMAGE_MODELS = /* @__PURE__ */ new Set(["gemini-3.7-flash"]);
3319
3963
  function isValidTokenCount2(value) {
3320
3964
  return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
3321
3965
  }
@@ -3403,7 +4047,10 @@ var GEMINI_3X_MIN_THINKING_LEVEL = /* @__PURE__ */ new Map([
3403
4047
  ["gemini-3.1-flash-lite-preview", "minimal"],
3404
4048
  ["gemini-3.5-flash", "minimal"],
3405
4049
  ["gemini-3.5-flash-lite", "minimal"],
3406
- ["gemini-3.6-flash", "minimal"]
4050
+ ["gemini-3.6-flash", "minimal"],
4051
+ // L4 live evidence: Interactions gemini-3.7-flash accepts low/medium/high,
4052
+ // but rejects minimal. Keep this exact entry isolated from legacy aliases.
4053
+ ["gemini-3.7-flash", "low"]
3407
4054
  ]);
3408
4055
  var GEMINI_MODELS_WITHOUT_PORTABLE_TEMPERATURE = /* @__PURE__ */ new Set([
3409
4056
  "gemini-3.5-flash-lite",
@@ -3430,6 +4077,61 @@ function warnGemini3xEffortNone(model) {
3430
4077
  }
3431
4078
  var GeminiProvider = class {
3432
4079
  name = "google";
4080
+ inputCapabilities(model) {
4081
+ return GEMINI_INTERACTIONS_IMAGE_MODELS.has(model) ? { image: { sources: ["bytes", "base64", "provider-file"] } } : {};
4082
+ }
4083
+ validateInput(request) {
4084
+ const effectiveModel = typeof request.providerOptions?.model === "string" ? request.providerOptions.model : request.model;
4085
+ const fail = (source, feature) => {
4086
+ throw new UnsupportedModelInputError({
4087
+ provider: this.name,
4088
+ model: effectiveModel || request.model,
4089
+ modality: "image",
4090
+ ...source ? { source } : {},
4091
+ ...feature ? { feature } : {}
4092
+ });
4093
+ };
4094
+ if (!GEMINI_INTERACTIONS_IMAGE_MODELS.has(effectiveModel)) {
4095
+ fail(void 0, "image input for this model");
4096
+ }
4097
+ if (request.providerOptions) {
4098
+ const forbidden = [
4099
+ "input",
4100
+ "contents",
4101
+ "messages",
4102
+ "previous_interaction_id",
4103
+ "background",
4104
+ "stream",
4105
+ "store"
4106
+ ].find((key) => key in request.providerOptions);
4107
+ if (forbidden) fail(void 0, `raw ${forbidden} providerOptions`);
4108
+ }
4109
+ for (const message of request.history) {
4110
+ if (!Array.isArray(message.content)) continue;
4111
+ if (message.role !== "user") fail(void 0, "rich non-user history");
4112
+ for (const part of message.content) {
4113
+ if (part.type !== "image") continue;
4114
+ if (part.source.type === "url")
4115
+ fail("url", "direct URL image input; pass bytes/base64 or a Gemini provider-file");
4116
+ if (part.source.type === "provider-file" && part.source.provider !== this.name)
4117
+ fail("provider-file");
4118
+ if (part.source.type === "provider-file" && !part.source.mediaType)
4119
+ fail("provider-file", "Interactions URI image mediaType");
4120
+ }
4121
+ }
4122
+ if (Array.isArray(request.input)) {
4123
+ for (const part of request.input) {
4124
+ if (part.type !== "image") continue;
4125
+ if (part.source.type === "url")
4126
+ fail("url", "direct URL image input; pass bytes/base64 or a Gemini provider-file");
4127
+ if (part.source.type === "provider-file" && part.source.provider !== this.name)
4128
+ fail("provider-file");
4129
+ if (part.source.type === "provider-file" && !part.source.mediaType)
4130
+ fail("provider-file", "Interactions URI image mediaType");
4131
+ }
4132
+ }
4133
+ return { effectiveModel };
4134
+ }
3433
4135
  /** Gemini accepts a `responseSchema` but `sanitizeSchemaForGemini` strips
3434
4136
  * keywords it doesn't support (`$ref`/`$defs`/`additionalProperties`/…), so a
3435
4137
  * derived schema can lose constraints — lossy, not faithful. */
@@ -3468,6 +4170,7 @@ var GeminiProvider = class {
3468
4170
  // chat - non-streaming completion
3469
4171
  // ---------------------------------------------------------------------------
3470
4172
  async chat(messages, options) {
4173
+ if (hasRichGeminiMessages(messages)) return this.chatInteraction(messages, options);
3471
4174
  const body = this.buildRequestBody(messages, options);
3472
4175
  const pricingContext = this.pricingContext(this.requestModel(options), body);
3473
4176
  this.assertSafeGemini3Continuation(messages, pricingContext.model);
@@ -3500,6 +4203,10 @@ var GeminiProvider = class {
3500
4203
  // stream - SSE streaming completion
3501
4204
  // ---------------------------------------------------------------------------
3502
4205
  async *stream(messages, options) {
4206
+ if (hasRichGeminiMessages(messages)) {
4207
+ yield* this.streamInteraction(messages, options);
4208
+ return;
4209
+ }
3503
4210
  const body = this.buildRequestBody(messages, options);
3504
4211
  const pricingContext = this.pricingContext(this.requestModel(options), body);
3505
4212
  this.assertSafeGemini3Continuation(messages, pricingContext.model);
@@ -3531,25 +4238,358 @@ var GeminiProvider = class {
3531
4238
  yield* this.parseSSEStream(res.body, pricingContext);
3532
4239
  }
3533
4240
  // ---------------------------------------------------------------------------
3534
- // Internal: request building
4241
+ // Rich image requests use Gemini Interactions. It is deliberately stateless:
4242
+ // Axl sends the complete application-owned history and never asks Google to
4243
+ // retain an interaction or follow a previous_interaction_id.
3535
4244
  // ---------------------------------------------------------------------------
3536
- buildHeaders(apiKey) {
3537
- return {
3538
- "Content-Type": "application/json",
3539
- "x-goog-api-key": apiKey
4245
+ async chatInteraction(messages, options) {
4246
+ this.assertSafeInteractionOptions(options);
4247
+ const body = this.buildInteractionRequestBody(messages, options, false);
4248
+ const headers = this.buildHeaders(await this.resolveKey());
4249
+ const res = await fetchWithRetry(
4250
+ `${this.baseUrl}/interactions`,
4251
+ { method: "POST", headers, body: JSON.stringify(body), signal: options.signal },
4252
+ { governor: this.governor, provider: this.name }
4253
+ );
4254
+ if (!res.ok) {
4255
+ const errorBody = await res.text();
4256
+ throw buildProviderError({
4257
+ provider: this.name,
4258
+ status: res.status,
4259
+ headers: res.headers,
4260
+ message: this.extractErrorMessage(errorBody, res.status),
4261
+ body: errorBody
4262
+ });
4263
+ }
4264
+ return this.parseInteractionResponse(await res.json());
4265
+ }
4266
+ async *streamInteraction(messages, options) {
4267
+ this.assertSafeInteractionOptions(options);
4268
+ const body = this.buildInteractionRequestBody(messages, options, true);
4269
+ const headers = this.buildHeaders(await this.resolveKey());
4270
+ const res = await fetchWithRetry(
4271
+ `${this.baseUrl}/interactions?alt=sse`,
4272
+ { method: "POST", headers, body: JSON.stringify(body), signal: options.signal },
4273
+ { governor: this.governor, provider: this.name }
4274
+ );
4275
+ if (!res.ok) {
4276
+ const errorBody = await res.text();
4277
+ throw buildProviderError({
4278
+ provider: this.name,
4279
+ status: res.status,
4280
+ headers: res.headers,
4281
+ message: this.extractErrorMessage(errorBody, res.status),
4282
+ body: errorBody
4283
+ });
4284
+ }
4285
+ if (!res.body) throw new Error("Gemini Interactions stream has no body");
4286
+ yield* this.parseInteractionSSEStream(res.body);
4287
+ }
4288
+ buildInteractionRequestBody(messages, options, stream) {
4289
+ const model = this.requestModel(options);
4290
+ const systemInstruction = messages.filter((message) => message.role === "system").map((message) => typeof message.content === "string" ? message.content : "").filter(Boolean).join("\n\n");
4291
+ const body = {
4292
+ model,
4293
+ input: this.mapInteractionInput(
4294
+ messages.filter((message) => message.role !== "system"),
4295
+ model
4296
+ ),
4297
+ stream
3540
4298
  };
4299
+ if (systemInstruction) body.system_instruction = systemInstruction;
4300
+ if (options.tools?.length) {
4301
+ body.tools = options.tools.map((tool2) => ({
4302
+ type: "function",
4303
+ name: tool2.function.name,
4304
+ description: tool2.function.description,
4305
+ parameters: sanitizeSchemaForGemini(tool2.function.parameters)
4306
+ }));
4307
+ }
4308
+ const generationConfig = {};
4309
+ if (options.maxTokens !== void 0) generationConfig.max_output_tokens = options.maxTokens;
4310
+ if (options.stop) generationConfig.stop_sequences = options.stop;
4311
+ if (options.toolChoice !== void 0) {
4312
+ generationConfig.tool_choice = typeof options.toolChoice === "string" ? options.toolChoice === "required" ? "any" : options.toolChoice : { allowed_tools: { mode: "any", tools: [options.toolChoice.function.name] } };
4313
+ }
4314
+ const thinking = resolveThinkingOptions(options);
4315
+ if (thinking.thinkingDisabled) {
4316
+ generationConfig.thinking_level = isGemini3x(model) ? minThinkingLevel(model) : "minimal";
4317
+ } else if (thinking.hasBudgetOverride) {
4318
+ generationConfig.thinking_level = budgetToThinkingLevel(thinking.thinkingBudget);
4319
+ } else if (thinking.activeEffort) {
4320
+ generationConfig.thinking_level = isGemini3x(model) ? THINKING_LEVELS[thinking.activeEffort] ?? "medium" : budgetToThinkingLevel(THINKING_BUDGETS2[thinking.activeEffort] ?? 5e3);
4321
+ }
4322
+ if (thinking.includeThoughts) generationConfig.thinking_summaries = "auto";
4323
+ if (Object.keys(generationConfig).length) body.generation_config = generationConfig;
4324
+ if (options.responseFormat?.type && options.responseFormat.type !== "text") {
4325
+ body.response_format = {
4326
+ type: "text",
4327
+ mime_type: "application/json",
4328
+ ...options.responseFormat.type === "json_schema" ? { schema: sanitizeSchemaForGemini(options.responseFormat.json_schema.schema) } : {}
4329
+ };
4330
+ }
4331
+ if (options.providerOptions) {
4332
+ const overrides = { ...options.providerOptions };
4333
+ delete overrides.model;
4334
+ delete overrides.input;
4335
+ delete overrides.contents;
4336
+ delete overrides.messages;
4337
+ delete overrides.previous_interaction_id;
4338
+ delete overrides.background;
4339
+ delete overrides.stream;
4340
+ delete overrides.store;
4341
+ const rawGenerationConfig = overrides.generation_config;
4342
+ if (rawGenerationConfig && typeof rawGenerationConfig === "object" && !Array.isArray(rawGenerationConfig)) {
4343
+ const generationConfigOverrides = { ...rawGenerationConfig };
4344
+ delete generationConfigOverrides.temperature;
4345
+ if (Object.keys(generationConfigOverrides).length)
4346
+ overrides.generation_config = generationConfigOverrides;
4347
+ else delete overrides.generation_config;
4348
+ }
4349
+ Object.assign(body, overrides);
4350
+ }
4351
+ body.stream = stream;
4352
+ body.store = false;
4353
+ return body;
3541
4354
  }
3542
- extractErrorMessage(body, status) {
3543
- try {
3544
- const json = JSON.parse(body);
3545
- if (json.error?.message) {
3546
- return `Gemini API error (${status}): ${json.error.message}`;
4355
+ assertSafeInteractionOptions(options) {
4356
+ const model = this.requestModel(options);
4357
+ const fail = (feature) => {
4358
+ throw new UnsupportedModelInputError({
4359
+ provider: this.name,
4360
+ model,
4361
+ modality: "image",
4362
+ feature
4363
+ });
4364
+ };
4365
+ if (!GEMINI_INTERACTIONS_IMAGE_MODELS.has(model)) fail("image input for this model");
4366
+ const forbidden = [
4367
+ "input",
4368
+ "contents",
4369
+ "messages",
4370
+ "previous_interaction_id",
4371
+ "background",
4372
+ "stream",
4373
+ "store"
4374
+ ].find((key) => key in (options.providerOptions ?? {}));
4375
+ if (forbidden) fail(`raw ${forbidden} providerOptions`);
4376
+ }
4377
+ mapInteractionInput(messages, model) {
4378
+ const steps = [];
4379
+ for (const message of messages) {
4380
+ if (message.role === "user") {
4381
+ steps.push({
4382
+ type: "user_input",
4383
+ content: Array.isArray(message.content) ? geminiInteractionContent(message.content, model) : [{ type: "text", text: message.content }]
4384
+ });
4385
+ } else if (message.role === "assistant") {
4386
+ const nativeSteps = message.providerMetadata?.geminiInteractionSteps;
4387
+ if (Array.isArray(nativeSteps) && nativeSteps.every(isGeminiInteractionStep)) {
4388
+ steps.push(...nativeSteps);
4389
+ continue;
4390
+ }
4391
+ if (typeof message.content === "string" && message.content) {
4392
+ steps.push({ type: "model_output", content: [{ type: "text", text: message.content }] });
4393
+ }
4394
+ for (const call of message.tool_calls ?? []) {
4395
+ steps.push({
4396
+ type: "function_call",
4397
+ id: call.id,
4398
+ name: call.function.name,
4399
+ arguments: safeJsonObject(call.function.arguments)
4400
+ });
4401
+ }
4402
+ } else if (message.role === "tool") {
4403
+ steps.push({
4404
+ type: "function_result",
4405
+ call_id: message.tool_call_id ?? "",
4406
+ result: [
4407
+ { type: "text", text: typeof message.content === "string" ? message.content : "" }
4408
+ ]
4409
+ });
3547
4410
  }
3548
- } catch {
3549
4411
  }
3550
- return `Gemini API error (${status}): ${body}`;
4412
+ return steps;
3551
4413
  }
3552
- pricingContext(fallbackModel, body) {
4414
+ parseInteractionResponse(json) {
4415
+ let content = "";
4416
+ let thinkingContent = "";
4417
+ const toolCalls = [];
4418
+ for (const step of json.steps ?? []) {
4419
+ if (step.type === "model_output") content += interactionText(step.content);
4420
+ else if (step.type === "thought") thinkingContent += interactionText(step.summary);
4421
+ else if (step.type === "function_call" && step.id && step.name) {
4422
+ toolCalls.push({
4423
+ id: step.id,
4424
+ type: "function",
4425
+ function: { name: step.name, arguments: JSON.stringify(step.arguments ?? {}) }
4426
+ });
4427
+ }
4428
+ }
4429
+ this.assertInteractionTerminalStatus(json.status, toolCalls.length);
4430
+ const usage = normalizeInteractionUsage(json.usage);
4431
+ return {
4432
+ content,
4433
+ thinking_content: thinkingContent || void 0,
4434
+ tool_calls: toolCalls.length ? toolCalls : void 0,
4435
+ usage,
4436
+ // Interactions returns modality usage but no authoritative monetary cost.
4437
+ cost: void 0,
4438
+ providerMetadata: json.steps?.length ? { geminiInteractionSteps: json.steps.filter(isGeminiInteractionStep) } : void 0
4439
+ };
4440
+ }
4441
+ async *parseInteractionSSEStream(body) {
4442
+ const reader = body.getReader();
4443
+ const decoder = new TextDecoder();
4444
+ let buffer = "";
4445
+ const steps = /* @__PURE__ */ new Map();
4446
+ const argumentBuffers = /* @__PURE__ */ new Map();
4447
+ const stopped = /* @__PURE__ */ new Set();
4448
+ let completed = false;
4449
+ try {
4450
+ while (true) {
4451
+ const { done, value } = await reader.read();
4452
+ if (done) break;
4453
+ buffer += decoder.decode(value, { stream: true });
4454
+ const lines = buffer.split("\n");
4455
+ buffer = lines.pop() ?? "";
4456
+ for (const line of lines) {
4457
+ const trimmed = line.trim();
4458
+ if (!trimmed.startsWith("data: ")) continue;
4459
+ if (trimmed.slice(6) === "[DONE]") continue;
4460
+ let event;
4461
+ try {
4462
+ event = JSON.parse(trimmed.slice(6));
4463
+ } catch {
4464
+ continue;
4465
+ }
4466
+ if (event.event_type === "error") {
4467
+ throw new Error(event.error?.message ?? "Gemini Interactions stream failed");
4468
+ }
4469
+ if (event.event_type === "step.start" && event.step) {
4470
+ const index = event.index;
4471
+ if (index === void 0 || steps.has(index)) {
4472
+ throw new Error("Gemini Interactions stream contained an invalid step.start index");
4473
+ }
4474
+ const step = cloneInteractionStep(event.step);
4475
+ steps.set(index, step);
4476
+ if (step.type === "model_output") {
4477
+ for (const text of interactionTextChunks(step.content)) {
4478
+ yield { type: "text_delta", content: text };
4479
+ }
4480
+ } else if (step.type === "thought") {
4481
+ for (const text of interactionTextChunks(step.summary)) {
4482
+ yield { type: "thinking_delta", content: text };
4483
+ }
4484
+ }
4485
+ if (step.type === "function_call" && step.id && step.name) {
4486
+ argumentBuffers.set(index, "");
4487
+ yield { type: "tool_call_delta", id: step.id, name: step.name };
4488
+ }
4489
+ } else if (event.event_type === "step.delta" && event.delta) {
4490
+ const delta = event.delta;
4491
+ const index = event.index;
4492
+ if (index === void 0 || stopped.has(index)) {
4493
+ throw new Error("Gemini Interactions stream delta arrived outside an active step");
4494
+ }
4495
+ const step = steps.get(index);
4496
+ if (!step)
4497
+ throw new Error("Gemini Interactions stream delta arrived before step.start");
4498
+ if (delta.type === "text" && typeof delta.text === "string" && step.type === "model_output") {
4499
+ appendInteractionContent(step, "content", { type: "text", text: delta.text });
4500
+ yield { type: "text_delta", content: delta.text };
4501
+ } else if (delta.type === "thought_summary" && step.type === "thought" && delta.content) {
4502
+ appendInteractionContent(step, "summary", delta.content);
4503
+ for (const text of interactionTextChunks([delta.content])) {
4504
+ yield { type: "thinking_delta", content: text };
4505
+ }
4506
+ } else if (delta.type === "thought_signature" && step.type === "thought" && typeof delta.signature === "string") {
4507
+ step.signature = delta.signature;
4508
+ } else if (delta.type === "arguments_delta" && step.type === "function_call" && typeof delta.arguments === "string") {
4509
+ argumentBuffers.set(index, (argumentBuffers.get(index) ?? "") + delta.arguments);
4510
+ if (step.id)
4511
+ yield { type: "tool_call_delta", id: step.id, arguments: delta.arguments };
4512
+ }
4513
+ } else if (event.event_type === "step.stop") {
4514
+ const index = event.index;
4515
+ const step = index === void 0 ? void 0 : steps.get(index);
4516
+ if (step === void 0 || stopped.has(index)) {
4517
+ throw new Error("Gemini Interactions stream contained an invalid step.stop index");
4518
+ }
4519
+ if (step.type === "function_call") {
4520
+ const rawArguments = argumentBuffers.get(index) ?? "";
4521
+ if (rawArguments) {
4522
+ try {
4523
+ const parsed = JSON.parse(rawArguments);
4524
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
4525
+ throw new Error("not an object");
4526
+ }
4527
+ step.arguments = parsed;
4528
+ } catch {
4529
+ throw new Error(
4530
+ "Gemini Interactions function_call arguments were not valid JSON"
4531
+ );
4532
+ }
4533
+ }
4534
+ }
4535
+ stopped.add(index);
4536
+ } else if (event.event_type === "interaction.completed") {
4537
+ if (steps.size !== stopped.size) {
4538
+ throw new Error("Gemini Interactions stream completed before all steps stopped");
4539
+ }
4540
+ const toolCallCount = [...steps.values()].filter(
4541
+ (step) => step.type === "function_call" && !!step.id && !!step.name
4542
+ ).length;
4543
+ this.assertInteractionTerminalStatus(event.interaction?.status, toolCallCount);
4544
+ completed = true;
4545
+ yield {
4546
+ type: "done",
4547
+ usage: normalizeInteractionUsage(event.interaction?.usage),
4548
+ providerMetadata: steps.size ? {
4549
+ geminiInteractionSteps: [...steps.entries()].sort(([a], [b]) => a - b).map(([, step]) => step)
4550
+ } : void 0
4551
+ };
4552
+ return;
4553
+ }
4554
+ }
4555
+ }
4556
+ if (!completed)
4557
+ throw new Error("Gemini Interactions stream ended before interaction.completed");
4558
+ } finally {
4559
+ reader.releaseLock();
4560
+ }
4561
+ }
4562
+ assertInteractionTerminalStatus(status, toolCallCount) {
4563
+ if (status === "completed") return;
4564
+ if (status === "requires_action" && toolCallCount > 0) return;
4565
+ const normalizedStatus = typeof status === "string" ? status : "unknown";
4566
+ throw new ProviderError({
4567
+ provider: this.name,
4568
+ status: 0,
4569
+ retryable: false,
4570
+ message: `Gemini Interactions ended with non-success status: ${normalizedStatus}`
4571
+ });
4572
+ }
4573
+ // ---------------------------------------------------------------------------
4574
+ // Internal: request building
4575
+ // ---------------------------------------------------------------------------
4576
+ buildHeaders(apiKey) {
4577
+ return {
4578
+ "Content-Type": "application/json",
4579
+ "x-goog-api-key": apiKey
4580
+ };
4581
+ }
4582
+ extractErrorMessage(body, status) {
4583
+ try {
4584
+ const json = JSON.parse(body);
4585
+ if (json.error?.message) {
4586
+ return `Gemini API error (${status}): ${json.error.message}`;
4587
+ }
4588
+ } catch {
4589
+ }
4590
+ return `Gemini API error (${status}): ${body}`;
4591
+ }
4592
+ pricingContext(fallbackModel, body) {
3553
4593
  return {
3554
4594
  // Gemini's model is normally selected in the URL. providerOptions is an
3555
4595
  // explicit raw escape hatch, so honor a post-merge model override for both
@@ -3785,14 +4825,15 @@ var GeminiProvider = class {
3785
4825
  }
3786
4826
  const result = [];
3787
4827
  for (const msg of messages) {
4828
+ const text = typeof msg.content === "string" ? msg.content : "";
3788
4829
  if (msg.role === "assistant") {
3789
4830
  const rawParts = msg.providerMetadata?.geminiParts;
3790
4831
  if (rawParts && rawParts.length > 0) {
3791
4832
  result.push({ role: "model", parts: rawParts });
3792
4833
  } else {
3793
4834
  const parts = [];
3794
- if (msg.content) {
3795
- parts.push({ text: msg.content });
4835
+ if (text) {
4836
+ parts.push({ text });
3796
4837
  }
3797
4838
  if (msg.tool_calls && msg.tool_calls.length > 0) {
3798
4839
  for (const tc of msg.tool_calls) {
@@ -3818,7 +4859,7 @@ var GeminiProvider = class {
3818
4859
  const functionName = (msg.tool_call_id ? geminiNativeNames.get(msg.tool_call_id) : void 0) ?? toolCallIdToName.get(msg.tool_call_id) ?? "unknown";
3819
4860
  const functionResponse = {
3820
4861
  name: functionName,
3821
- response: parseGeminiFunctionResponse(msg.content)
4862
+ response: parseGeminiFunctionResponse(text)
3822
4863
  };
3823
4864
  if (msg.tool_call_id && geminiNativeIds.has(msg.tool_call_id)) {
3824
4865
  functionResponse.id = msg.tool_call_id;
@@ -3828,7 +4869,7 @@ var GeminiProvider = class {
3828
4869
  parts: [{ functionResponse }]
3829
4870
  });
3830
4871
  } else if (msg.role === "user") {
3831
- result.push({ role: "user", parts: [{ text: msg.content }] });
4872
+ result.push({ role: "user", parts: [{ text }] });
3832
4873
  }
3833
4874
  }
3834
4875
  return this.mergeConsecutiveRoles(result);
@@ -4016,6 +5057,56 @@ var GeminiProvider = class {
4016
5057
  }
4017
5058
  }
4018
5059
  };
5060
+ function isGeminiInteractionStep(value) {
5061
+ return value !== null && typeof value === "object" && ["user_input", "model_output", "thought", "function_call", "function_result"].includes(
5062
+ value.type
5063
+ );
5064
+ }
5065
+ function interactionText(content) {
5066
+ return content?.map((part) => part.type === "text" && typeof part.text === "string" ? part.text : "").join("") ?? "";
5067
+ }
5068
+ function interactionTextChunks(content) {
5069
+ return content?.flatMap(
5070
+ (part) => part.type === "text" && typeof part.text === "string" ? [part.text] : []
5071
+ ) ?? [];
5072
+ }
5073
+ function cloneInteractionStep(step) {
5074
+ return JSON.parse(JSON.stringify(step));
5075
+ }
5076
+ function appendInteractionContent(step, field, content) {
5077
+ const existing = step[field] ?? [];
5078
+ const last = existing.at(-1);
5079
+ if (content.type === "text" && last?.type === "text" && typeof content.text === "string" && typeof last.text === "string") {
5080
+ last.text += content.text;
5081
+ } else {
5082
+ existing.push(content);
5083
+ }
5084
+ step[field] = existing;
5085
+ }
5086
+ function safeJsonObject(value) {
5087
+ try {
5088
+ const parsed = JSON.parse(value);
5089
+ return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
5090
+ } catch {
5091
+ return {};
5092
+ }
5093
+ }
5094
+ function normalizeInteractionUsage(usage) {
5095
+ if (!usage || !isValidTokenCount2(usage.total_input_tokens) || !isValidTokenCount2(usage.total_output_tokens) || !isValidTokenCount2(usage.total_tokens)) {
5096
+ return void 0;
5097
+ }
5098
+ const cached = usage.total_cached_tokens;
5099
+ const thinking = usage.total_thought_tokens;
5100
+ if (cached !== void 0 && (!isValidTokenCount2(cached) || cached > usage.total_input_tokens) || thinking !== void 0 && !isValidTokenCount2(thinking))
5101
+ return void 0;
5102
+ return {
5103
+ prompt_tokens: usage.total_input_tokens,
5104
+ completion_tokens: usage.total_output_tokens,
5105
+ total_tokens: usage.total_tokens,
5106
+ ...cached ? { cached_tokens: cached } : {},
5107
+ ...thinking ? { reasoning_tokens: thinking } : {}
5108
+ };
5109
+ }
4019
5110
 
4020
5111
  // src/providers/profiles/openrouter.ts
4021
5112
  var OPENROUTER_PROFILE = {
@@ -4456,59 +5547,1211 @@ var ProviderRegistry = class {
4456
5547
  }
4457
5548
  throw new Error(`Unknown provider "${name}". Registered providers: ${this.list().join(", ")}`);
4458
5549
  }
4459
- /**
4460
- * Resolve a "provider:model" URI string into a Provider instance and model name.
4461
- *
4462
- * Supported formats:
4463
- * - "openai:gpt-4o" -> provider=openai, model=gpt-4o
4464
- * - "anthropic:claude-3" -> provider=anthropic, model=claude-3
4465
- * - "gpt-4o" -> uses defaultProvider from config, model=gpt-4o
4466
- * - undefined / empty -> uses defaultProvider and defaultModel from config
4467
- *
4468
- * @param uri Provider:model string, or just a model name
4469
- * @param config Axl configuration for provider options and defaults
4470
- */
4471
- resolve(uri, config = {}) {
4472
- if (!uri) {
4473
- const providerName2 = config.defaultProvider ?? "openai";
4474
- const model2 = config.defaultModel ?? "gpt-4o";
4475
- return { provider: this.get(providerName2, config), model: model2 };
4476
- }
4477
- const colonIndex = uri.indexOf(":");
4478
- if (colonIndex === -1) {
4479
- const providerName2 = config.defaultProvider ?? "openai";
4480
- return { provider: this.get(providerName2, config), model: uri };
4481
- }
4482
- const providerName = uri.slice(0, colonIndex);
4483
- const model = uri.slice(colonIndex + 1);
4484
- if (!providerName || !model) {
4485
- throw new Error(
4486
- `Invalid provider URI "${uri}". Expected format: "provider:model" (e.g. "openai:gpt-4o")`
4487
- );
4488
- }
4489
- return { provider: this.get(providerName, config), model };
5550
+ /**
5551
+ * Resolve a "provider:model" URI string into a Provider instance and model name.
5552
+ *
5553
+ * Supported formats:
5554
+ * - "openai:gpt-4o" -> provider=openai, model=gpt-4o
5555
+ * - "anthropic:claude-3" -> provider=anthropic, model=claude-3
5556
+ * - "gpt-4o" -> uses defaultProvider from config, model=gpt-4o
5557
+ * - undefined / empty -> uses defaultProvider and defaultModel from config
5558
+ *
5559
+ * @param uri Provider:model string, or just a model name
5560
+ * @param config Axl configuration for provider options and defaults
5561
+ */
5562
+ resolve(uri, config = {}) {
5563
+ if (!uri) {
5564
+ const providerName2 = config.defaultProvider ?? "openai";
5565
+ const model2 = config.defaultModel ?? "gpt-4o";
5566
+ return { provider: this.get(providerName2, config), model: model2 };
5567
+ }
5568
+ const colonIndex = uri.indexOf(":");
5569
+ if (colonIndex === -1) {
5570
+ const providerName2 = config.defaultProvider ?? "openai";
5571
+ return { provider: this.get(providerName2, config), model: uri };
5572
+ }
5573
+ const providerName = uri.slice(0, colonIndex);
5574
+ const model = uri.slice(colonIndex + 1);
5575
+ if (!providerName || !model) {
5576
+ throw new Error(
5577
+ `Invalid provider URI "${uri}". Expected format: "provider:model" (e.g. "openai:gpt-4o")`
5578
+ );
5579
+ }
5580
+ return { provider: this.get(providerName, config), model };
5581
+ }
5582
+ /**
5583
+ * Clear all cached provider instances. Useful for testing or reconfiguration.
5584
+ */
5585
+ clearCache() {
5586
+ this.instances.clear();
5587
+ }
5588
+ /**
5589
+ * Clear all registered factories (including built-ins).
5590
+ * Useful for test runtimes where only explicitly registered instances should be used.
5591
+ */
5592
+ clearFactories() {
5593
+ this.factories.clear();
5594
+ }
5595
+ };
5596
+ var defaultRegistry = new ProviderRegistry();
5597
+
5598
+ // src/transcription.ts
5599
+ var MAX_INLINE_TRANSCRIPTION_BYTES = 25 * 1024 * 1024;
5600
+ var RESERVED_PROVIDER_OPTION_KEYS = /* @__PURE__ */ new Set([
5601
+ "model",
5602
+ "audio",
5603
+ "signal",
5604
+ "cleanup",
5605
+ "cleanupStatus",
5606
+ "usage",
5607
+ "cost",
5608
+ "pricingStatus",
5609
+ "accounting"
5610
+ ]);
5611
+ function nonEmptyString(value) {
5612
+ return typeof value === "string" && value.trim().length > 0;
5613
+ }
5614
+ function cloneValue(value) {
5615
+ try {
5616
+ return structuredClone(value);
5617
+ } catch {
5618
+ throw new InvalidTranscriptionInputError("Transcription options must be structured-cloneable");
5619
+ }
5620
+ }
5621
+ function normalizeTranscriptionRequest(request) {
5622
+ if (!request || typeof request !== "object" || !nonEmptyString(request.model)) {
5623
+ throw new InvalidTranscriptionInputError("A transcription model URI is required");
5624
+ }
5625
+ const audio = request.audio;
5626
+ if (!audio || typeof audio !== "object") {
5627
+ throw new InvalidTranscriptionInputError("A transcription audio source is required");
5628
+ }
5629
+ let normalizedAudio;
5630
+ if (audio.type === "bytes") {
5631
+ if (!(audio.data instanceof Uint8Array) || audio.data.byteLength === 0 || !nonEmptyString(audio.mediaType)) {
5632
+ throw new InvalidTranscriptionInputError(
5633
+ "Inline transcription bytes require nonempty data and mediaType"
5634
+ );
5635
+ }
5636
+ if (audio.data.byteLength > MAX_INLINE_TRANSCRIPTION_BYTES) {
5637
+ throw new InvalidTranscriptionInputError(
5638
+ "Inline transcription audio must not exceed 25 MiB; chunk it or use a supported provider-file source"
5639
+ );
5640
+ }
5641
+ normalizedAudio = {
5642
+ type: "bytes",
5643
+ data: new Uint8Array(audio.data),
5644
+ mediaType: audio.mediaType
5645
+ };
5646
+ } else if (audio.type === "base64") {
5647
+ if (!nonEmptyString(audio.data) || !nonEmptyString(audio.mediaType)) {
5648
+ throw new InvalidTranscriptionInputError(
5649
+ "Inline base64 transcription audio requires data and mediaType"
5650
+ );
5651
+ }
5652
+ if (audio.data.length > 4 * Math.ceil(MAX_INLINE_TRANSCRIPTION_BYTES / 3)) {
5653
+ throw new InvalidTranscriptionInputError(
5654
+ "Inline transcription audio must not exceed 25 MiB; chunk it or use a supported provider-file source"
5655
+ );
5656
+ }
5657
+ let decoded;
5658
+ try {
5659
+ decoded = Uint8Array.from(Buffer.from(audio.data, "base64"));
5660
+ } catch {
5661
+ throw new InvalidTranscriptionInputError(
5662
+ "Transcription base64 must be canonical and decode to audio"
5663
+ );
5664
+ }
5665
+ if (!decoded.byteLength || Buffer.from(decoded).toString("base64") !== audio.data) {
5666
+ throw new InvalidTranscriptionInputError(
5667
+ "Transcription base64 must be canonical and decode to nonempty audio"
5668
+ );
5669
+ }
5670
+ if (decoded.byteLength > MAX_INLINE_TRANSCRIPTION_BYTES) {
5671
+ throw new InvalidTranscriptionInputError(
5672
+ "Inline transcription audio must not exceed 25 MiB; chunk it or use a supported provider-file source"
5673
+ );
5674
+ }
5675
+ normalizedAudio = { type: "base64", data: audio.data, mediaType: audio.mediaType };
5676
+ } else if (audio.type === "provider-file") {
5677
+ if (!nonEmptyString(audio.provider) || !nonEmptyString(audio.reference)) {
5678
+ throw new InvalidTranscriptionInputError(
5679
+ "A provider-file transcription source requires provider and reference"
5680
+ );
5681
+ }
5682
+ if (audio.mediaType !== void 0 && !nonEmptyString(audio.mediaType)) {
5683
+ throw new InvalidTranscriptionInputError(
5684
+ "Transcription provider-file mediaType must be nonempty when supplied"
5685
+ );
5686
+ }
5687
+ normalizedAudio = {
5688
+ type: "provider-file",
5689
+ provider: audio.provider,
5690
+ reference: audio.reference,
5691
+ ...audio.mediaType !== void 0 ? { mediaType: audio.mediaType } : {}
5692
+ };
5693
+ } else {
5694
+ throw new InvalidTranscriptionInputError(
5695
+ "Transcription audio must be bytes, base64, or provider-file"
5696
+ );
5697
+ }
5698
+ if (request.language !== void 0 && !nonEmptyString(request.language)) {
5699
+ throw new InvalidTranscriptionInputError(
5700
+ "Transcription language must be nonempty when supplied"
5701
+ );
5702
+ }
5703
+ if (request.timestamps !== void 0 && request.timestamps !== "segment" && request.timestamps !== "word") {
5704
+ throw new InvalidTranscriptionInputError("Transcription timestamps must be segment or word");
5705
+ }
5706
+ if (request.diarization !== void 0 && typeof request.diarization !== "boolean") {
5707
+ throw new InvalidTranscriptionInputError("Transcription diarization must be boolean");
5708
+ }
5709
+ if (request.providerOptions !== void 0) {
5710
+ if (!request.providerOptions || Array.isArray(request.providerOptions) || typeof request.providerOptions !== "object") {
5711
+ throw new InvalidTranscriptionInputError("Transcription providerOptions must be an object");
5712
+ }
5713
+ for (const key of Object.keys(request.providerOptions)) {
5714
+ if (RESERVED_PROVIDER_OPTION_KEYS.has(key)) {
5715
+ throw new InvalidTranscriptionInputError(
5716
+ "Transcription providerOptions cannot override reserved operation fields"
5717
+ );
5718
+ }
5719
+ }
5720
+ }
5721
+ return {
5722
+ model: request.model,
5723
+ audio: normalizedAudio,
5724
+ ...request.language !== void 0 ? { language: request.language } : {},
5725
+ ...request.timestamps !== void 0 ? { timestamps: request.timestamps } : {},
5726
+ ...request.diarization !== void 0 ? { diarization: request.diarization } : {},
5727
+ ...request.providerOptions !== void 0 ? { providerOptions: cloneValue(request.providerOptions) } : {}
5728
+ };
5729
+ }
5730
+ function cloneTranscript(transcript) {
5731
+ if (!transcript || typeof transcript !== "object" || typeof transcript.text !== "string") {
5732
+ throw new InvalidTranscriptionInputError(
5733
+ "Transcription provider returned an invalid transcript"
5734
+ );
5735
+ }
5736
+ const cloned = cloneValue(transcript);
5737
+ const accounting = normalizeTranscriptionAccounting(cloned.usage, cloned.pricingStatus);
5738
+ return {
5739
+ text: cloned.text,
5740
+ ...cloned.detectedLanguages ? { detectedLanguages: cloned.detectedLanguages } : {},
5741
+ ...cloned.segments ? { segments: cloned.segments } : {},
5742
+ ...cloned.words ? { words: cloned.words } : {},
5743
+ ...accounting.usage ? { usage: accounting.usage } : {},
5744
+ ...accounting.pricingStatus ? { pricingStatus: accounting.pricingStatus } : {},
5745
+ ...cloned.providerMetadata ? { providerMetadata: cloned.providerMetadata } : {}
5746
+ };
5747
+ }
5748
+ function normalizeTranscriptionAccounting(usageValue, pricingValue) {
5749
+ const value = usageValue && typeof usageValue === "object" ? usageValue : void 0;
5750
+ const number = (key) => {
5751
+ const candidate = value?.[key];
5752
+ return typeof candidate === "number" && Number.isFinite(candidate) && candidate >= 0 ? candidate : void 0;
5753
+ };
5754
+ const audioSeconds = number("audioSeconds");
5755
+ const inputTokens = number("inputTokens");
5756
+ const outputTokens = number("outputTokens");
5757
+ const totalTokens = number("totalTokens");
5758
+ const reportedCost = number("cost");
5759
+ const usage = value && (audioSeconds !== void 0 || inputTokens !== void 0 || outputTokens !== void 0 || totalTokens !== void 0 || reportedCost !== void 0) ? {
5760
+ ...audioSeconds !== void 0 ? { audioSeconds } : {},
5761
+ ...inputTokens !== void 0 ? { inputTokens } : {},
5762
+ ...outputTokens !== void 0 ? { outputTokens } : {},
5763
+ ...totalTokens !== void 0 ? { totalTokens } : {},
5764
+ ...reportedCost !== void 0 ? { cost: reportedCost } : {}
5765
+ } : void 0;
5766
+ const hasWork = (audioSeconds ?? 0) > 0 || (inputTokens ?? 0) > 0 || (outputTokens ?? 0) > 0 || (totalTokens ?? 0) > 0;
5767
+ const requested = pricingValue === "priced" || pricingValue === "unpriced" || pricingValue === "zero" ? pricingValue : void 0;
5768
+ const cost = requested === "zero" && (reportedCost === void 0 || reportedCost === 0) ? 0 : reportedCost;
5769
+ const pricingStatus = requested === "zero" && (reportedCost === void 0 || reportedCost === 0) ? "zero" : requested === "priced" && cost !== void 0 ? "priced" : requested === "unpriced" && cost === void 0 ? "unpriced" : cost !== void 0 ? "priced" : hasWork || requested === "unpriced" || requested === "priced" ? "unpriced" : void 0;
5770
+ const tokens = inputTokens !== void 0 || outputTokens !== void 0 ? {
5771
+ ...inputTokens !== void 0 ? { input: inputTokens } : {},
5772
+ ...outputTokens !== void 0 ? { output: outputTokens } : {}
5773
+ } : void 0;
5774
+ return {
5775
+ ...usage ? { usage } : {},
5776
+ ...pricingStatus ? { pricingStatus } : {},
5777
+ ...cost !== void 0 ? { cost } : {},
5778
+ ...tokens ? { tokens } : {},
5779
+ hasWork
5780
+ };
5781
+ }
5782
+
5783
+ // src/providers/transcription-utils.ts
5784
+ var OPENAI_TRANSCRIPTION_MODEL = "gpt-transcribe";
5785
+ var GEMINI_TRANSCRIPTION_MODEL = "gemini-3.5-transcribe";
5786
+ var OPENROUTER_TRANSCRIPTION_MODEL = "openai/whisper-1";
5787
+ function requireExactTranscriptionModel(provider, model, expected) {
5788
+ if (model !== expected) {
5789
+ throw new UnsupportedTranscriptionInputError({
5790
+ provider,
5791
+ model,
5792
+ feature: "this transcription model"
5793
+ });
5794
+ }
5795
+ }
5796
+ function inlineAudioBytes(audio) {
5797
+ if (audio.type === "bytes") return new Uint8Array(audio.data);
5798
+ if (audio.type === "base64") return Uint8Array.from(Buffer.from(audio.data, "base64"));
5799
+ throw new UnsupportedTranscriptionInputError({
5800
+ provider: audio.provider,
5801
+ model: "",
5802
+ feature: "provider-file transcription input"
5803
+ });
5804
+ }
5805
+ function requireNoProviderFile(audio, provider, model) {
5806
+ if (audio.type === "provider-file") {
5807
+ throw new UnsupportedTranscriptionInputError({
5808
+ provider,
5809
+ model,
5810
+ feature: "provider-file transcription input"
5811
+ });
5812
+ }
5813
+ }
5814
+ function assertOnlyProviderOptions(request, provider, model, allowed) {
5815
+ const options = request.providerOptions ?? {};
5816
+ for (const key of Object.keys(options)) {
5817
+ if (!allowed.includes(key)) {
5818
+ throw new InvalidTranscriptionInputError(
5819
+ `Unsupported ${provider} transcription provider option for model '${model}'`
5820
+ );
5821
+ }
5822
+ }
5823
+ return options;
5824
+ }
5825
+ function transcriptUsage(value) {
5826
+ const source = value && typeof value === "object" ? value : {};
5827
+ const number = (...keys) => {
5828
+ for (const key of keys) {
5829
+ const candidate = source[key];
5830
+ if (typeof candidate === "number" && Number.isFinite(candidate) && candidate >= 0)
5831
+ return candidate;
5832
+ }
5833
+ return void 0;
5834
+ };
5835
+ const inputTokens = number("total_input_tokens", "input_tokens", "prompt_tokens", "inputTokens");
5836
+ const outputTokens = number(
5837
+ "total_output_tokens",
5838
+ "output_tokens",
5839
+ "completion_tokens",
5840
+ "outputTokens"
5841
+ );
5842
+ const totalTokens = number("total_tokens", "totalTokens");
5843
+ const audioSeconds = number("seconds", "audio_seconds", "audioSeconds", "duration_seconds");
5844
+ const cost = number("cost");
5845
+ const usage = inputTokens !== void 0 || outputTokens !== void 0 || totalTokens !== void 0 || audioSeconds !== void 0 || cost !== void 0 ? {
5846
+ ...audioSeconds !== void 0 ? { audioSeconds } : {},
5847
+ ...inputTokens !== void 0 ? { inputTokens } : {},
5848
+ ...outputTokens !== void 0 ? { outputTokens } : {},
5849
+ ...totalTokens !== void 0 ? { totalTokens } : {},
5850
+ ...cost !== void 0 ? { cost } : {}
5851
+ } : void 0;
5852
+ const hasWork = (audioSeconds ?? 0) > 0 || (inputTokens ?? 0) > 0 || (outputTokens ?? 0) > 0 || (totalTokens ?? 0) > 0;
5853
+ return {
5854
+ ...usage ? { usage } : {},
5855
+ ...cost !== void 0 ? { pricingStatus: "priced" } : hasWork ? { pricingStatus: "unpriced" } : {}
5856
+ };
5857
+ }
5858
+ function safeProviderMessage(provider, status, body) {
5859
+ try {
5860
+ const parsed = JSON.parse(body);
5861
+ const message = parsed.error?.message ?? parsed.message;
5862
+ if (typeof message === "string")
5863
+ return `${provider} transcription API error (${status}): ${message}`;
5864
+ } catch {
5865
+ }
5866
+ return `${provider} transcription API error (${status})`;
5867
+ }
5868
+ function attachTranscriptionFailureMetadata(error, metadata) {
5869
+ for (const [key, value] of Object.entries(metadata)) {
5870
+ if (value !== void 0) Object.defineProperty(error, key, { enumerable: true, value });
5871
+ }
5872
+ return error;
5873
+ }
5874
+
5875
+ // src/providers/openai-transcription.ts
5876
+ var OPENAI_FILE_EXTENSIONS = {
5877
+ "audio/flac": "flac",
5878
+ "audio/m4a": "m4a",
5879
+ "audio/mp3": "mp3",
5880
+ "audio/mp4": "mp4",
5881
+ "audio/mpeg": "mp3",
5882
+ "audio/mpga": "mpga",
5883
+ "audio/ogg": "ogg",
5884
+ "audio/wav": "wav",
5885
+ "audio/webm": "webm"
5886
+ };
5887
+ var OpenAITranscriptionProvider = class {
5888
+ name = "openai-transcription";
5889
+ baseUrl;
5890
+ apiKeySource;
5891
+ governor;
5892
+ constructor(options = {}) {
5893
+ this.apiKeySource = options.apiKey ?? process.env.OPENAI_API_KEY ?? "";
5894
+ this.baseUrl = (options.baseUrl ?? process.env.OPENAI_BASE_URL ?? "https://api.openai.com/v1").replace(/\/$/, "");
5895
+ assertSafeProviderBaseUrl(
5896
+ this.baseUrl,
5897
+ "OpenAI transcription provider",
5898
+ options.dangerouslyAllowInsecureHttp
5899
+ );
5900
+ this.governor = options.rateLimit ? new RateLimiter(options.rateLimit) : void 0;
5901
+ }
5902
+ capabilities(model) {
5903
+ return model === OPENAI_TRANSCRIPTION_MODEL ? { sources: ["bytes", "base64"] } : void 0;
5904
+ }
5905
+ async transcribe(request) {
5906
+ requireExactTranscriptionModel(this.name, request.model, OPENAI_TRANSCRIPTION_MODEL);
5907
+ requireNoProviderFile(request.audio, this.name, request.model);
5908
+ if (request.audio.type === "provider-file")
5909
+ throw new UnsupportedTranscriptionInputError({
5910
+ provider: this.name,
5911
+ model: request.model,
5912
+ feature: "provider-file transcription input"
5913
+ });
5914
+ const options = assertOnlyProviderOptions(request, this.name, request.model, [
5915
+ "temperature",
5916
+ "prompt"
5917
+ ]);
5918
+ if (options.temperature !== void 0 && (typeof options.temperature !== "number" || !Number.isFinite(options.temperature) || options.temperature < 0 || options.temperature > 1))
5919
+ throw new InvalidTranscriptionInputError(
5920
+ "OpenAI transcription temperature must be a number between 0 and 1"
5921
+ );
5922
+ if (options.prompt !== void 0 && typeof options.prompt !== "string")
5923
+ throw new InvalidTranscriptionInputError("OpenAI transcription prompt must be a string");
5924
+ const mediaType2 = request.audio.mediaType;
5925
+ const extension = OPENAI_FILE_EXTENSIONS[mediaType2];
5926
+ if (!extension)
5927
+ throw new UnsupportedTranscriptionInputError({
5928
+ provider: this.name,
5929
+ model: request.model,
5930
+ feature: "this audio media type"
5931
+ });
5932
+ const bytes = inlineAudioBytes(request.audio);
5933
+ if (bytes.byteLength > MAX_INLINE_TRANSCRIPTION_BYTES)
5934
+ throw new InvalidTranscriptionInputError("OpenAI transcription audio must not exceed 25 MiB");
5935
+ const form = new FormData();
5936
+ form.set("file", new Blob([Buffer.from(bytes)], { type: mediaType2 }), `audio.${extension}`);
5937
+ form.set("model", request.model);
5938
+ form.set("response_format", "json");
5939
+ if (request.language) form.set("language", request.language);
5940
+ if (options.temperature !== void 0) form.set("temperature", String(options.temperature));
5941
+ if (options.prompt !== void 0) form.set("prompt", options.prompt);
5942
+ const key = await this.resolveKey();
5943
+ const res = await fetchWithRetry(
5944
+ `${this.baseUrl}/audio/transcriptions`,
5945
+ {
5946
+ method: "POST",
5947
+ headers: { Authorization: `Bearer ${key}` },
5948
+ body: form,
5949
+ signal: request.signal
5950
+ },
5951
+ { governor: this.governor, provider: this.name }
5952
+ );
5953
+ if (!res.ok) {
5954
+ const body = await res.text();
5955
+ throw buildProviderError({
5956
+ provider: this.name,
5957
+ status: res.status,
5958
+ headers: res.headers,
5959
+ message: safeProviderMessage("OpenAI", res.status, body),
5960
+ body
5961
+ });
5962
+ }
5963
+ const json = await res.json();
5964
+ if (typeof json.text !== "string")
5965
+ throw new InvalidTranscriptionInputError(
5966
+ "OpenAI transcription response did not contain text"
5967
+ );
5968
+ return {
5969
+ transcript: { text: json.text, ...transcriptUsage(json.usage) },
5970
+ cleanupStatus: "not_required"
5971
+ };
5972
+ }
5973
+ async resolveKey() {
5974
+ const key = await resolveApiKey(this.apiKeySource);
5975
+ if (!key)
5976
+ throw new Error("OpenAI API key is required. Set OPENAI_API_KEY or pass apiKey in options.");
5977
+ return key;
5978
+ }
5979
+ };
5980
+
5981
+ // src/providers/gemini-transcription.ts
5982
+ var CLEANUP_TIMEOUT_MS = 3e3;
5983
+ var READY_TIMEOUT_MS = 3e4;
5984
+ var READY_POLL_MS = 250;
5985
+ var GEMINI_AUDIO_MEDIA_TYPES = /* @__PURE__ */ new Set([
5986
+ "audio/wav",
5987
+ "audio/mp3",
5988
+ "audio/aiff",
5989
+ "audio/aac",
5990
+ "audio/ogg",
5991
+ "audio/flac",
5992
+ "audio/mpeg",
5993
+ "audio/m4a",
5994
+ "audio/l16",
5995
+ "audio/opus",
5996
+ "audio/alaw",
5997
+ "audio/mulaw",
5998
+ "audio/webm"
5999
+ ]);
6000
+ function abortError2() {
6001
+ return new DOMException("The transcription operation was aborted", "AbortError");
6002
+ }
6003
+ function audioMime(audio) {
6004
+ if (!audio.mediaType)
6005
+ throw new UnsupportedTranscriptionInputError({
6006
+ provider: "gemini-transcription",
6007
+ model: GEMINI_TRANSCRIPTION_MODEL,
6008
+ feature: "provider-file audio mediaType"
6009
+ });
6010
+ return audio.mediaType;
6011
+ }
6012
+ function assertGeminiAudioMime(mediaType2) {
6013
+ if (!mediaType2 || !GEMINI_AUDIO_MEDIA_TYPES.has(mediaType2)) {
6014
+ throw new UnsupportedTranscriptionInputError({
6015
+ provider: "gemini-transcription",
6016
+ model: GEMINI_TRANSCRIPTION_MODEL,
6017
+ feature: "this audio media type"
6018
+ });
6019
+ }
6020
+ return mediaType2;
6021
+ }
6022
+ function secondsOffset(value) {
6023
+ if (typeof value !== "string") return void 0;
6024
+ const match = /^(\d+(?:\.\d+)?)s$/.exec(value);
6025
+ if (!match) return void 0;
6026
+ const seconds = Number(match[1]);
6027
+ return Number.isFinite(seconds) && seconds >= 0 ? seconds : void 0;
6028
+ }
6029
+ function sleep3(ms, signal) {
6030
+ return new Promise((resolve2, reject) => {
6031
+ if (signal?.aborted) {
6032
+ reject(abortError2());
6033
+ return;
6034
+ }
6035
+ const timer = setTimeout(done, ms);
6036
+ const onAbort = () => {
6037
+ clearTimeout(timer);
6038
+ done(abortError2());
6039
+ };
6040
+ function done(error) {
6041
+ signal?.removeEventListener("abort", onAbort);
6042
+ if (error) reject(error);
6043
+ else resolve2();
6044
+ }
6045
+ signal?.addEventListener("abort", onAbort, { once: true });
6046
+ });
6047
+ }
6048
+ var GeminiTranscriptionProvider = class {
6049
+ name = "gemini-transcription";
6050
+ baseUrl;
6051
+ apiKeySource;
6052
+ governor;
6053
+ constructor(options = {}) {
6054
+ this.apiKeySource = options.apiKey ?? process.env.GOOGLE_API_KEY ?? process.env.GEMINI_API_KEY ?? "";
6055
+ this.baseUrl = (options.baseUrl ?? "https://generativelanguage.googleapis.com/v1beta").replace(
6056
+ /\/$/,
6057
+ ""
6058
+ );
6059
+ assertSafeProviderBaseUrl(
6060
+ this.baseUrl,
6061
+ "Gemini transcription provider",
6062
+ options.dangerouslyAllowInsecureHttp
6063
+ );
6064
+ this.governor = options.rateLimit ? new RateLimiter(options.rateLimit) : void 0;
6065
+ }
6066
+ capabilities(model) {
6067
+ return model === GEMINI_TRANSCRIPTION_MODEL ? { sources: ["bytes", "base64", "provider-file"], timestamps: ["word"], diarization: true } : void 0;
6068
+ }
6069
+ async transcribe(request) {
6070
+ requireExactTranscriptionModel(this.name, request.model, GEMINI_TRANSCRIPTION_MODEL);
6071
+ const options = assertOnlyProviderOptions(request, this.name, request.model, [
6072
+ "mode",
6073
+ "customVocabulary"
6074
+ ]);
6075
+ const mode = options.mode ?? "verbatim";
6076
+ if (mode !== "smart" && mode !== "verbatim")
6077
+ throw new InvalidTranscriptionInputError(
6078
+ "Gemini transcription mode must be smart or verbatim"
6079
+ );
6080
+ if (options.customVocabulary !== void 0 && (!Array.isArray(options.customVocabulary) || options.customVocabulary.length > 1e3 || options.customVocabulary.some(
6081
+ (value) => typeof value !== "string" || !value.trim() || value.length > 256
6082
+ )))
6083
+ throw new InvalidTranscriptionInputError(
6084
+ "Gemini transcription customVocabulary must contain at most 1000 nonempty strings up to 256 characters"
6085
+ );
6086
+ if (mode === "smart" && (request.timestamps || request.diarization))
6087
+ throw new InvalidTranscriptionInputError(
6088
+ "Gemini smart transcription does not support timestamps or diarization"
6089
+ );
6090
+ if (request.timestamps && options.customVocabulary !== void 0)
6091
+ throw new InvalidTranscriptionInputError(
6092
+ "Gemini transcription customVocabulary cannot be combined with timestamps"
6093
+ );
6094
+ if (request.timestamps && request.timestamps !== "word")
6095
+ throw new UnsupportedTranscriptionInputError({
6096
+ provider: this.name,
6097
+ model: request.model,
6098
+ feature: "segment timestamps"
6099
+ });
6100
+ if (request.diarization && request.timestamps !== "word")
6101
+ throw new InvalidTranscriptionInputError("Gemini diarization requires word timestamps");
6102
+ if (request.audio.type === "provider-file") {
6103
+ if (request.audio.provider !== this.name || typeof request.audio.reference !== "string" || !request.audio.reference.trim()) {
6104
+ throw new UnsupportedTranscriptionInputError({
6105
+ provider: this.name,
6106
+ model: request.model,
6107
+ feature: "a matching nonempty provider-file reference"
6108
+ });
6109
+ }
6110
+ assertGeminiAudioMime(request.audio.mediaType);
6111
+ } else {
6112
+ assertGeminiAudioMime(request.audio.mediaType);
6113
+ }
6114
+ const key = await this.resolveKey();
6115
+ if (request.audio.type === "provider-file")
6116
+ return this.transcribeReference(request, key, mode, options);
6117
+ return this.transcribeUpload(request, key, mode, options);
6118
+ }
6119
+ async transcribeReference(request, key, mode, options) {
6120
+ const audio = request.audio;
6121
+ if (audio.type !== "provider-file" || audio.provider !== this.name)
6122
+ throw new UnsupportedTranscriptionInputError({
6123
+ provider: this.name,
6124
+ model: request.model,
6125
+ feature: "a provider-file owned by another provider"
6126
+ });
6127
+ const mimeType = audioMime(audio);
6128
+ const transcript = await this.infer(request, key, audio.reference, mimeType, mode, options);
6129
+ return { transcript, cleanupStatus: "not_required" };
6130
+ }
6131
+ async transcribeUpload(request, key, mode, options) {
6132
+ let created = false;
6133
+ let fileName;
6134
+ let primaryError;
6135
+ let transcript;
6136
+ let cleanupStatus = "not_required";
6137
+ try {
6138
+ const uploaded = await this.upload(request, key);
6139
+ created = true;
6140
+ fileName = uploaded.name;
6141
+ const ready = await this.waitForReady(
6142
+ this.fileFromJson(uploaded.file, uploaded.mimeType),
6143
+ key,
6144
+ request.signal
6145
+ );
6146
+ transcript = await this.infer(request, key, ready.uri, ready.mimeType, mode, options);
6147
+ } catch (error) {
6148
+ primaryError = error;
6149
+ } finally {
6150
+ if (created && fileName) cleanupStatus = await this.cleanup(fileName, key);
6151
+ }
6152
+ if (primaryError) {
6153
+ throw attachTranscriptionFailureMetadata(
6154
+ primaryError instanceof Error ? primaryError : new Error("Gemini transcription failed"),
6155
+ { cleanupStatus }
6156
+ );
6157
+ }
6158
+ if (!transcript)
6159
+ throw new InvalidTranscriptionInputError(
6160
+ "Gemini transcription response did not contain text"
6161
+ );
6162
+ return { transcript, cleanupStatus };
6163
+ }
6164
+ /** A finalization response with a name creates a caller-owned cleanup obligation
6165
+ * even when its URI/state is malformed. `transcribeUpload` stores the name before
6166
+ * strictly parsing the rest of the file representation. */
6167
+ async upload(request, key) {
6168
+ if (request.audio.type === "provider-file")
6169
+ throw new UnsupportedTranscriptionInputError({
6170
+ provider: this.name,
6171
+ model: request.model,
6172
+ feature: "provider-file upload"
6173
+ });
6174
+ const mimeType = assertGeminiAudioMime(request.audio.mediaType);
6175
+ const bytes = inlineAudioBytes(request.audio);
6176
+ const start = await fetchWithRetry(
6177
+ `${this.baseUrl.replace(/\/v1beta$/, "")}/upload/v1beta/files`,
6178
+ {
6179
+ method: "POST",
6180
+ headers: {
6181
+ "x-goog-api-key": key,
6182
+ "X-Goog-Upload-Protocol": "resumable",
6183
+ "X-Goog-Upload-Command": "start",
6184
+ "X-Goog-Upload-Header-Content-Length": String(bytes.byteLength),
6185
+ "X-Goog-Upload-Header-Content-Type": mimeType,
6186
+ "Content-Type": "application/json"
6187
+ },
6188
+ body: JSON.stringify({ file: { display_name: "axl-transcription" } }),
6189
+ signal: request.signal
6190
+ },
6191
+ { governor: this.governor, provider: this.name, maxRetries: 0 }
6192
+ );
6193
+ if (!start.ok) {
6194
+ const body = await start.text();
6195
+ throw buildProviderError({
6196
+ provider: this.name,
6197
+ status: start.status,
6198
+ headers: start.headers,
6199
+ message: safeProviderMessage("Gemini", start.status, body),
6200
+ body
6201
+ });
6202
+ }
6203
+ const uploadUrl = start.headers.get("x-goog-upload-url");
6204
+ if (!uploadUrl)
6205
+ throw new InvalidTranscriptionInputError(
6206
+ "Gemini transcription upload did not return an upload URL"
6207
+ );
6208
+ let parsedUploadUrl;
6209
+ try {
6210
+ parsedUploadUrl = new URL(uploadUrl);
6211
+ } catch {
6212
+ throw new InvalidTranscriptionInputError(
6213
+ "Gemini transcription upload returned an invalid upload URL"
6214
+ );
6215
+ }
6216
+ const baseOrigin = new URL(this.baseUrl).origin;
6217
+ if (parsedUploadUrl.username || parsedUploadUrl.password || parsedUploadUrl.origin !== baseOrigin) {
6218
+ throw new InvalidTranscriptionInputError(
6219
+ "Gemini transcription upload returned an untrusted upload URL"
6220
+ );
6221
+ }
6222
+ assertSafeProviderBaseUrl(
6223
+ parsedUploadUrl.toString(),
6224
+ "Gemini transcription upload endpoint",
6225
+ new URL(this.baseUrl).protocol === "http:"
6226
+ );
6227
+ const finalize = await fetchWithRetry(
6228
+ parsedUploadUrl.toString(),
6229
+ {
6230
+ method: "POST",
6231
+ headers: {
6232
+ "X-Goog-Upload-Command": "upload, finalize",
6233
+ "X-Goog-Upload-Offset": "0",
6234
+ "Content-Length": String(bytes.byteLength)
6235
+ },
6236
+ body: bytes,
6237
+ signal: request.signal
6238
+ },
6239
+ { governor: this.governor, provider: this.name, maxRetries: 0 }
6240
+ );
6241
+ if (!finalize.ok) {
6242
+ const body = await finalize.text();
6243
+ throw buildProviderError({
6244
+ provider: this.name,
6245
+ status: finalize.status,
6246
+ headers: finalize.headers,
6247
+ message: safeProviderMessage("Gemini", finalize.status, body),
6248
+ body
6249
+ });
6250
+ }
6251
+ const payload = await finalize.json();
6252
+ const file = payload.file;
6253
+ if (!file || typeof file.name !== "string") {
6254
+ throw new InvalidTranscriptionInputError(
6255
+ "Gemini transcription upload did not return a file name"
6256
+ );
6257
+ }
6258
+ return { name: file.name, file, mimeType };
6259
+ }
6260
+ async waitForReady(file, key, signal) {
6261
+ let current = file;
6262
+ const deadline = Date.now() + READY_TIMEOUT_MS;
6263
+ if (!current.state) {
6264
+ throw new InvalidTranscriptionInputError(
6265
+ "Gemini transcription file readiness state is missing"
6266
+ );
6267
+ }
6268
+ while (current.state && current.state !== "ACTIVE") {
6269
+ if (current.state === "FAILED")
6270
+ throw new InvalidTranscriptionInputError("Gemini transcription file processing failed");
6271
+ if (Date.now() >= deadline)
6272
+ throw new InvalidTranscriptionInputError("Gemini transcription file readiness timed out");
6273
+ await sleep3(Math.min(READY_POLL_MS, deadline - Date.now()), signal);
6274
+ const res = await fetchWithRetry(
6275
+ `${this.baseUrl}/${current.name}`,
6276
+ { method: "GET", headers: { "x-goog-api-key": key }, signal },
6277
+ { governor: this.governor, provider: this.name, maxRetries: 0 }
6278
+ );
6279
+ if (!res.ok) {
6280
+ const body = await res.text();
6281
+ throw buildProviderError({
6282
+ provider: this.name,
6283
+ status: res.status,
6284
+ headers: res.headers,
6285
+ message: safeProviderMessage("Gemini", res.status, body),
6286
+ body
6287
+ });
6288
+ }
6289
+ current = this.fileFromJson(await res.json(), current.mimeType);
6290
+ }
6291
+ return current;
6292
+ }
6293
+ async infer(request, key, uri, mimeType, mode, options) {
6294
+ const transcriptionConfig = {
6295
+ mode: mode === "smart" ? "smart" : {
6296
+ type: "verbatim",
6297
+ ...request.timestamps ? { timestamp_granularities: ["word"] } : {},
6298
+ ...request.diarization ? { diarization_mode: "speaker" } : {}
6299
+ },
6300
+ ...request.language ? { language_codes: [request.language] } : {},
6301
+ ...options.customVocabulary ? { custom_vocabulary: options.customVocabulary } : {}
6302
+ };
6303
+ const body = {
6304
+ model: request.model,
6305
+ input: [{ type: "audio", uri, mime_type: mimeType }],
6306
+ store: false,
6307
+ generation_config: { transcription_config: transcriptionConfig }
6308
+ };
6309
+ const res = await fetchWithRetry(
6310
+ `${this.baseUrl}/interactions`,
6311
+ {
6312
+ method: "POST",
6313
+ headers: { "x-goog-api-key": key, "Content-Type": "application/json" },
6314
+ body: JSON.stringify(body),
6315
+ signal: request.signal
6316
+ },
6317
+ { governor: this.governor, provider: this.name }
6318
+ );
6319
+ if (!res.ok) {
6320
+ const errorBody = await res.text();
6321
+ throw buildProviderError({
6322
+ provider: this.name,
6323
+ status: res.status,
6324
+ headers: res.headers,
6325
+ message: safeProviderMessage("Gemini", res.status, errorBody),
6326
+ body: errorBody
6327
+ });
6328
+ }
6329
+ const json = await res.json();
6330
+ if (json.status !== "completed") {
6331
+ throw new InvalidTranscriptionInputError("Gemini transcription interaction did not complete");
6332
+ }
6333
+ const words = [];
6334
+ let text = "";
6335
+ let hasModelOutputText = false;
6336
+ if (Array.isArray(json.steps)) {
6337
+ for (const step of json.steps) {
6338
+ if (!step || typeof step !== "object") continue;
6339
+ const modelOutput = step;
6340
+ if (modelOutput.type !== "model_output" || !Array.isArray(modelOutput.content)) continue;
6341
+ for (const block of modelOutput.content) {
6342
+ if (!block || typeof block !== "object") continue;
6343
+ const content = block;
6344
+ if (content.type !== "text" || typeof content.text !== "string") continue;
6345
+ hasModelOutputText = true;
6346
+ text += content.text;
6347
+ if (!Array.isArray(content.annotations)) continue;
6348
+ for (const raw of content.annotations) {
6349
+ if (!raw || typeof raw !== "object") continue;
6350
+ const annotation = raw;
6351
+ if (annotation.type !== "word_info") continue;
6352
+ if (typeof annotation.text !== "string") continue;
6353
+ const start = secondsOffset(annotation.start_offset);
6354
+ const end = secondsOffset(annotation.end_offset);
6355
+ if (start === void 0 || end === void 0 || end < start) continue;
6356
+ words.push({
6357
+ text: annotation.text,
6358
+ start,
6359
+ end,
6360
+ ...typeof annotation.speaker === "string" ? { speaker: annotation.speaker } : {}
6361
+ });
6362
+ }
6363
+ }
6364
+ }
6365
+ }
6366
+ if (!hasModelOutputText)
6367
+ throw new InvalidTranscriptionInputError(
6368
+ "Gemini transcription response did not contain model output text"
6369
+ );
6370
+ return { text, ...words.length ? { words } : {}, ...transcriptUsage(json.usage) };
6371
+ }
6372
+ fileFromJson(value, fallbackMimeType) {
6373
+ const file = "file" in value && value.file ? value.file : value;
6374
+ if (typeof file.name !== "string" || typeof file.uri !== "string")
6375
+ throw new InvalidTranscriptionInputError(
6376
+ "Gemini transcription upload did not return a file reference"
6377
+ );
6378
+ return {
6379
+ name: file.name,
6380
+ uri: file.uri,
6381
+ mimeType: typeof file.mimeType === "string" ? file.mimeType : typeof file.mime_type === "string" ? file.mime_type : fallbackMimeType,
6382
+ ...typeof file.state === "string" ? { state: file.state } : {}
6383
+ };
6384
+ }
6385
+ async cleanup(fileName, key) {
6386
+ const controller = new AbortController();
6387
+ const timer = setTimeout(() => controller.abort(), CLEANUP_TIMEOUT_MS);
6388
+ try {
6389
+ const res = await fetchWithRetry(
6390
+ `${this.baseUrl}/${fileName}`,
6391
+ { method: "DELETE", headers: { "x-goog-api-key": key }, signal: controller.signal },
6392
+ { governor: this.governor, provider: this.name, maxRetries: 0 }
6393
+ );
6394
+ return res.ok ? "deleted" : "failed";
6395
+ } catch {
6396
+ return controller.signal.aborted ? "timed_out" : "failed";
6397
+ } finally {
6398
+ clearTimeout(timer);
6399
+ }
6400
+ }
6401
+ async resolveKey() {
6402
+ const key = await resolveApiKey(this.apiKeySource);
6403
+ if (!key)
6404
+ throw new Error("Google API key is required. Set GOOGLE_API_KEY or pass apiKey in options.");
6405
+ return key;
6406
+ }
6407
+ };
6408
+
6409
+ // src/providers/openrouter-transcription.ts
6410
+ var FORMAT_BY_MIME = {
6411
+ "audio/aac": "aac",
6412
+ "audio/flac": "flac",
6413
+ "audio/m4a": "m4a",
6414
+ "audio/mp3": "mp3",
6415
+ "audio/mp4": "mp4",
6416
+ "audio/mpeg": "mp3",
6417
+ "audio/ogg": "ogg",
6418
+ "audio/wav": "wav",
6419
+ "audio/webm": "webm"
6420
+ };
6421
+ var OpenRouterTranscriptionProvider = class {
6422
+ name = "openrouter-transcription";
6423
+ baseUrl;
6424
+ apiKeySource;
6425
+ governor;
6426
+ constructor(options = {}) {
6427
+ this.apiKeySource = options.apiKey ?? process.env.OPENROUTER_API_KEY ?? "";
6428
+ this.baseUrl = (options.baseUrl ?? process.env.OPENROUTER_BASE_URL ?? "https://openrouter.ai/api/v1").replace(/\/$/, "");
6429
+ assertSafeProviderBaseUrl(
6430
+ this.baseUrl,
6431
+ "OpenRouter transcription provider",
6432
+ options.dangerouslyAllowInsecureHttp
6433
+ );
6434
+ this.governor = options.rateLimit ? new RateLimiter(options.rateLimit) : void 0;
6435
+ }
6436
+ capabilities(model) {
6437
+ return model === OPENROUTER_TRANSCRIPTION_MODEL ? { sources: ["bytes", "base64"] } : void 0;
6438
+ }
6439
+ async transcribe(request) {
6440
+ requireExactTranscriptionModel(this.name, request.model, OPENROUTER_TRANSCRIPTION_MODEL);
6441
+ requireNoProviderFile(request.audio, this.name, request.model);
6442
+ if (request.audio.type === "provider-file")
6443
+ throw new UnsupportedTranscriptionInputError({
6444
+ provider: this.name,
6445
+ model: request.model,
6446
+ feature: "provider-file transcription input"
6447
+ });
6448
+ const options = assertOnlyProviderOptions(request, this.name, request.model, [
6449
+ "temperature",
6450
+ "provider"
6451
+ ]);
6452
+ if (options.temperature !== void 0 && (typeof options.temperature !== "number" || !Number.isFinite(options.temperature) || options.temperature < 0 || options.temperature > 1))
6453
+ throw new InvalidTranscriptionInputError(
6454
+ "OpenRouter transcription temperature must be a number between 0 and 1"
6455
+ );
6456
+ if (options.provider !== void 0 && (!options.provider || Array.isArray(options.provider) || typeof options.provider !== "object"))
6457
+ throw new InvalidTranscriptionInputError(
6458
+ "OpenRouter transcription provider routing must be an object"
6459
+ );
6460
+ const format = FORMAT_BY_MIME[request.audio.mediaType];
6461
+ if (!format)
6462
+ throw new UnsupportedTranscriptionInputError({
6463
+ provider: this.name,
6464
+ model: request.model,
6465
+ feature: "this audio media type"
6466
+ });
6467
+ const bytes = inlineAudioBytes(request.audio);
6468
+ const body = {
6469
+ model: request.model,
6470
+ input_audio: { data: Buffer.from(bytes).toString("base64"), format },
6471
+ ...request.language ? { language: request.language } : {},
6472
+ ...options.temperature !== void 0 ? { temperature: options.temperature } : {},
6473
+ ...options.provider !== void 0 ? { provider: options.provider } : {}
6474
+ };
6475
+ const key = await this.resolveKey();
6476
+ const res = await fetchWithRetry(
6477
+ `${this.baseUrl}/audio/transcriptions`,
6478
+ {
6479
+ method: "POST",
6480
+ headers: { Authorization: `Bearer ${key}`, "Content-Type": "application/json" },
6481
+ body: JSON.stringify(body),
6482
+ signal: request.signal
6483
+ },
6484
+ { governor: this.governor, provider: this.name }
6485
+ );
6486
+ if (!res.ok) {
6487
+ const errorBody = await res.text();
6488
+ throw buildProviderError({
6489
+ provider: this.name,
6490
+ status: res.status,
6491
+ headers: res.headers,
6492
+ message: safeProviderMessage("OpenRouter", res.status, errorBody),
6493
+ body: errorBody
6494
+ });
6495
+ }
6496
+ const json = await res.json();
6497
+ if (typeof json.text !== "string")
6498
+ throw new InvalidTranscriptionInputError(
6499
+ "OpenRouter transcription response did not contain text"
6500
+ );
6501
+ return {
6502
+ transcript: { text: json.text, ...transcriptUsage(json.usage) },
6503
+ cleanupStatus: "not_required"
6504
+ };
6505
+ }
6506
+ async resolveKey() {
6507
+ const key = await resolveApiKey(this.apiKeySource);
6508
+ if (!key)
6509
+ throw new Error(
6510
+ "OpenRouter API key is required. Set OPENROUTER_API_KEY or pass apiKey in options."
6511
+ );
6512
+ return key;
6513
+ }
6514
+ };
6515
+
6516
+ // src/providers/transcription-registry.ts
6517
+ var builtinFactories2 = {
6518
+ "openai-transcription": (config) => {
6519
+ const opts = config.providers?.openai ?? {};
6520
+ return new OpenAITranscriptionProvider({
6521
+ apiKey: opts.apiKey,
6522
+ baseUrl: opts.baseUrl,
6523
+ dangerouslyAllowInsecureHttp: opts.dangerouslyAllowInsecureHttp,
6524
+ rateLimit: opts.rateLimit
6525
+ });
6526
+ },
6527
+ "gemini-transcription": (config) => {
6528
+ const opts = config.providers?.google ?? {};
6529
+ return new GeminiTranscriptionProvider({
6530
+ apiKey: opts.apiKey,
6531
+ baseUrl: opts.baseUrl,
6532
+ dangerouslyAllowInsecureHttp: opts.dangerouslyAllowInsecureHttp,
6533
+ rateLimit: opts.rateLimit
6534
+ });
6535
+ },
6536
+ "openrouter-transcription": (config) => {
6537
+ const opts = config.providers?.openrouter ?? {};
6538
+ return new OpenRouterTranscriptionProvider({
6539
+ apiKey: opts.apiKey,
6540
+ baseUrl: opts.baseUrl,
6541
+ dangerouslyAllowInsecureHttp: opts.dangerouslyAllowInsecureHttp,
6542
+ rateLimit: opts.rateLimit
6543
+ });
6544
+ }
6545
+ };
6546
+ var TranscriptionProviderRegistry = class {
6547
+ instances = /* @__PURE__ */ new Map();
6548
+ factories = /* @__PURE__ */ new Map();
6549
+ constructor() {
6550
+ for (const [name, factory] of Object.entries(builtinFactories2))
6551
+ this.factories.set(name, factory);
6552
+ }
6553
+ register(name, factory) {
6554
+ this.factories.set(name, factory);
6555
+ this.instances.delete(name);
6556
+ }
6557
+ registerInstance(name, provider) {
6558
+ this.instances.set(name, provider);
6559
+ }
6560
+ has(name) {
6561
+ return this.instances.has(name) || this.factories.has(name);
4490
6562
  }
4491
- /**
4492
- * Clear all cached provider instances. Useful for testing or reconfiguration.
4493
- */
4494
- clearCache() {
4495
- this.instances.clear();
6563
+ get(name, config = {}) {
6564
+ const instance = this.instances.get(name);
6565
+ if (instance) return instance;
6566
+ const factory = this.factories.get(name);
6567
+ if (factory) {
6568
+ const created = factory(config);
6569
+ this.instances.set(name, created);
6570
+ return created;
6571
+ }
6572
+ throw new UnsupportedTranscriptionInputError({
6573
+ provider: name,
6574
+ model: "",
6575
+ feature: "transcription"
6576
+ });
4496
6577
  }
4497
- /**
4498
- * Clear all registered factories (including built-ins).
4499
- * Useful for test runtimes where only explicitly registered instances should be used.
4500
- */
4501
- clearFactories() {
4502
- this.factories.clear();
6578
+ resolve(uri, config = {}) {
6579
+ const colon = typeof uri === "string" ? uri.indexOf(":") : -1;
6580
+ if (colon <= 0 || colon === uri.length - 1 || uri.indexOf(":", colon + 1) !== -1) {
6581
+ throw new UnsupportedTranscriptionInputError({
6582
+ provider: "unknown",
6583
+ model: "",
6584
+ feature: "an explicit transcription provider:model URI"
6585
+ });
6586
+ }
6587
+ const providerName = uri.slice(0, colon);
6588
+ const model = uri.slice(colon + 1);
6589
+ return { provider: this.get(providerName, config), model, providerName };
4503
6590
  }
4504
6591
  };
4505
- var defaultRegistry = new ProviderRegistry();
4506
6592
 
4507
6593
  // src/context.ts
4508
6594
  var import_node_async_hooks = require("async_hooks");
4509
6595
  var import_node_crypto = require("crypto");
4510
6596
  var import_zod2 = require("zod");
4511
6597
 
6598
+ // src/input.ts
6599
+ var MAX_INLINE_MODEL_INPUT_BYTES = 25 * 1024 * 1024;
6600
+ function invalid(message) {
6601
+ throw new InvalidModelInputError(message);
6602
+ }
6603
+ function nonEmptyString2(value, field) {
6604
+ if (typeof value !== "string" || value.length === 0)
6605
+ invalid(`${field} must be a non-empty string`);
6606
+ return value;
6607
+ }
6608
+ function mediaType(value, field) {
6609
+ return nonEmptyString2(value, field);
6610
+ }
6611
+ function validBase64(value) {
6612
+ return value.length > 0 && value.length % 4 === 0 && /^[A-Za-z0-9+/]*={0,2}$/.test(value);
6613
+ }
6614
+ function decodedBase64Bytes(value) {
6615
+ return Math.floor(value.length * 3 / 4) - (value.endsWith("==") ? 2 : value.endsWith("=") ? 1 : 0);
6616
+ }
6617
+ function normalizeModelInput(input) {
6618
+ if (typeof input === "string") return input;
6619
+ if (!Array.isArray(input) || input.length === 0)
6620
+ invalid("ModelInput parts must be a non-empty array");
6621
+ let inlineBytes = 0;
6622
+ const reserveInlineBytes = (bytes) => {
6623
+ if (bytes > MAX_INLINE_MODEL_INPUT_BYTES - inlineBytes) {
6624
+ invalid(
6625
+ "Inline image data must not exceed 25 MiB total; use a URL or provider-file source where supported"
6626
+ );
6627
+ }
6628
+ inlineBytes += bytes;
6629
+ };
6630
+ return input.map((part, index) => {
6631
+ if (!part || typeof part !== "object") invalid(`ModelInput part ${index} must be an object`);
6632
+ if (part.type === "text")
6633
+ return { type: "text", text: nonEmptyString2(part.text, `part ${index}.text`) };
6634
+ if (part.type !== "image") invalid(`part ${index}.type must be 'text' or 'image'`);
6635
+ const source = part.source;
6636
+ if (!source || typeof source !== "object") invalid(`part ${index}.source must be an object`);
6637
+ let clone;
6638
+ switch (source.type) {
6639
+ case "url": {
6640
+ const url = nonEmptyString2(source.url, `part ${index}.source.url`);
6641
+ let parsed;
6642
+ try {
6643
+ parsed = new URL(url);
6644
+ } catch {
6645
+ invalid(`part ${index}.source.url must be an http(s) URL`);
6646
+ }
6647
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:")
6648
+ invalid(`part ${index}.source.url must be an http(s) URL`);
6649
+ clone = {
6650
+ type: "url",
6651
+ url,
6652
+ ...source.mediaType === void 0 ? {} : { mediaType: mediaType(source.mediaType, `part ${index}.source.mediaType`) }
6653
+ };
6654
+ break;
6655
+ }
6656
+ case "bytes":
6657
+ if (!(source.data instanceof Uint8Array) || source.data.byteLength === 0)
6658
+ invalid(`part ${index}.source.data must be a non-empty Uint8Array`);
6659
+ reserveInlineBytes(source.data.byteLength);
6660
+ clone = {
6661
+ type: "bytes",
6662
+ data: source.data.slice(),
6663
+ mediaType: mediaType(source.mediaType, `part ${index}.source.mediaType`)
6664
+ };
6665
+ break;
6666
+ case "base64": {
6667
+ const data = nonEmptyString2(source.data, `part ${index}.source.data`);
6668
+ if (data.length > 4 * Math.ceil(MAX_INLINE_MODEL_INPUT_BYTES / 3)) {
6669
+ invalid(
6670
+ "Inline image data must not exceed 25 MiB total; use a URL or provider-file source where supported"
6671
+ );
6672
+ }
6673
+ if (!validBase64(data)) invalid(`part ${index}.source.data must be valid base64`);
6674
+ reserveInlineBytes(decodedBase64Bytes(data));
6675
+ clone = {
6676
+ type: "base64",
6677
+ data,
6678
+ mediaType: mediaType(source.mediaType, `part ${index}.source.mediaType`)
6679
+ };
6680
+ break;
6681
+ }
6682
+ case "provider-file":
6683
+ clone = {
6684
+ type: "provider-file",
6685
+ provider: nonEmptyString2(source.provider, `part ${index}.source.provider`),
6686
+ reference: nonEmptyString2(source.reference, `part ${index}.source.reference`),
6687
+ ...source.mediaType === void 0 ? {} : { mediaType: mediaType(source.mediaType, `part ${index}.source.mediaType`) }
6688
+ };
6689
+ break;
6690
+ default:
6691
+ invalid(`part ${index}.source.type is unsupported`);
6692
+ }
6693
+ return {
6694
+ type: "image",
6695
+ source: clone,
6696
+ ...part.label === void 0 ? {} : { label: nonEmptyString2(part.label, `part ${index}.label`) }
6697
+ };
6698
+ });
6699
+ }
6700
+ function inputText(input) {
6701
+ return typeof input === "string" ? input : input.filter((part) => part.type === "text").map((part) => part.text).join("\n");
6702
+ }
6703
+ function cloneModelInput(input) {
6704
+ return typeof input === "string" ? input : normalizeModelInput(input);
6705
+ }
6706
+ function describeModelInput(input) {
6707
+ if (typeof input === "string") return void 0;
6708
+ return {
6709
+ parts: input.map((part) => {
6710
+ if (part.type === "text") return { type: "text", characters: part.text.length };
6711
+ const { source } = part;
6712
+ return {
6713
+ type: "image",
6714
+ source: source.type,
6715
+ ...source.mediaType ? { mediaType: source.mediaType } : {},
6716
+ ...source.type === "bytes" ? { bytes: source.data.byteLength } : {},
6717
+ ...source.type === "base64" ? {
6718
+ bytes: decodedBase64Bytes(source.data)
6719
+ } : {},
6720
+ ...(source.type === "url" ? source.url : source.type === "provider-file" ? source.reference : void 0) ? {
6721
+ locator: source.type === "url" ? source.url : source.type === "provider-file" ? source.reference : void 0
6722
+ } : {},
6723
+ ...part.label ? { label: part.label } : {}
6724
+ };
6725
+ })
6726
+ };
6727
+ }
6728
+ function summarizeModelInput(input) {
6729
+ if (typeof input === "string") return input;
6730
+ return input.map(
6731
+ (part) => part.type === "text" ? part.text : `[image ${part.source.mediaType ?? "media"}]`
6732
+ ).join("\n");
6733
+ }
6734
+ function sanitizeModelInputForTrace(input) {
6735
+ return inputText(input);
6736
+ }
6737
+ function normalizePersistedSessionHistory(history) {
6738
+ return history.map((message, index) => {
6739
+ if (typeof message.content === "string") return { ...message };
6740
+ if (message.role !== "user") {
6741
+ throw new InvalidModelInputError(
6742
+ `History message ${index} has non-text content on a non-user role`
6743
+ );
6744
+ }
6745
+ const content = normalizeModelInput(message.content);
6746
+ if (typeof content !== "string" && content.some((part) => part.type === "image" && part.source.type === "bytes")) {
6747
+ throw new InvalidModelInputError(
6748
+ "Uint8Array image input cannot be persisted in session history"
6749
+ );
6750
+ }
6751
+ return { ...message, content };
6752
+ });
6753
+ }
6754
+
4512
6755
  // src/partial-json.ts
4513
6756
  var MAX_DEPTH = 256;
4514
6757
  function parsePartialJson(input) {
@@ -5080,7 +7323,8 @@ var COST_BEARING_LEAF_TYPES = [
5080
7323
  "agent_call_end",
5081
7324
  "tool_call_end",
5082
7325
  "memory_remember",
5083
- "memory_recall"
7326
+ "memory_recall",
7327
+ "transcription_end"
5084
7328
  ];
5085
7329
  var COST_LEAF_SET = new Set(COST_BEARING_LEAF_TYPES);
5086
7330
  function hasPositiveTokens(event) {
@@ -5088,6 +7332,12 @@ function hasPositiveTokens(event) {
5088
7332
  if (!t) return false;
5089
7333
  return (t.input ?? 0) + (t.output ?? 0) + (t.reasoning ?? 0) + (t.cached ?? 0) + (t.cacheWrite ?? 0) > 0;
5090
7334
  }
7335
+ function hasPositiveBillableWork(event) {
7336
+ if (hasPositiveTokens(event)) return true;
7337
+ const seconds = event.data?.usage?.audioSeconds;
7338
+ const totalTokens = event.data?.usage?.totalTokens;
7339
+ return typeof seconds === "number" && Number.isFinite(seconds) && seconds > 0 || typeof totalTokens === "number" && Number.isFinite(totalTokens) && totalTokens > 0;
7340
+ }
5091
7341
  function isUsableCost(cost) {
5092
7342
  return typeof cost === "number" && Number.isFinite(cost) && cost >= 0;
5093
7343
  }
@@ -5100,7 +7350,7 @@ function isCostBearingLeaf(event) {
5100
7350
  return COST_LEAF_SET.has(event.type);
5101
7351
  }
5102
7352
  function isUnpricedLeaf(event) {
5103
- return event.type !== void 0 && COST_LEAF_SET.has(event.type) && !isUsableCost(event.cost) && hasPositiveTokens(event);
7353
+ return event.type !== void 0 && COST_LEAF_SET.has(event.type) && !isUsableCost(event.cost) && hasPositiveBillableWork(event);
5104
7354
  }
5105
7355
  function isRootLevel(event) {
5106
7356
  const d = event.depth;
@@ -5115,6 +7365,9 @@ var AXL_EVENT_TYPES = [
5115
7365
  // Workflow lifecycle
5116
7366
  "workflow_start",
5117
7367
  "workflow_end",
7368
+ // Transcription operation lifecycle (separate from chat/agent turns)
7369
+ "transcription_start",
7370
+ "transcription_end",
5118
7371
  // Ask boundary
5119
7372
  "ask_start",
5120
7373
  "ask_end",
@@ -5205,6 +7458,8 @@ var LIFECYCLE_TYPES = /* @__PURE__ */ new Set([
5205
7458
  "schema_diagnostic",
5206
7459
  "workflow_start",
5207
7460
  "workflow_end",
7461
+ "transcription_start",
7462
+ "transcription_end",
5208
7463
  "checkpoint_save",
5209
7464
  "checkpoint_replay",
5210
7465
  "await_human",
@@ -6020,6 +8275,21 @@ function redactGate(event) {
6020
8275
  }
6021
8276
  };
6022
8277
  }
8278
+ function redactInputDescriptor(input) {
8279
+ return {
8280
+ ...input,
8281
+ parts: input.parts.map((part) => {
8282
+ if (!part || typeof part !== "object" || !("type" in part) || part.type === "text")
8283
+ return part;
8284
+ const value = part;
8285
+ return {
8286
+ ...part,
8287
+ ...value.locator !== void 0 ? { locator: REDACTED } : {},
8288
+ ...value.label !== void 0 ? { label: REDACTED } : {}
8289
+ };
8290
+ })
8291
+ };
8292
+ }
6023
8293
  function redactMemory(event) {
6024
8294
  const d = event.data;
6025
8295
  if (!d || typeof d !== "object") return event;
@@ -6043,7 +8313,11 @@ var LEGACY_REDACTION_RULES = {
6043
8313
  }
6044
8314
  };
6045
8315
  },
6046
- ask_start: (e) => ({ ...e, prompt: REDACTED }),
8316
+ ask_start: (e) => ({
8317
+ ...e,
8318
+ prompt: REDACTED,
8319
+ ...e.input ? { input: redactInputDescriptor(e.input) } : {}
8320
+ }),
6047
8321
  ask_end: (e) => ({
6048
8322
  ...e,
6049
8323
  outcome: e.outcome.ok ? { ok: true, result: REDACTED } : { ok: false, error: REDACTED }
@@ -6051,6 +8325,14 @@ var LEGACY_REDACTION_RULES = {
6051
8325
  agent_call_start: (e) => {
6052
8326
  const d = e.data;
6053
8327
  const out = { ...d, prompt: REDACTED };
8328
+ if (d.input) {
8329
+ out.input = redactInputDescriptor(d.input);
8330
+ }
8331
+ if (d.messageInputs)
8332
+ out.messageInputs = d.messageInputs.map((entry) => ({
8333
+ ...entry,
8334
+ input: redactInputDescriptor(entry.input)
8335
+ }));
6054
8336
  if (d.system !== void 0) out.system = REDACTED;
6055
8337
  if (Array.isArray(d.messages)) {
6056
8338
  out.messages = [{ role: "system", content: `[${d.messages.length} messages redacted]` }];
@@ -6206,6 +8488,18 @@ function reuseLegacyRule(type) {
6206
8488
  var COMMON_V2_REDACTION_RULES = {
6207
8489
  workflow_start: reuseLegacyRule("workflow_start"),
6208
8490
  workflow_end: reuseLegacyRule("workflow_end"),
8491
+ transcription_start: (e) => e,
8492
+ transcription_end: (e) => {
8493
+ const d = e.data;
8494
+ return {
8495
+ ...e,
8496
+ data: {
8497
+ ...d,
8498
+ ...d.text !== void 0 ? { text: REDACTED } : {},
8499
+ ...d.error !== void 0 ? { error: REDACTED } : {}
8500
+ }
8501
+ };
8502
+ },
6209
8503
  ask_start: reuseLegacyRule("ask_start"),
6210
8504
  ask_end: reuseLegacyRule("ask_end"),
6211
8505
  agent_call_start: reuseLegacyRule("agent_call_start"),
@@ -6400,7 +8694,7 @@ function resolveConfig(config) {
6400
8694
 
6401
8695
  // src/tool-model-output.ts
6402
8696
  var import_node_util = require("util");
6403
- function invalid(path, reason) {
8697
+ function invalid2(path, reason) {
6404
8698
  return new Error(`Invalid tool model output at ${path}: ${reason}`);
6405
8699
  }
6406
8700
  function propertyPath(parent, key) {
@@ -6418,14 +8712,14 @@ function propertyPath(parent, key) {
6418
8712
  function normalize(value, path, stack) {
6419
8713
  if (value === null || typeof value === "string" || typeof value === "boolean") return value;
6420
8714
  if (typeof value === "number") {
6421
- if (!Number.isFinite(value)) throw invalid(path, "number must be finite");
8715
+ if (!Number.isFinite(value)) throw invalid2(path, "number must be finite");
6422
8716
  return value;
6423
8717
  }
6424
- if (value === void 0) throw invalid(path, "undefined is only allowed on object properties");
8718
+ if (value === void 0) throw invalid2(path, "undefined is only allowed on object properties");
6425
8719
  if (typeof value === "bigint" || typeof value === "function" || typeof value === "symbol") {
6426
- throw invalid(path, `unsupported ${typeof value}`);
8720
+ throw invalid2(path, `unsupported ${typeof value}`);
6427
8721
  }
6428
- if (typeof value !== "object") throw invalid(path, `unsupported ${typeof value}`);
8722
+ if (typeof value !== "object") throw invalid2(path, `unsupported ${typeof value}`);
6429
8723
  let prototype;
6430
8724
  let descriptors;
6431
8725
  try {
@@ -6433,38 +8727,38 @@ function normalize(value, path, stack) {
6433
8727
  descriptors = Object.getOwnPropertyDescriptors(value);
6434
8728
  Object.setPrototypeOf(descriptors, null);
6435
8729
  } catch (cause) {
6436
- throw invalid(
8730
+ throw invalid2(
6437
8731
  path,
6438
8732
  `object inspection failed (${cause instanceof Error ? cause.name : "error"})`
6439
8733
  );
6440
8734
  }
6441
- if (stack.has(value)) throw invalid(path, "cyclic reference");
8735
+ if (stack.has(value)) throw invalid2(path, "cyclic reference");
6442
8736
  for (const key of Reflect.ownKeys(descriptors)) {
6443
8737
  const descriptor = descriptors[key];
6444
8738
  if (descriptor && !("value" in descriptor)) {
6445
- throw invalid(
8739
+ throw invalid2(
6446
8740
  typeof key === "string" ? propertyPath(path, key) : `${path}[symbol]`,
6447
8741
  "accessor properties are not supported"
6448
8742
  );
6449
8743
  }
6450
8744
  if (typeof key === "symbol") {
6451
- throw invalid(`${path}[symbol]`, "symbol keys are not supported");
8745
+ throw invalid2(`${path}[symbol]`, "symbol keys are not supported");
6452
8746
  }
6453
8747
  }
6454
8748
  if (typeof descriptors.toJSON?.value === "function") {
6455
- throw invalid(propertyPath(path, "toJSON"), "custom toJSON is not supported");
8749
+ throw invalid2(propertyPath(path, "toJSON"), "custom toJSON is not supported");
6456
8750
  }
6457
8751
  const array = Array.isArray(value);
6458
8752
  if (array && prototype !== Array.prototype && prototype !== null) {
6459
- throw invalid(path, "array subclasses are not supported");
8753
+ throw invalid2(path, "array subclasses are not supported");
6460
8754
  }
6461
8755
  const inheritedToJSON = prototype === Array.prototype || prototype === Object.prototype ? Object.getOwnPropertyDescriptor(prototype, "toJSON") : void 0;
6462
8756
  if (typeof inheritedToJSON?.value === "function") {
6463
- throw invalid(propertyPath(path, "toJSON"), "inherited custom toJSON is not supported");
8757
+ throw invalid2(propertyPath(path, "toJSON"), "inherited custom toJSON is not supported");
6464
8758
  }
6465
8759
  const inheritedThen = prototype === Array.prototype || prototype === Object.prototype ? Object.getOwnPropertyDescriptor(prototype, "then") : void 0;
6466
8760
  if (typeof descriptors.then?.value === "function" || inheritedThen !== void 0 && (!("value" in inheritedThen) || typeof inheritedThen.value === "function")) {
6467
- throw invalid(path, "thenable output is not supported");
8761
+ throw invalid2(path, "thenable output is not supported");
6468
8762
  }
6469
8763
  stack.add(value);
6470
8764
  try {
@@ -6472,15 +8766,15 @@ function normalize(value, path, stack) {
6472
8766
  const normalized2 = [];
6473
8767
  const length = descriptors.length?.value;
6474
8768
  if (!Number.isSafeInteger(length) || length < 0) {
6475
- throw invalid(propertyPath(path, "length"), "array length is invalid");
8769
+ throw invalid2(propertyPath(path, "length"), "array length is invalid");
6476
8770
  }
6477
8771
  for (let index = 0; index < length; index++) {
6478
8772
  const descriptor = descriptors[String(index)];
6479
8773
  if (!descriptor || !("value" in descriptor)) {
6480
- throw invalid(`${path}[${index}]`, "arrays must be dense data properties");
8774
+ throw invalid2(`${path}[${index}]`, "arrays must be dense data properties");
6481
8775
  }
6482
8776
  if (descriptor.value === void 0) {
6483
- throw invalid(`${path}[${index}]`, "array values cannot be undefined");
8777
+ throw invalid2(`${path}[${index}]`, "array values cannot be undefined");
6484
8778
  }
6485
8779
  normalized2.push(normalize(descriptor.value, `${path}[${index}]`, stack));
6486
8780
  }
@@ -6489,21 +8783,21 @@ function normalize(value, path, stack) {
6489
8783
  if (!descriptor?.enumerable) continue;
6490
8784
  const index = Number(key);
6491
8785
  if (!Number.isInteger(index) || index < 0 || index >= length || String(index) !== key) {
6492
- throw invalid(propertyPath(path, key), "non-index array properties are not supported");
8786
+ throw invalid2(propertyPath(path, key), "non-index array properties are not supported");
6493
8787
  }
6494
8788
  }
6495
8789
  Object.setPrototypeOf(normalized2, null);
6496
8790
  return normalized2;
6497
8791
  }
6498
8792
  if (prototype !== Object.prototype && prototype !== null) {
6499
- throw invalid(path, "only plain objects and null-prototype records are supported");
8793
+ throw invalid2(path, "only plain objects and null-prototype records are supported");
6500
8794
  }
6501
8795
  const normalized = /* @__PURE__ */ Object.create(null);
6502
8796
  for (const key of Object.keys(descriptors)) {
6503
8797
  const descriptor = descriptors[key];
6504
8798
  if (!descriptor?.enumerable) continue;
6505
8799
  if (!("value" in descriptor)) {
6506
- throw invalid(propertyPath(path, key), "accessor properties are not supported");
8800
+ throw invalid2(propertyPath(path, key), "accessor properties are not supported");
6507
8801
  }
6508
8802
  if (descriptor.value === void 0) continue;
6509
8803
  normalized[key] = normalize(descriptor.value, propertyPath(path, key), stack);
@@ -6539,7 +8833,15 @@ function cloneToolArguments(value) {
6539
8833
  }
6540
8834
  }
6541
8835
  function parseToolInvocation(options) {
6542
- const { toolCall, configuredTool, override, mcpCall, mcpTraceName, availableTools } = options;
8836
+ const {
8837
+ toolCall,
8838
+ configuredTool,
8839
+ override,
8840
+ mcpCall,
8841
+ mcpTraceName,
8842
+ availableTools,
8843
+ providerVisibleSchema
8844
+ } = options;
6543
8845
  const requestedTool = toolCall.function.name;
6544
8846
  if (!override && !configuredTool && !mcpCall) {
6545
8847
  return {
@@ -6599,7 +8901,7 @@ function parseToolInvocation(options) {
6599
8901
  args: parsedArguments,
6600
8902
  issues: toolArgumentIssues(error)
6601
8903
  },
6602
- modelMessage: INVALID_ARGUMENTS_MODEL_MESSAGE
8904
+ modelMessage: toolArgumentModelMessage(error, providerVisibleSchema) ?? INVALID_ARGUMENTS_MODEL_MESSAGE
6603
8905
  };
6604
8906
  }
6605
8907
  }
@@ -7003,44 +9305,44 @@ function resolveBranchDrainTimeoutMs(value) {
7003
9305
  return resolved;
7004
9306
  }
7005
9307
  function parseHumanDecision(value) {
7006
- const invalid2 = () => {
9308
+ const invalid3 = () => {
7007
9309
  throw new AxlError(
7008
9310
  "INVALID_HUMAN_DECISION",
7009
9311
  "Human decision must be a plain object with a boolean approved field and only branch-appropriate string data"
7010
9312
  );
7011
9313
  };
7012
- if (typeof value !== "object" || value === null || Array.isArray(value)) invalid2();
9314
+ if (typeof value !== "object" || value === null || Array.isArray(value)) invalid3();
7013
9315
  let prototype;
7014
9316
  try {
7015
9317
  prototype = Object.getPrototypeOf(value);
7016
9318
  } catch {
7017
- return invalid2();
9319
+ return invalid3();
7018
9320
  }
7019
- if (prototype !== Object.prototype && prototype !== null) invalid2();
9321
+ if (prototype !== Object.prototype && prototype !== null) invalid3();
7020
9322
  let ownKeys;
7021
9323
  try {
7022
9324
  ownKeys = Reflect.ownKeys(value);
7023
9325
  } catch {
7024
- return invalid2();
9326
+ return invalid3();
7025
9327
  }
7026
9328
  if (ownKeys.some(
7027
9329
  (key) => typeof key !== "string" || key !== "approved" && key !== "data" && key !== "reason"
7028
9330
  )) {
7029
- invalid2();
9331
+ invalid3();
7030
9332
  }
7031
9333
  const readDataProperty = (key) => {
7032
9334
  let descriptor;
7033
9335
  try {
7034
9336
  descriptor = Object.getOwnPropertyDescriptor(value, key);
7035
9337
  } catch {
7036
- return invalid2();
9338
+ return invalid3();
7037
9339
  }
7038
9340
  if (!descriptor) {
7039
- if (ownKeys.includes(key)) invalid2();
9341
+ if (ownKeys.includes(key)) invalid3();
7040
9342
  return { present: false, value: void 0 };
7041
9343
  }
7042
- if (!ownKeys.includes(key)) return invalid2();
7043
- if (!("value" in descriptor)) return invalid2();
9344
+ if (!ownKeys.includes(key)) return invalid3();
9345
+ if (!("value" in descriptor)) return invalid3();
7044
9346
  return { present: true, value: descriptor.value };
7045
9347
  };
7046
9348
  const approved = readDataProperty("approved");
@@ -7059,7 +9361,7 @@ function parseHumanDecision(value) {
7059
9361
  };
7060
9362
  }
7061
9363
  }
7062
- return invalid2();
9364
+ return invalid3();
7063
9365
  }
7064
9366
  var askStorage = new import_node_async_hooks.AsyncLocalStorage();
7065
9367
  var inlineSchemaCache = /* @__PURE__ */ new WeakMap();
@@ -7167,8 +9469,12 @@ function appendRetryMessages(messages, content, feedbackMessage, providerMetadat
7167
9469
  }
7168
9470
  function estimateMessagesTokens(messages) {
7169
9471
  let total = 0;
9472
+ let unmeasured = false;
7170
9473
  for (const msg of messages) {
7171
- total += estimateTokens(msg.content);
9474
+ total += estimateTokens(inputText(msg.content));
9475
+ if (typeof msg.content !== "string") {
9476
+ unmeasured ||= msg.content.some((part) => part.type === "image");
9477
+ }
7172
9478
  if (msg.tool_calls) {
7173
9479
  for (const tc of msg.tool_calls) {
7174
9480
  total += estimateTokens(tc.function.name + tc.function.arguments);
@@ -7176,7 +9482,73 @@ function estimateMessagesTokens(messages) {
7176
9482
  }
7177
9483
  total += 4;
7178
9484
  }
7179
- return total;
9485
+ return { tokens: total, unmeasured };
9486
+ }
9487
+ function cloneMessagesForProvider(messages) {
9488
+ return messages.map((message) => ({
9489
+ ...message,
9490
+ content: cloneModelInput(message.content),
9491
+ ...message.tool_calls ? {
9492
+ tool_calls: message.tool_calls.map((call) => ({
9493
+ ...call,
9494
+ function: { ...call.function }
9495
+ }))
9496
+ } : {}
9497
+ }));
9498
+ }
9499
+ function assertRichMessagesAreUserOnly(messages) {
9500
+ for (const message of messages) {
9501
+ if (typeof message.content !== "string" && message.role !== "user") {
9502
+ throw new InvalidModelInputError(
9503
+ "Non-text model input parts are only supported on user messages"
9504
+ );
9505
+ }
9506
+ }
9507
+ }
9508
+ function normalizeSessionHistory(messages) {
9509
+ return messages.map((message) => {
9510
+ if (typeof message.content === "string") return { ...message };
9511
+ if (message.role !== "user") {
9512
+ throw new InvalidModelInputError(
9513
+ "Non-text model input parts are only supported on user messages"
9514
+ );
9515
+ }
9516
+ return { ...message, content: normalizeModelInput(message.content) };
9517
+ });
9518
+ }
9519
+ function hasRichMessage(messages) {
9520
+ return messages.some((message) => typeof message.content !== "string");
9521
+ }
9522
+ function firstImageSource(input, history) {
9523
+ const find = (value) => typeof value === "string" ? void 0 : value.find((part) => part.type === "image")?.source.type;
9524
+ return find(input) ?? history.map((message) => find(message.content)).find(Boolean);
9525
+ }
9526
+ function appendHandoffInstruction(input, originalText, instruction) {
9527
+ if (typeof input === "string" || instruction.length === 0 || instruction === originalText)
9528
+ return input;
9529
+ return [...input, { type: "text", text: instruction }];
9530
+ }
9531
+ var RICH_MODEL_INPUT_ERROR_MESSAGE = "Provider request failed while processing rich model input";
9532
+ var RICH_MODEL_INPUT_ERROR_NAME = "RichModelInputError";
9533
+ var MAX_RICH_FAILURE_ASSOCIATIONS = 32;
9534
+ var MAX_RICH_FAILURE_CAUSE_DEPTH = 4;
9535
+ function legacyErrorMessage(error) {
9536
+ return error instanceof Error ? error.message : String(error);
9537
+ }
9538
+ function providerErrorEventMetadata(error, richRequest) {
9539
+ if (!(error instanceof ProviderError)) {
9540
+ return error instanceof AxlError && richRequest ? { code: error.code } : {};
9541
+ }
9542
+ return {
9543
+ status: error.status,
9544
+ retryable: error.retryable,
9545
+ ...richRequest ? {
9546
+ code: error.code,
9547
+ provider: error.provider,
9548
+ ...error.requestId ? { requestId: error.requestId } : {},
9549
+ ...error.retryAfterMs !== void 0 ? { retryAfterMs: error.retryAfterMs } : {}
9550
+ } : {}
9551
+ };
7180
9552
  }
7181
9553
  var WorkflowContext = class _WorkflowContext {
7182
9554
  input;
@@ -7192,6 +9564,7 @@ var WorkflowContext = class _WorkflowContext {
7192
9564
  }
7193
9565
  config;
7194
9566
  providerRegistry;
9567
+ transcriptionProviderRegistry;
7195
9568
  sessionHistory;
7196
9569
  onTrace;
7197
9570
  pendingDecisions;
@@ -7217,6 +9590,10 @@ var WorkflowContext = class _WorkflowContext {
7217
9590
  */
7218
9591
  autoCheckpointCounters;
7219
9592
  branchDrainState;
9593
+ /** Bounded exact-identity failure association for this execution tree.
9594
+ * Unlike a process-global marker, this cannot affect another context that
9595
+ * happens to reuse the same Error object. */
9596
+ richFailureState;
7220
9597
  /** Root-shared idempotency guards for workflow lifecycle emission.
7221
9598
  * Both `runtime.execute()` and `runtime.stream()` have paths where a
7222
9599
  * post-emit side-effect (checkpoint deletion, state-store persistence)
@@ -7300,7 +9677,8 @@ var WorkflowContext = class _WorkflowContext {
7300
9677
  get events() {
7301
9678
  if (!this._busRef.current) {
7302
9679
  this._busRef.current = new AxlEventBus(this._eventStreamOptions);
7303
- if (this.signal?.aborted) {
9680
+ const signal = this.currentSignal;
9681
+ if (signal?.aborted) {
7304
9682
  this._busRef.current._finish();
7305
9683
  }
7306
9684
  }
@@ -7338,6 +9716,7 @@ var WorkflowContext = class _WorkflowContext {
7338
9716
  this.metadata = init.metadata ?? {};
7339
9717
  this.config = init.config;
7340
9718
  this.providerRegistry = init.providerRegistry;
9719
+ this.transcriptionProviderRegistry = init.transcriptionProviderRegistry;
7341
9720
  this.sessionHistory = init.sessionHistory ?? [];
7342
9721
  this.onTrace = init.onTrace;
7343
9722
  this.pendingDecisions = init.pendingDecisions;
@@ -7371,6 +9750,11 @@ var WorkflowContext = class _WorkflowContext {
7371
9750
  byAgent: /* @__PURE__ */ new Map(),
7372
9751
  root: 0
7373
9752
  };
9753
+ this.richFailureState = init.richFailureState ?? {
9754
+ associations: [],
9755
+ overflowed: false,
9756
+ indeterminate: false
9757
+ };
7374
9758
  const branchDrainTimeoutMs = resolveBranchDrainTimeoutMs(init.branchDrainTimeoutMs);
7375
9759
  this.branchDrainState = init.branchDrainState ?? {
7376
9760
  pending: /* @__PURE__ */ new Set(),
@@ -7403,6 +9787,7 @@ var WorkflowContext = class _WorkflowContext {
7403
9787
  executionId: this.executionId,
7404
9788
  config: this.config,
7405
9789
  providerRegistry: this.providerRegistry,
9790
+ transcriptionProviderRegistry: this.transcriptionProviderRegistry,
7406
9791
  metadata: { ...this.metadata },
7407
9792
  // Shared infrastructure
7408
9793
  budgetContext: this.budgetContext,
@@ -7425,6 +9810,7 @@ var WorkflowContext = class _WorkflowContext {
7425
9810
  autoCheckpointCounters: this.autoCheckpointCounters,
7426
9811
  branchDrainState: this.branchDrainState,
7427
9812
  workflowLifecycleState: this.workflowLifecycleState,
9813
+ richFailureState: this.richFailureState,
7428
9814
  // Share the parent's bus slot (mutable ref). Late allocation in
7429
9815
  // either parent or child propagates because both read the same
7430
9816
  // slot. Nested-ask events (agent-as-tool pattern) bubble up to
@@ -7436,6 +9822,52 @@ var WorkflowContext = class _WorkflowContext {
7436
9822
  // Isolated: sessionHistory (empty)
7437
9823
  });
7438
9824
  }
9825
+ /** Associate an exact thrown value with rich input in this execution only. */
9826
+ recordRichFailure(error) {
9827
+ const state = this.richFailureState;
9828
+ if (state.associations.some((known) => Object.is(known, error))) return;
9829
+ if (state.associations.length >= MAX_RICH_FAILURE_ASSOCIATIONS) {
9830
+ state.associations.shift();
9831
+ state.overflowed = true;
9832
+ state.indeterminate = true;
9833
+ }
9834
+ state.associations.push(error);
9835
+ }
9836
+ /** @internal Safe observer projection for a failure associated with rich input. */
9837
+ _observerErrorProjection(error) {
9838
+ const state = this.richFailureState;
9839
+ const safe = () => ({
9840
+ message: RICH_MODEL_INPUT_ERROR_MESSAGE,
9841
+ name: RICH_MODEL_INPUT_ERROR_NAME
9842
+ });
9843
+ const indeterminate = () => {
9844
+ if (state.associations.length === 0) return void 0;
9845
+ state.indeterminate = true;
9846
+ return safe();
9847
+ };
9848
+ let current = error;
9849
+ const seen = /* @__PURE__ */ new WeakSet();
9850
+ for (let depth = 0; depth < MAX_RICH_FAILURE_CAUSE_DEPTH; depth++) {
9851
+ if (state.associations.some((known) => Object.is(known, current))) {
9852
+ return safe();
9853
+ }
9854
+ if ((typeof current !== "object" || current === null) && typeof current !== "function") {
9855
+ return state.overflowed || state.indeterminate ? safe() : void 0;
9856
+ }
9857
+ if (seen.has(current)) return indeterminate();
9858
+ seen.add(current);
9859
+ let descriptor;
9860
+ try {
9861
+ descriptor = Object.getOwnPropertyDescriptor(current, "cause");
9862
+ } catch {
9863
+ return indeterminate();
9864
+ }
9865
+ if (!descriptor) return state.overflowed || state.indeterminate ? safe() : void 0;
9866
+ if (!("value" in descriptor)) return indeterminate();
9867
+ current = descriptor.value;
9868
+ }
9869
+ return indeterminate();
9870
+ }
7439
9871
  /**
7440
9872
  * Resolve the current abort signal.
7441
9873
  * Branch-scoped signals (from race/spawn/map/budget) in AsyncLocalStorage
@@ -7502,7 +9934,7 @@ var WorkflowContext = class _WorkflowContext {
7502
9934
  /** Resolve, execute, and observe one non-handoff tool request. Rejections
7503
9935
  * never start. Every accepted request emits one terminal event before a
7504
9936
  * continuation message or abort escapes this coordinator. */
7505
- async observeToolInvocation(agent2, toolCall, currentMessages, availableTools) {
9937
+ async observeToolInvocation(agent2, toolCall, currentMessages, toolDefs) {
7506
9938
  const signal = this.currentSignal;
7507
9939
  signal?.throwIfAborted();
7508
9940
  const requestedTool = toolCall.function.name;
@@ -7512,6 +9944,7 @@ var WorkflowContext = class _WorkflowContext {
7512
9944
  const override = this.toolOverrides?.get(requestedTool);
7513
9945
  const mcpManager = this.mcpManager;
7514
9946
  const mcpMatch = !configuredTool ? mcpManager?.getToolsForAgent(agent2._config.mcp, agent2._config.mcpTools).find(({ tool: tool2 }) => tool2.name === requestedTool) : void 0;
9947
+ const providerVisibleSchema = toolDefs.find((tool2) => tool2.function.name === requestedTool)?.function.parameters;
7515
9948
  const parsed = parseToolInvocation({
7516
9949
  toolCall,
7517
9950
  configuredTool,
@@ -7520,7 +9953,8 @@ var WorkflowContext = class _WorkflowContext {
7520
9953
  mcpCall: (args) => mcpManager.callTool(requestedTool, args, mcpMatch.server),
7521
9954
  mcpTraceName: `${mcpMatch.server}:${requestedTool}`
7522
9955
  } : {},
7523
- availableTools
9956
+ availableTools: toolDefs.map((tool2) => tool2.function.name),
9957
+ ...configuredTool ? { providerVisibleSchema } : {}
7524
9958
  });
7525
9959
  signal?.throwIfAborted();
7526
9960
  if ("kind" in parsed) {
@@ -7635,6 +10069,17 @@ var WorkflowContext = class _WorkflowContext {
7635
10069
  }
7636
10070
  // ── ctx.ask() ─────────────────────────────────────────────────────────
7637
10071
  async ask(agent2, prompt, options) {
10072
+ const normalizedInput = normalizeModelInput(prompt);
10073
+ const normalizedHistory = normalizeSessionHistory(this.sessionHistory);
10074
+ const delegateInputHolder = agent2;
10075
+ if (delegateInputHolder._delegateOriginalInput !== void 0) {
10076
+ delegateInputHolder._delegateOriginalInput = normalizeModelInput(
10077
+ delegateInputHolder._delegateOriginalInput
10078
+ );
10079
+ }
10080
+ const richAsk = typeof normalizedInput !== "string" || hasRichMessage(normalizedHistory) || delegateInputHolder._delegateOriginalInput !== void 0 && typeof delegateInputHolder._delegateOriginalInput !== "string";
10081
+ const promptText = inputText(normalizedInput);
10082
+ const inputDescriptor = describeModelInput(normalizedInput);
7638
10083
  const agentName = agent2._name;
7639
10084
  return this._checkpoint(
7640
10085
  this._autoCheckpointName("ask", agentName),
@@ -7654,7 +10099,11 @@ var WorkflowContext = class _WorkflowContext {
7654
10099
  };
7655
10100
  return askStorage.run(frame, async () => {
7656
10101
  const askStart = Date.now();
7657
- this.emitEvent({ type: "ask_start", prompt });
10102
+ this.emitEvent({
10103
+ type: "ask_start",
10104
+ prompt: promptText,
10105
+ ...inputDescriptor ? { input: inputDescriptor } : {}
10106
+ });
7658
10107
  const costBefore = this.budgetContext?.totalCost ?? 0;
7659
10108
  const unpricedCountBefore = this.budgetContext?.unpricedCount ?? 0;
7660
10109
  const resolveCtx = options?.metadata ? { metadata: { ...this.metadata, ...options.metadata } } : { metadata: this.metadata };
@@ -7662,10 +10111,11 @@ var WorkflowContext = class _WorkflowContext {
7662
10111
  const doCall = async () => {
7663
10112
  const result2 = await this.executeAgentCall(
7664
10113
  agent2,
7665
- prompt,
10114
+ normalizedInput,
7666
10115
  options,
7667
10116
  void 0,
7668
- usageCapture
10117
+ usageCapture,
10118
+ normalizedHistory
7669
10119
  );
7670
10120
  return result2;
7671
10121
  };
@@ -7682,6 +10132,9 @@ var WorkflowContext = class _WorkflowContext {
7682
10132
  },
7683
10133
  async (span) => {
7684
10134
  const r = await doCall();
10135
+ if (usageCapture.modelUri) {
10136
+ span.setAttribute("axl.agent.model", usageCapture.modelUri);
10137
+ }
7685
10138
  const costAfter2 = this.budgetContext?.totalCost ?? 0;
7686
10139
  span.setAttribute("axl.agent.cost", costAfter2 - costBefore);
7687
10140
  span.setAttribute("axl.agent.duration", Date.now() - askStart);
@@ -7715,9 +10168,10 @@ var WorkflowContext = class _WorkflowContext {
7715
10168
  try {
7716
10169
  this.onAgentCallComplete({
7717
10170
  agent: agent2._name,
7718
- prompt,
10171
+ prompt: promptText,
10172
+ ...inputDescriptor ? { input: inputDescriptor } : {},
7719
10173
  response: typeof result === "string" ? result : JSON.stringify(result),
7720
- model: agent2.resolveModel(resolveCtx),
10174
+ model: usageCapture.modelUri ?? agent2.resolveModel(resolveCtx),
7721
10175
  cost: costAfter - costBefore,
7722
10176
  unpriced: frame.askUnpriced || unpricedCountAfter > unpricedCountBefore,
7723
10177
  duration: Date.now() - askStart,
@@ -7741,9 +10195,10 @@ var WorkflowContext = class _WorkflowContext {
7741
10195
  } catch (err) {
7742
10196
  askFailed = true;
7743
10197
  askFailure = err;
10198
+ if (richAsk) this.recordRichFailure(err);
7744
10199
  outcome = {
7745
10200
  ok: false,
7746
- error: err instanceof Error ? err.message : String(err)
10201
+ error: this._observerErrorProjection(err)?.message ?? legacyErrorMessage(err)
7747
10202
  };
7748
10203
  }
7749
10204
  let terminalFailure;
@@ -7771,7 +10226,7 @@ var WorkflowContext = class _WorkflowContext {
7771
10226
  { agent: agentName }
7772
10227
  );
7773
10228
  }
7774
- async executeAgentCall(agent2, prompt, options, handoffMessages, usageCapture) {
10229
+ async executeAgentCall(agent2, input, options, handoffMessages, usageCapture, sessionHistory = normalizeSessionHistory(this.sessionHistory)) {
7775
10230
  if (this.budgetContext?.exceeded) {
7776
10231
  const { limit, totalCost: spent, policy } = this.budgetContext;
7777
10232
  if (policy === "warn") {
@@ -7788,7 +10243,82 @@ var WorkflowContext = class _WorkflowContext {
7788
10243
  const resolveCtx = options?.metadata ? { metadata: { ...this.metadata, ...options.metadata } } : { metadata: this.metadata };
7789
10244
  const modelUri = agent2.resolveModel(resolveCtx);
7790
10245
  const systemPrompt = agent2.resolveSystem(resolveCtx);
7791
- const { provider, model } = this.providerRegistry.resolve(modelUri, this.config);
10246
+ const { provider, model: resolvedModel } = this.providerRegistry.resolve(modelUri, this.config);
10247
+ let model = resolvedModel;
10248
+ const prompt = inputText(input);
10249
+ const providerOptions = options?.providerOptions ?? agent2._config.providerOptions;
10250
+ const richRequest = typeof input !== "string" || hasRichMessage(sessionHistory);
10251
+ if (richRequest) {
10252
+ if (providerOptions && ("messages" in providerOptions || "input" in providerOptions)) {
10253
+ throw new UnsupportedModelInputError({
10254
+ provider: provider.name ?? modelUri.split(":", 1)[0],
10255
+ model,
10256
+ modality: "image",
10257
+ feature: "raw input-container providerOptions"
10258
+ });
10259
+ }
10260
+ const source = firstImageSource(input, sessionHistory);
10261
+ if (!provider.validateInput) {
10262
+ throw new UnsupportedModelInputError({
10263
+ provider: provider.name ?? modelUri.split(":", 1)[0],
10264
+ model,
10265
+ modality: "image",
10266
+ ...source ? { source } : {}
10267
+ });
10268
+ }
10269
+ const validation = provider.validateInput({
10270
+ model,
10271
+ input: cloneModelInput(input),
10272
+ history: cloneMessagesForProvider(sessionHistory),
10273
+ stream: this._streamingEnabled,
10274
+ hasTools: Boolean(
10275
+ agent2._config.tools?.length || agent2._config.mcp?.length || agent2._config.mcpTools?.length || agent2._config.handoffs
10276
+ ),
10277
+ responseMode: options?.schema ? "structured" : "text",
10278
+ providerOptions
10279
+ });
10280
+ if (!validation || typeof validation.effectiveModel !== "string" || validation.effectiveModel.length === 0) {
10281
+ throw new UnsupportedModelInputError({
10282
+ provider: provider.name ?? modelUri.split(":", 1)[0],
10283
+ model,
10284
+ modality: "image",
10285
+ ...source ? { source } : {}
10286
+ });
10287
+ }
10288
+ model = validation.effectiveModel;
10289
+ }
10290
+ const effectiveModelUri = !richRequest ? modelUri : `${modelUri.slice(0, modelUri.indexOf(":") + 1)}${model}`;
10291
+ if (usageCapture) usageCapture.modelUri ??= effectiveModelUri;
10292
+ if (richRequest) {
10293
+ const descriptors = [
10294
+ describeModelInput(input),
10295
+ ...sessionHistory.map((message) => describeModelInput(message.content))
10296
+ ].filter(
10297
+ (descriptor) => descriptor !== void 0
10298
+ );
10299
+ const parts = descriptors.flatMap((descriptor) => descriptor.parts);
10300
+ this.spanManager?.addEventToActiveSpan("axl.model_input", {
10301
+ "axl.input.parts": parts.length,
10302
+ "axl.input.images": parts.filter((part) => part.type === "image").length,
10303
+ "axl.input.source.bytes": parts.filter(
10304
+ (part) => part.type === "image" && part.source === "bytes"
10305
+ ).length,
10306
+ "axl.input.source.base64": parts.filter(
10307
+ (part) => part.type === "image" && part.source === "base64"
10308
+ ).length,
10309
+ "axl.input.source.url": parts.filter(
10310
+ (part) => part.type === "image" && part.source === "url"
10311
+ ).length,
10312
+ "axl.input.source.provider_file": parts.filter(
10313
+ (part) => part.type === "image" && part.source === "provider-file"
10314
+ ).length,
10315
+ "axl.input.inline_bytes": parts.reduce(
10316
+ (sum, part) => sum + (part.type === "image" ? part.bytes ?? 0 : 0),
10317
+ 0
10318
+ )
10319
+ });
10320
+ }
10321
+ assertRichMessagesAreUserOnly(sessionHistory);
7792
10322
  let resolvedHandoffs;
7793
10323
  if (typeof agent2._config.handoffs === "function") {
7794
10324
  try {
@@ -7809,50 +10339,60 @@ var WorkflowContext = class _WorkflowContext {
7809
10339
  messages.push({ role: "system", content: systemPrompt });
7810
10340
  }
7811
10341
  const maxContext = agent2._config.maxContext;
7812
- if (maxContext && this.sessionHistory.length > 0) {
10342
+ if (maxContext && sessionHistory.length > 0) {
7813
10343
  const reserveTokens = this.config.contextManagement?.reserveTokens ?? 2e3;
7814
10344
  const systemTokens = systemPrompt ? estimateTokens(systemPrompt) : 0;
7815
10345
  const toolTokens = toolDefs.length > 0 ? estimateTokens(JSON.stringify(toolDefs)) : 0;
7816
10346
  const overhead = systemTokens + toolTokens + reserveTokens;
7817
10347
  const availableForHistory = maxContext - overhead;
7818
- const historyTokens = estimateMessagesTokens(this.sessionHistory);
7819
- if (historyTokens > availableForHistory) {
10348
+ const historyEstimate = estimateMessagesTokens(sessionHistory);
10349
+ if (historyEstimate.unmeasured) {
10350
+ this.emitEvent({
10351
+ type: "log",
10352
+ data: {
10353
+ warning: "Context media contribution is unmeasured; provider will enforce its context limit"
10354
+ }
10355
+ });
10356
+ }
10357
+ if (historyEstimate.tokens > availableForHistory) {
7820
10358
  const summarizedMessages = await this.summarizeHistory(
7821
10359
  provider,
7822
10360
  model,
7823
- this.sessionHistory,
10361
+ sessionHistory,
7824
10362
  availableForHistory
7825
10363
  );
7826
10364
  for (const msg of summarizedMessages) {
7827
10365
  messages.push(msg);
7828
10366
  }
7829
10367
  } else {
7830
- for (const msg of this.sessionHistory) {
10368
+ for (const msg of sessionHistory) {
7831
10369
  messages.push(msg);
7832
10370
  }
7833
10371
  }
7834
10372
  } else {
7835
- for (const msg of this.sessionHistory) {
10373
+ for (const msg of sessionHistory) {
7836
10374
  messages.push(msg);
7837
10375
  }
7838
10376
  }
7839
10377
  const schemaPromptMode = options?.schemaPrompt ?? agent2._config.schemaPrompt ?? "json-schema";
7840
- let userContent = prompt;
10378
+ let userContent = input;
7841
10379
  let appendedSchemaText;
7842
10380
  if (options?.schema) {
7843
10381
  const schema = options.schema;
7844
10382
  if (schemaPromptMode === "none") {
7845
10383
  } else if (schemaPromptMode === "json-schema") {
7846
10384
  appendedSchemaText = renderSchemaForPrompt(schema);
7847
- userContent += `
10385
+ const guidance = `
7848
10386
 
7849
10387
  Respond with valid JSON matching this schema:
7850
10388
  ${appendedSchemaText}`;
10389
+ userContent = typeof input === "string" ? input + guidance : [...input, { type: "text", text: guidance }];
7851
10390
  } else {
7852
10391
  appendedSchemaText = typeof schemaPromptMode.render === "function" ? schemaPromptMode.render(schema) : schemaPromptMode.render;
7853
- userContent += `
10392
+ const guidance = `
7854
10393
 
7855
10394
  ${appendedSchemaText}`;
10395
+ userContent = typeof input === "string" ? input + guidance : [...input, { type: "text", text: guidance }];
7856
10396
  }
7857
10397
  }
7858
10398
  messages.push({ role: "user", content: userContent });
@@ -7880,7 +10420,10 @@ ${appendedSchemaText}`;
7880
10420
  }
7881
10421
  const guardrails = agent2._config.guardrails;
7882
10422
  if (guardrails?.input) {
7883
- const inputResult = await guardrails.input(prompt, { metadata: this.metadata });
10423
+ const inputResult = await guardrails.input(prompt, {
10424
+ metadata: this.metadata,
10425
+ input: cloneModelInput(input)
10426
+ });
7884
10427
  this.emitEvent({
7885
10428
  type: "guardrail",
7886
10429
  agent: agent2._name,
@@ -7971,7 +10514,7 @@ ${appendedSchemaText}`;
7971
10514
  includeThoughts: options?.includeThoughts ?? agent2._config.includeThoughts,
7972
10515
  toolChoice: options?.toolChoice ?? agent2._config.toolChoice,
7973
10516
  stop: options?.stop ?? agent2._config.stop,
7974
- providerOptions: options?.providerOptions ?? agent2._config.providerOptions,
10517
+ providerOptions,
7975
10518
  signal: this.currentSignal
7976
10519
  };
7977
10520
  if (options?.schema && toolDefs.length === 0) {
@@ -7997,15 +10540,26 @@ ${appendedSchemaText}`;
7997
10540
  ...chatOptions.stop !== void 0 ? { stop: chatOptions.stop } : {}
7998
10541
  };
7999
10542
  let messagesSnapshot;
10543
+ let messageInputs;
8000
10544
  if (verboseTrace) {
8001
10545
  try {
8002
- messagesSnapshot = structuredClone(currentMessages);
10546
+ messageInputs = currentMessages.flatMap((message, index) => {
10547
+ const descriptor = describeModelInput(message.content);
10548
+ return descriptor ? [{ index, input: descriptor }] : [];
10549
+ });
10550
+ messagesSnapshot = currentMessages.map((message) => ({
10551
+ ...message,
10552
+ content: sanitizeModelInputForTrace(message.content)
10553
+ }));
8003
10554
  } catch (err) {
8004
10555
  console.warn(
8005
10556
  "[axl] verbose trace messages snapshot failed to clone; emitting shallow copy:",
8006
10557
  err instanceof Error ? err.message : String(err)
8007
10558
  );
8008
- messagesSnapshot = [...currentMessages];
10559
+ messagesSnapshot = currentMessages.map((message) => ({
10560
+ ...message,
10561
+ content: sanitizeModelInputForTrace(message.content)
10562
+ }));
8009
10563
  }
8010
10564
  }
8011
10565
  const retryReason = pendingRetryReason;
@@ -8013,16 +10567,18 @@ ${appendedSchemaText}`;
8013
10567
  this.emitEvent({
8014
10568
  type: "agent_call_start",
8015
10569
  agent: agent2._name,
8016
- model: modelUri,
10570
+ model: effectiveModelUri,
8017
10571
  turn: turns,
8018
10572
  data: {
8019
10573
  prompt,
10574
+ ...typeof input === "string" ? {} : { input: describeModelInput(input) },
8020
10575
  ...systemPrompt ? { system: systemPrompt } : {},
8021
10576
  params: callParams,
8022
10577
  turn: turns,
8023
10578
  ...retryReason ? { retryReason } : {},
8024
10579
  ...toolDefs.length > 0 ? { toolNames: toolDefs.map((t) => t.function.name) } : {},
8025
- ...messagesSnapshot ? { messages: messagesSnapshot } : {}
10580
+ ...messagesSnapshot ? { messages: messagesSnapshot } : {},
10581
+ ...messageInputs && messageInputs.length > 0 ? { messageInputs } : {}
8026
10582
  }
8027
10583
  });
8028
10584
  let response;
@@ -8046,7 +10602,10 @@ ${appendedSchemaText}`;
8046
10602
  });
8047
10603
  }
8048
10604
  }) : void 0;
8049
- for await (const chunk of provider.stream(currentMessages, chatOptions)) {
10605
+ for await (const chunk of provider.stream(
10606
+ cloneMessagesForProvider(currentMessages),
10607
+ chatOptions
10608
+ )) {
8050
10609
  if (chunk.type === "text_delta") {
8051
10610
  content2 += chunk.content;
8052
10611
  this.emitEvent({ type: "token", data: chunk.content });
@@ -8112,24 +10671,25 @@ ${appendedSchemaText}`;
8112
10671
  response.thinking_content = thinkingContent;
8113
10672
  }
8114
10673
  } else {
8115
- response = await provider.chat(currentMessages, chatOptions);
10674
+ response = await provider.chat(cloneMessagesForProvider(currentMessages), chatOptions);
8116
10675
  }
8117
10676
  } catch (err) {
10677
+ if (richRequest) this.recordRichFailure(err);
8118
10678
  this.emitEvent({
8119
10679
  type: "agent_call_end",
8120
10680
  agent: agent2._name,
8121
- model: modelUri,
10681
+ model: effectiveModelUri,
8122
10682
  promptVersion: agent2._config.version,
8123
10683
  duration: Date.now() - turnStart,
8124
10684
  data: {
8125
10685
  response: "",
8126
10686
  turn: turns,
8127
10687
  ...retryReason ? { retryReason } : {},
8128
- error: err instanceof Error ? err.message : String(err),
10688
+ error: this._observerErrorProjection(err)?.message ?? legacyErrorMessage(err),
8129
10689
  // Enrich with typed-error metadata when available. `body` is
8130
10690
  // deliberately NOT emitted — it's redaction-eligible (see
8131
10691
  // docs/security.md). `data.error` already carries the message.
8132
- ...err instanceof ProviderError ? { status: err.status, retryable: err.retryable } : {}
10692
+ ...providerErrorEventMetadata(err, richRequest)
8133
10693
  }
8134
10694
  });
8135
10695
  throw err;
@@ -8143,7 +10703,7 @@ ${appendedSchemaText}`;
8143
10703
  this.emitEvent({
8144
10704
  type: "agent_call_end",
8145
10705
  agent: agent2._name,
8146
- model: modelUri,
10706
+ model: effectiveModelUri,
8147
10707
  promptVersion: agent2._config.version,
8148
10708
  cost: response.cost,
8149
10709
  tokens: response.usage ? {
@@ -8177,12 +10737,12 @@ ${appendedSchemaText}`;
8177
10737
  if (descriptor) {
8178
10738
  const mode = descriptor.mode ?? "oneway";
8179
10739
  let handoffPrompt = prompt;
8180
- if (mode === "roundtrip") {
8181
- try {
8182
- const args = JSON.parse(toolCall.function.arguments);
8183
- if (args.message) handoffPrompt = args.message;
8184
- } catch {
10740
+ try {
10741
+ const args = JSON.parse(toolCall.function.arguments);
10742
+ if (typeof args.message === "string" && args.message.length > 0) {
10743
+ handoffPrompt = args.message;
8185
10744
  }
10745
+ } catch {
8186
10746
  }
8187
10747
  const handoffStart = Date.now();
8188
10748
  const sourceFrame = askStorage.getStore();
@@ -8209,15 +10769,27 @@ ${appendedSchemaText}`;
8209
10769
  validateRetries: options.validateRetries
8210
10770
  } : void 0;
8211
10771
  const handoffFn = () => askStorage.run(targetFrame, async () => {
8212
- this.emitEvent({ type: "ask_start", prompt: handoffPrompt });
10772
+ const delegateOriginal = agent2._delegateOriginalInput;
10773
+ const handoffInput = delegateOriginal ?? (typeof input === "string" ? handoffPrompt : input);
10774
+ const enrichedHandoffInput = appendHandoffInstruction(
10775
+ handoffInput,
10776
+ inputText(input),
10777
+ handoffPrompt
10778
+ );
10779
+ this.emitEvent({
10780
+ type: "ask_start",
10781
+ prompt: inputText(enrichedHandoffInput),
10782
+ ...typeof enrichedHandoffInput === "string" ? {} : { input: describeModelInput(enrichedHandoffInput) }
10783
+ });
8213
10784
  const targetAskStart = Date.now();
8214
10785
  try {
8215
10786
  const result = await this.executeAgentCall(
8216
10787
  descriptor.agent,
8217
- handoffPrompt,
10788
+ enrichedHandoffInput,
8218
10789
  handoffOptions,
8219
10790
  currentMessages,
8220
- usageCapture
10791
+ usageCapture,
10792
+ sessionHistory
8221
10793
  );
8222
10794
  this.emitEvent({
8223
10795
  type: "ask_end",
@@ -8228,11 +10800,13 @@ ${appendedSchemaText}`;
8228
10800
  });
8229
10801
  return result;
8230
10802
  } catch (err) {
10803
+ const richHandoff = typeof enrichedHandoffInput !== "string" || hasRichMessage(sessionHistory);
10804
+ if (richHandoff) this.recordRichFailure(err);
8231
10805
  this.emitEvent({
8232
10806
  type: "ask_end",
8233
10807
  outcome: {
8234
10808
  ok: false,
8235
- error: err instanceof Error ? err.message : String(err)
10809
+ error: this._observerErrorProjection(err)?.message ?? legacyErrorMessage(err)
8236
10810
  },
8237
10811
  cost: targetFrame.askCost.value,
8238
10812
  ...targetFrame.askUnpriced ? { unpriced: true } : {},
@@ -8331,12 +10905,7 @@ ${appendedSchemaText}`;
8331
10905
  return await handoffFn();
8332
10906
  }
8333
10907
  }
8334
- await this.observeToolInvocation(
8335
- agent2,
8336
- toolCall,
8337
- currentMessages,
8338
- toolDefs.map((tool2) => tool2.function.name)
8339
- );
10908
+ await this.observeToolInvocation(agent2, toolCall, currentMessages, toolDefs);
8340
10909
  }
8341
10910
  continue;
8342
10911
  }
@@ -8746,12 +11315,12 @@ ${appendedSchemaText}`;
8746
11315
  content: `Summary of earlier conversation:
8747
11316
  ${this.summaryCache}`
8748
11317
  };
8749
- const summaryTokens = estimateTokens(summaryMsg2.content) + 4;
11318
+ const summaryTokens = estimateTokens(summarizeModelInput(summaryMsg2.content)) + 4;
8750
11319
  const remaining = availableTokens - summaryTokens;
8751
11320
  let recentTokens2 = 0;
8752
11321
  let splitIdx2 = history.length;
8753
11322
  for (let i = history.length - 1; i >= 0; i--) {
8754
- const msgTokens = estimateTokens(history[i].content) + 4;
11323
+ const msgTokens = estimateTokens(summarizeModelInput(history[i].content)) + 4;
8755
11324
  if (recentTokens2 + msgTokens > remaining) break;
8756
11325
  recentTokens2 += msgTokens;
8757
11326
  splitIdx2 = i;
@@ -8764,7 +11333,7 @@ ${this.summaryCache}`
8764
11333
  let splitIdx = history.length;
8765
11334
  const targetRecent = Math.floor(availableTokens * 0.6);
8766
11335
  for (let i = history.length - 1; i >= 0; i--) {
8767
- const msgTokens = estimateTokens(history[i].content) + 4;
11336
+ const msgTokens = estimateTokens(summarizeModelInput(history[i].content)) + 4;
8768
11337
  if (recentTokens + msgTokens > targetRecent) break;
8769
11338
  recentTokens += msgTokens;
8770
11339
  splitIdx = i;
@@ -8782,7 +11351,7 @@ ${this.summaryCache}`
8782
11351
  summaryProvider = provider;
8783
11352
  summaryModel = model;
8784
11353
  }
8785
- const oldContent = oldMessages.map((m) => `${m.role}: ${m.content}`).join("\n");
11354
+ const oldContent = oldMessages.map((m) => `${m.role}: ${summarizeModelInput(m.content)}`).join("\n");
8786
11355
  const summaryResponse = await summaryProvider.chat(
8787
11356
  [
8788
11357
  {
@@ -10016,7 +12585,9 @@ ${summaryResponse.content}`
10016
12585
  reason: "routed"
10017
12586
  }
10018
12587
  });
10019
- return this.ask(routerAgent, prompt, {
12588
+ const routerInput = options?.routerInput === "text" ? inputText(prompt) : prompt;
12589
+ routerAgent._delegateOriginalInput = prompt;
12590
+ return this.ask(routerAgent, routerInput, {
10020
12591
  schema: options?.schema,
10021
12592
  schemaPrompt: options?.schemaPrompt,
10022
12593
  nativeStructuredOutput: options?.nativeStructuredOutput,
@@ -10026,6 +12597,278 @@ ${summaryResponse.content}`
10026
12597
  validateRetries: options?.validateRetries
10027
12598
  });
10028
12599
  }
12600
+ /** Transcribe recorded audio through the dedicated transcription registry.
12601
+ * This operation never enters the chat history or chat-provider path. */
12602
+ async transcribe(request) {
12603
+ const startedAt = Date.now();
12604
+ const transcriptionId = (0, import_node_crypto.randomUUID)();
12605
+ const signal = this.currentSignal;
12606
+ const rawAudio = request && typeof request === "object" && "audio" in request ? request.audio : void 0;
12607
+ const source = rawAudio?.type;
12608
+ const requestedUri = typeof request?.model === "string" ? request.model : void 0;
12609
+ const colon = requestedUri?.indexOf(":") ?? -1;
12610
+ const requestedProvider = colon > 0 ? requestedUri.slice(0, colon) : void 0;
12611
+ const requestedModel = colon > 0 && colon < requestedUri.length - 1 ? requestedUri.slice(colon + 1) : void 0;
12612
+ const startAudio = {
12613
+ source: typeof source === "string" ? source : "unknown",
12614
+ ...rawAudio?.data instanceof Uint8Array ? { bytes: rawAudio.data.byteLength } : {},
12615
+ ...typeof rawAudio?.mediaType === "string" ? { mediaType: rawAudio.mediaType } : {}
12616
+ };
12617
+ this.emitEvent({
12618
+ type: "transcription_start",
12619
+ transcriptionId,
12620
+ model: requestedModel,
12621
+ data: {
12622
+ ...requestedProvider ? { provider: requestedProvider } : {},
12623
+ ...requestedModel ? { model: requestedModel } : {},
12624
+ audio: startAudio
12625
+ }
12626
+ });
12627
+ let terminalEmitted = false;
12628
+ const emitTerminal = (partial) => {
12629
+ if (terminalEmitted) return;
12630
+ terminalEmitted = true;
12631
+ this.emitEvent({
12632
+ type: "transcription_end",
12633
+ transcriptionId,
12634
+ model: partial.model,
12635
+ duration: Date.now() - startedAt,
12636
+ ...partial.cost !== void 0 ? { cost: partial.cost } : {},
12637
+ ...partial.tokens ? { tokens: partial.tokens } : {},
12638
+ data: partial.data
12639
+ });
12640
+ };
12641
+ const abortError3 = () => new DOMException("The transcription operation was aborted", "AbortError");
12642
+ const cleanupStatus = (value) => value === "not_required" || value === "deleted" || value === "failed" || value === "timed_out" ? value : void 0;
12643
+ const safeProviderFailure = (error, provider, model) => {
12644
+ const details = error && typeof error === "object" ? error : void 0;
12645
+ const accounting = normalizeTranscriptionAccounting(details?.usage, details?.pricingStatus);
12646
+ const cleanup = cleanupStatus(details?.cleanupStatus);
12647
+ const providerError = error instanceof ProviderError ? error : void 0;
12648
+ const diagnostics = providerError ? {
12649
+ status: providerError.status,
12650
+ retryable: providerError.retryable,
12651
+ ...providerError.retryAfterMs !== void 0 ? { retryAfterMs: providerError.retryAfterMs } : {},
12652
+ ...providerError.requestId ? { requestId: providerError.requestId } : {}
12653
+ } : void 0;
12654
+ return {
12655
+ accounting,
12656
+ cleanup,
12657
+ diagnostics,
12658
+ error: new TranscriptionOperationError({
12659
+ provider,
12660
+ model,
12661
+ ...accounting.usage ? { usage: accounting.usage } : {},
12662
+ ...accounting.pricingStatus ? { pricingStatus: accounting.pricingStatus } : {},
12663
+ ...cleanup ? { cleanupStatus: cleanup } : {},
12664
+ ...diagnostics ?? {},
12665
+ cause: error
12666
+ })
12667
+ };
12668
+ };
12669
+ const terminalData = (status, provider, model, audio, accounting, extras = {}) => ({
12670
+ status,
12671
+ ...provider ? { provider } : {},
12672
+ ...model ? { model } : {},
12673
+ ...audio ? { audio } : {},
12674
+ ...accounting.usage ? { usage: accounting.usage } : {},
12675
+ ...accounting.pricingStatus ? { pricingStatus: accounting.pricingStatus } : {},
12676
+ ...extras
12677
+ });
12678
+ try {
12679
+ const normalized = normalizeTranscriptionRequest(request);
12680
+ const descriptor = {
12681
+ source: normalized.audio.type,
12682
+ ...normalized.audio.type === "bytes" ? { bytes: normalized.audio.data.byteLength } : {},
12683
+ ...normalized.audio.mediaType !== void 0 ? { mediaType: normalized.audio.mediaType } : {}
12684
+ };
12685
+ if (this.currentSignal?.aborted || signal?.aborted) {
12686
+ emitTerminal({
12687
+ model: requestedModel,
12688
+ data: terminalData("aborted", requestedProvider, requestedModel, descriptor, {
12689
+ hasWork: false
12690
+ })
12691
+ });
12692
+ throw abortError3();
12693
+ }
12694
+ if (this.budgetContext?.exceeded) {
12695
+ const { limit, totalCost: spent, policy } = this.budgetContext;
12696
+ if (policy === "warn") {
12697
+ this.emitEvent({
12698
+ type: "log",
12699
+ data: { warning: "Budget exceeded", limit, spent, policy }
12700
+ });
12701
+ } else {
12702
+ throw new BudgetExceededError(limit, spent, policy);
12703
+ }
12704
+ }
12705
+ if (!this.transcriptionProviderRegistry) {
12706
+ throw new InvalidTranscriptionInputError(
12707
+ "No transcription provider registry is configured"
12708
+ );
12709
+ }
12710
+ const resolved = this.transcriptionProviderRegistry.resolve(normalized.model, this.config);
12711
+ if (normalized.audio.type === "provider-file" && normalized.audio.provider !== resolved.providerName) {
12712
+ throw new InvalidTranscriptionInputError(
12713
+ "Transcription provider-file source does not match the requested provider"
12714
+ );
12715
+ }
12716
+ const capabilities = resolved.provider.capabilities(resolved.model);
12717
+ if (!capabilities || !capabilities.sources.includes(normalized.audio.type)) {
12718
+ throw new InvalidTranscriptionInputError(
12719
+ "The requested transcription model does not support this audio source"
12720
+ );
12721
+ }
12722
+ if (normalized.timestamps && !capabilities.timestamps?.includes(normalized.timestamps)) {
12723
+ throw new InvalidTranscriptionInputError(
12724
+ "The requested transcription model does not support requested timestamps"
12725
+ );
12726
+ }
12727
+ if (normalized.diarization && !capabilities.diarization) {
12728
+ throw new InvalidTranscriptionInputError(
12729
+ "The requested transcription model does not support diarization"
12730
+ );
12731
+ }
12732
+ const providerRequest = {
12733
+ ...normalized,
12734
+ model: resolved.model,
12735
+ ...signal ? { signal } : {}
12736
+ };
12737
+ let result;
12738
+ try {
12739
+ result = await resolved.provider.transcribe(providerRequest);
12740
+ } catch (error) {
12741
+ const failure = safeProviderFailure(error, resolved.providerName, resolved.model);
12742
+ if (this.currentSignal?.aborted || signal?.aborted) {
12743
+ if (failure.accounting.cost !== void 0)
12744
+ this._accumulateBudgetCost(failure.accounting.cost);
12745
+ emitTerminal({
12746
+ model: resolved.model,
12747
+ ...failure.accounting.cost !== void 0 ? { cost: failure.accounting.cost } : {},
12748
+ ...failure.accounting.tokens ? { tokens: failure.accounting.tokens } : {},
12749
+ data: terminalData(
12750
+ "aborted",
12751
+ resolved.providerName,
12752
+ resolved.model,
12753
+ descriptor,
12754
+ failure.accounting,
12755
+ ...failure.cleanup ? [{ cleanupStatus: failure.cleanup }] : []
12756
+ )
12757
+ });
12758
+ throw abortError3();
12759
+ }
12760
+ if (failure.accounting.cost !== void 0)
12761
+ this._accumulateBudgetCost(failure.accounting.cost);
12762
+ emitTerminal({
12763
+ model: resolved.model,
12764
+ ...failure.accounting.cost !== void 0 ? { cost: failure.accounting.cost } : {},
12765
+ ...failure.accounting.tokens ? { tokens: failure.accounting.tokens } : {},
12766
+ data: terminalData(
12767
+ "failed",
12768
+ resolved.providerName,
12769
+ resolved.model,
12770
+ descriptor,
12771
+ failure.accounting,
12772
+ {
12773
+ error: "Transcription operation failed",
12774
+ errorCode: failure.error.code,
12775
+ ...failure.diagnostics ? { providerError: failure.diagnostics } : {},
12776
+ ...failure.cleanup ? { cleanupStatus: failure.cleanup } : {}
12777
+ }
12778
+ )
12779
+ });
12780
+ throw failure.error;
12781
+ }
12782
+ const accounting = normalizeTranscriptionAccounting(
12783
+ result.transcript?.usage,
12784
+ result.transcript?.pricingStatus
12785
+ );
12786
+ if (accounting.cost !== void 0) this._accumulateBudgetCost(accounting.cost);
12787
+ if (this.currentSignal?.aborted || signal?.aborted) {
12788
+ emitTerminal({
12789
+ model: resolved.model,
12790
+ ...accounting.cost !== void 0 ? { cost: accounting.cost } : {},
12791
+ ...accounting.tokens ? { tokens: accounting.tokens } : {},
12792
+ data: terminalData(
12793
+ "aborted",
12794
+ resolved.providerName,
12795
+ resolved.model,
12796
+ descriptor,
12797
+ accounting,
12798
+ ...cleanupStatus(result.cleanupStatus) ? [{ cleanupStatus: cleanupStatus(result.cleanupStatus) }] : []
12799
+ )
12800
+ });
12801
+ throw abortError3();
12802
+ }
12803
+ let transcript;
12804
+ try {
12805
+ transcript = cloneTranscript(result.transcript);
12806
+ } catch (error) {
12807
+ const cleanup = cleanupStatus(result.cleanupStatus);
12808
+ const failure = new TranscriptionOperationError({
12809
+ provider: resolved.providerName,
12810
+ model: resolved.model,
12811
+ ...accounting.usage ? { usage: accounting.usage } : {},
12812
+ ...accounting.pricingStatus ? { pricingStatus: accounting.pricingStatus } : {},
12813
+ ...cleanup ? { cleanupStatus: cleanup } : {},
12814
+ cause: error
12815
+ });
12816
+ emitTerminal({
12817
+ model: resolved.model,
12818
+ ...accounting.cost !== void 0 ? { cost: accounting.cost } : {},
12819
+ ...accounting.tokens ? { tokens: accounting.tokens } : {},
12820
+ data: terminalData(
12821
+ "failed",
12822
+ resolved.providerName,
12823
+ resolved.model,
12824
+ descriptor,
12825
+ accounting,
12826
+ {
12827
+ error: "Transcription operation failed",
12828
+ errorCode: "TRANSCRIPTION_PROVIDER_ERROR",
12829
+ ...cleanup ? { cleanupStatus: cleanup } : {}
12830
+ }
12831
+ )
12832
+ });
12833
+ throw failure;
12834
+ }
12835
+ emitTerminal({
12836
+ model: resolved.model,
12837
+ ...accounting.cost !== void 0 ? { cost: accounting.cost } : {},
12838
+ ...accounting.tokens ? { tokens: accounting.tokens } : {},
12839
+ data: terminalData(
12840
+ "completed",
12841
+ resolved.providerName,
12842
+ resolved.model,
12843
+ descriptor,
12844
+ accounting,
12845
+ {
12846
+ text: transcript.text,
12847
+ ...cleanupStatus(result.cleanupStatus) ? { cleanupStatus: cleanupStatus(result.cleanupStatus) } : {}
12848
+ }
12849
+ )
12850
+ });
12851
+ return transcript;
12852
+ } catch (error) {
12853
+ if (!terminalEmitted) {
12854
+ const aborted = signal?.aborted || error instanceof Error && error.name === "AbortError";
12855
+ emitTerminal({
12856
+ model: requestedModel,
12857
+ data: terminalData(
12858
+ aborted ? "aborted" : "failed",
12859
+ requestedProvider,
12860
+ requestedModel,
12861
+ void 0,
12862
+ { hasWork: false },
12863
+ {
12864
+ error: aborted ? "Transcription operation aborted" : "Transcription operation failed"
12865
+ }
12866
+ )
12867
+ });
12868
+ }
12869
+ throw error;
12870
+ }
12871
+ }
10029
12872
  // ── Private ───────────────────────────────────────────────────────────
10030
12873
  /**
10031
12874
  * Internal emitter input — intentionally loose so call sites don't need to
@@ -10041,7 +12884,7 @@ ${summaryResponse.content}`
10041
12884
  const cost = partial.cost;
10042
12885
  if (COST_BEARING_LEAF_TYPES.includes(partial.type)) {
10043
12886
  const usable = isUsableCost(cost);
10044
- const unpriced = !usable && hasPositiveTokens(
12887
+ const unpriced = !usable && hasPositiveBillableWork(
10045
12888
  partial
10046
12889
  );
10047
12890
  if (frame) {
@@ -10146,11 +12989,13 @@ function agent(config) {
10146
12989
  _name: defaultName,
10147
12990
  async ask(prompt, options) {
10148
12991
  const registry = new ProviderRegistry();
12992
+ const transcriptionRegistry = new TranscriptionProviderRegistry();
10149
12993
  const ctx = new WorkflowContext({
10150
12994
  input: prompt,
10151
12995
  executionId: (0, import_node_crypto2.randomUUID)(),
10152
12996
  config: {},
10153
- providerRegistry: registry
12997
+ providerRegistry: registry,
12998
+ transcriptionProviderRegistry: transcriptionRegistry
10154
12999
  });
10155
13000
  return ctx.ask(agentInstance, prompt, options);
10156
13001
  },
@@ -10210,7 +13055,7 @@ var MemoryStore = class {
10210
13055
  return data !== void 0 ? structuredClone(data) : null;
10211
13056
  }
10212
13057
  async saveSession(sessionId, history) {
10213
- this.sessions.set(sessionId, structuredClone(history));
13058
+ this.sessions.set(sessionId, structuredClone(normalizePersistedSessionHistory(history)));
10214
13059
  }
10215
13060
  async getSession(sessionId) {
10216
13061
  const history = this.sessions.get(sessionId);
@@ -10634,7 +13479,7 @@ var SQLiteStore = class {
10634
13479
  const stmt = this.db.prepare(
10635
13480
  "INSERT OR REPLACE INTO sessions (session_id, history) VALUES (?, ?)"
10636
13481
  );
10637
- stmt.run(sessionId, JSON.stringify(history));
13482
+ stmt.run(sessionId, JSON.stringify(normalizePersistedSessionHistory(history)));
10638
13483
  }
10639
13484
  async getSession(sessionId) {
10640
13485
  const stmt = this.db.prepare("SELECT history FROM sessions WHERE session_id = ?");
@@ -11202,7 +14047,7 @@ var AxlStream = class extends import_node_stream.Readable {
11202
14047
  /** Signal an error. `executionId` is required for the same reason as
11203
14048
  * `_done`: terminal events must carry a real id even when the
11204
14049
  * failure happens before any real trace event fires (review S4). */
11205
- _error(error, executionId) {
14050
+ _error(error, executionId, observerMessage = error.message, observerName = error.name) {
11206
14051
  if (this.events.isFinished) return;
11207
14052
  const errorEvent = {
11208
14053
  schemaVersion: 2,
@@ -11210,7 +14055,7 @@ var AxlStream = class extends import_node_stream.Readable {
11210
14055
  executionId,
11211
14056
  step: Number.MAX_SAFE_INTEGER,
11212
14057
  timestamp: Date.now(),
11213
- data: { message: error.message, name: error.name }
14058
+ data: { message: observerMessage, name: observerName }
11214
14059
  };
11215
14060
  this.events._push(errorEvent);
11216
14061
  this.push(errorEvent);
@@ -11937,6 +14782,10 @@ var StreamingFlusher = class {
11937
14782
  function isAbortError4(err) {
11938
14783
  return typeof err === "object" && err !== null && err.name === "AbortError";
11939
14784
  }
14785
+ function executionErrorProjection(ctx, error) {
14786
+ const safe = ctx?._observerErrorProjection(error);
14787
+ return safe ?? { message: error instanceof Error ? error.message : String(error) };
14788
+ }
11940
14789
  function pushEventBounded(execInfo, event, cap) {
11941
14790
  if (STREAMING_EXCLUDED_TYPES.has(event.type)) return;
11942
14791
  if (cap === Infinity || execInfo.events.length < cap) {
@@ -12031,6 +14880,7 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
12031
14880
  tools = /* @__PURE__ */ new Map();
12032
14881
  agents = /* @__PURE__ */ new Map();
12033
14882
  providerRegistry;
14883
+ transcriptionProviderRegistry;
12034
14884
  stateStore;
12035
14885
  executions = /* @__PURE__ */ new Map();
12036
14886
  pendingDecisionResolvers = /* @__PURE__ */ new Map();
@@ -12086,6 +14936,7 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
12086
14936
  super();
12087
14937
  this.config = resolveConfig(config ?? {});
12088
14938
  this.providerRegistry = new ProviderRegistry();
14939
+ this.transcriptionProviderRegistry = new TranscriptionProviderRegistry();
12089
14940
  this.stateStore = this.createStateStore();
12090
14941
  const requested = this.config.state?.maxEventsPerExecution;
12091
14942
  if (requested === void 0) {
@@ -12234,7 +15085,7 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
12234
15085
  execInfo.status = "failed";
12235
15086
  execInfo.completedAt = Date.now();
12236
15087
  execInfo.duration = execInfo.completedAt - execInfo.startedAt;
12237
- execInfo.error = terminalError instanceof Error ? terminalError.message : String(terminalError);
15088
+ execInfo.error = executionErrorProjection(ctx, terminalError).message;
12238
15089
  execInfo.observation = observation;
12239
15090
  ctx._emitWorkflowEnd({
12240
15091
  status: "failed",
@@ -12489,6 +15340,7 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
12489
15340
  metadata: options?.metadata,
12490
15341
  config: this.config,
12491
15342
  providerRegistry: this.providerRegistry,
15343
+ transcriptionProviderRegistry: this.transcriptionProviderRegistry,
12492
15344
  stateStore: this.stateStore,
12493
15345
  mcpManager: this.mcpManager,
12494
15346
  spanManager: this.spanManager,
@@ -12517,6 +15369,10 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
12517
15369
  registerProvider(name, provider) {
12518
15370
  this.providerRegistry.registerInstance(name, provider);
12519
15371
  }
15372
+ /** Register a dedicated transcription adapter. It is never used for chat. */
15373
+ registerTranscriptionProvider(name, provider) {
15374
+ this.transcriptionProviderRegistry.registerInstance(name, provider);
15375
+ }
12520
15376
  /** Resolve a provider:model URI to a Provider instance and model name. */
12521
15377
  resolveProvider(uri) {
12522
15378
  return this.providerRegistry.resolve(uri, this.config);
@@ -12560,6 +15416,7 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
12560
15416
  metadata: options?.metadata,
12561
15417
  config: this.config,
12562
15418
  providerRegistry: this.providerRegistry,
15419
+ transcriptionProviderRegistry: this.transcriptionProviderRegistry,
12563
15420
  sessionHistory,
12564
15421
  signal: controller.signal,
12565
15422
  eventStreamOptions: options?.events,
@@ -12647,6 +15504,7 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
12647
15504
  const executionId = (0, import_node_crypto3.randomUUID)();
12648
15505
  this.abortControllers.set(executionId, controller);
12649
15506
  let execInfo;
15507
+ let wfCtx;
12650
15508
  const run = async () => {
12651
15509
  const workflow2 = this.workflows.get(name);
12652
15510
  if (!workflow2) throw new Error(`Workflow "${name}" not registered`);
@@ -12670,12 +15528,13 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
12670
15528
  metadata: liftPersistedMetadata(options?.metadata)
12671
15529
  };
12672
15530
  this.executions.set(executionId, execInfo);
12673
- const wfCtx = new WorkflowContext({
15531
+ const context = new WorkflowContext({
12674
15532
  input: validated,
12675
15533
  executionId,
12676
15534
  metadata: options?.metadata,
12677
15535
  config: this.config,
12678
15536
  providerRegistry: this.providerRegistry,
15537
+ transcriptionProviderRegistry: this.transcriptionProviderRegistry,
12679
15538
  sessionHistory,
12680
15539
  signal: controller.signal,
12681
15540
  eventStreamOptions: options?.events,
@@ -12740,6 +15599,7 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
12740
15599
  unpricedWarned: false
12741
15600
  }
12742
15601
  });
15602
+ wfCtx = context;
12743
15603
  return this.spanManager.withSpanAsync(
12744
15604
  "axl.workflow.execute",
12745
15605
  {
@@ -12749,7 +15609,7 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
12749
15609
  },
12750
15610
  (span) => this.runWorkflowBody({
12751
15611
  workflow: workflow2,
12752
- ctx: wfCtx,
15612
+ ctx: context,
12753
15613
  execInfo,
12754
15614
  validated,
12755
15615
  span,
@@ -12762,12 +15622,14 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
12762
15622
  execInfo.status = "failed";
12763
15623
  execInfo.completedAt = Date.now();
12764
15624
  execInfo.duration = execInfo.completedAt - execInfo.startedAt;
12765
- execInfo.error = err instanceof Error ? err.message : String(err);
15625
+ execInfo.error = executionErrorProjection(wfCtx, err).message;
12766
15626
  this.persistExecution(execInfo);
12767
15627
  }
12768
15628
  this.abortControllers.delete(executionId);
12769
15629
  cleanupAbortForwarder();
12770
- axlStream._error(err instanceof Error ? err : new Error(String(err)), executionId);
15630
+ const originalError = err instanceof Error ? err : new Error(String(err));
15631
+ const projection = executionErrorProjection(wfCtx, err);
15632
+ axlStream._error(originalError, executionId, projection.message, projection.name);
12771
15633
  });
12772
15634
  return axlStream;
12773
15635
  }
@@ -13178,7 +16040,7 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
13178
16040
  },
13179
16041
  {
13180
16042
  role: "user",
13181
- content: messages.map((m) => `${m.role}: ${m.content}`).join("\n")
16043
+ content: messages.map((m) => `${m.role}: ${summarizeModelInput(m.content)}`).join("\n")
13182
16044
  }
13183
16045
  ],
13184
16046
  { model, maxTokens: 1024 }
@@ -13625,7 +16487,7 @@ async function registerConditions(conditions) {
13625
16487
  ].join("\n");
13626
16488
  nodeModule.register(`data:text/javascript,${encodeURIComponent(hookCode)}`);
13627
16489
  } catch {
13628
- console.warn("Warning: --conditions requires Node.js 20.6+");
16490
+ console.warn("Warning: this Node.js runtime does not support --conditions");
13629
16491
  }
13630
16492
  }
13631
16493
 
@@ -13934,7 +16796,11 @@ var RedisStore = class _RedisStore {
13934
16796
  async saveSession(sessionId, history) {
13935
16797
  const ttl = this.ttlFor("session");
13936
16798
  const setOptions = ttl !== void 0 ? { EX: ttl } : void 0;
13937
- await this.client.multi().set(this.sessionKey(sessionId), JSON.stringify(history), setOptions).sAdd(this.sessionIdsKey(), sessionId).exec();
16799
+ await this.client.multi().set(
16800
+ this.sessionKey(sessionId),
16801
+ JSON.stringify(normalizePersistedSessionHistory(history)),
16802
+ setOptions
16803
+ ).sAdd(this.sessionIdsKey(), sessionId).exec();
13938
16804
  }
13939
16805
  async getSession(sessionId) {
13940
16806
  const raw = await this.client.get(this.sessionKey(sessionId));
@@ -14423,10 +17289,15 @@ function cosineSimilarity2(a, b) {
14423
17289
  EventStreamOverflowError,
14424
17290
  GROQ_PROFILE,
14425
17291
  GeminiProvider,
17292
+ GeminiTranscriptionProvider,
14426
17293
  GuardrailError,
14427
17294
  InMemoryVectorStore,
17295
+ InvalidModelInputError,
17296
+ InvalidTranscriptionInputError,
14428
17297
  LLAMACPP_PROFILE,
14429
17298
  LMSTUDIO_PROFILE,
17299
+ MAX_INLINE_MODEL_INPUT_BYTES,
17300
+ MAX_INLINE_TRANSCRIPTION_BYTES,
14430
17301
  MISTRAL_PROFILE,
14431
17302
  MaxTurnsError,
14432
17303
  McpManager,
@@ -14442,6 +17313,8 @@ function cosineSimilarity2(a, b) {
14442
17313
  OpenAIEmbedder,
14443
17314
  OpenAIProvider,
14444
17315
  OpenAIResponsesProvider,
17316
+ OpenAITranscriptionProvider,
17317
+ OpenRouterTranscriptionProvider,
14445
17318
  ProviderError,
14446
17319
  ProviderRegistry,
14447
17320
  QuorumNotMet,
@@ -14457,7 +17330,11 @@ function cosineSimilarity2(a, b) {
14457
17330
  TimeoutError,
14458
17331
  ToolFailure,
14459
17332
  ToolModelOutputError,
17333
+ TranscriptionOperationError,
17334
+ TranscriptionProviderRegistry,
14460
17335
  UnsupportedEventSchemaVersionError,
17336
+ UnsupportedModelInputError,
17337
+ UnsupportedTranscriptionInputError,
14461
17338
  VLLM_PROFILE,
14462
17339
  ValidationError,
14463
17340
  VerifyError,
@@ -14474,8 +17351,10 @@ function cosineSimilarity2(a, b) {
14474
17351
  findConfig,
14475
17352
  getEventSchemaVersion,
14476
17353
  getExecutionEventSchemaVersion,
17354
+ hasPositiveBillableWork,
14477
17355
  hasPositiveTokens,
14478
17356
  importModule,
17357
+ inputText,
14479
17358
  isCostBearingLeaf,
14480
17359
  isRetryableStatus,
14481
17360
  isRootLevel,