@axlsdk/axl 0.21.1 → 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;
@@ -1195,6 +1277,38 @@ function unsafeTransportError(surface, remediation, exposedData) {
1195
1277
  }
1196
1278
 
1197
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
+ }
1198
1312
  function resolvePerModel(value, model, fallback) {
1199
1313
  if (value === void 0) return fallback;
1200
1314
  return typeof value === "function" ? value(model) : value;
@@ -1310,6 +1424,41 @@ function normalizeMessageContent(content) {
1310
1424
  }
1311
1425
  var OpenAICompatibleProvider = class {
1312
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
+ }
1313
1462
  /** `json_schema` is honored natively when the profile's `supportsJsonSchema`
1314
1463
  * capability is true for this model (the default); otherwise the engine
1315
1464
  * downgrades the request to plain `json_object` (see `buildRequestBody`),
@@ -1483,7 +1632,7 @@ var OpenAICompatibleProvider = class {
1483
1632
  formatMessage(msg, model) {
1484
1633
  const out = {
1485
1634
  role: this.profile.roleFor ? this.profile.roleFor(msg.role, model) : msg.role,
1486
- content: msg.content
1635
+ content: Array.isArray(msg.content) && this.name === "openrouter" && model === OPENROUTER_IMAGE_MODEL ? openRouterImageParts(msg.content) : msg.content
1487
1636
  };
1488
1637
  if (msg.name && (this.profile.capabilities?.emitsMessageName ?? true)) out.name = msg.name;
1489
1638
  if (msg.tool_calls) out.tool_calls = msg.tool_calls;
@@ -2230,8 +2379,88 @@ var OpenAIProvider = class extends OpenAICompatibleProvider {
2230
2379
  };
2231
2380
 
2232
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
+ }
2233
2424
  var OpenAIResponsesProvider = class {
2234
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
+ }
2235
2464
  /** The Responses API honors `json_schema` (structured outputs) natively. */
2236
2465
  nativeStructuredOutputSupport() {
2237
2466
  return "schema";
@@ -2396,11 +2625,12 @@ var OpenAIResponsesProvider = class {
2396
2625
  buildInput(messages) {
2397
2626
  const input = [];
2398
2627
  for (const msg of messages) {
2628
+ const text = typeof msg.content === "string" ? msg.content : "";
2399
2629
  if (msg.role === "tool") {
2400
2630
  input.push({
2401
2631
  type: "function_call_output",
2402
2632
  call_id: msg.tool_call_id ?? "",
2403
- output: msg.content
2633
+ output: text
2404
2634
  });
2405
2635
  } else if (msg.role === "assistant" && msg.tool_calls && msg.tool_calls.length > 0) {
2406
2636
  const reasoningItems = msg.providerMetadata?.openaiReasoningItems;
@@ -2409,8 +2639,8 @@ var OpenAIResponsesProvider = class {
2409
2639
  input.push(item);
2410
2640
  }
2411
2641
  }
2412
- if (msg.content) {
2413
- input.push({ type: "message", role: "assistant", content: msg.content });
2642
+ if (text) {
2643
+ input.push({ type: "message", role: "assistant", content: text });
2414
2644
  }
2415
2645
  for (const tc of msg.tool_calls) {
2416
2646
  input.push({
@@ -2430,7 +2660,7 @@ var OpenAIResponsesProvider = class {
2430
2660
  input.push({
2431
2661
  type: "message",
2432
2662
  role: msg.role,
2433
- content: msg.content
2663
+ content: typeof msg.content === "string" ? msg.content : responseImageParts(msg.content)
2434
2664
  });
2435
2665
  }
2436
2666
  }
@@ -2497,7 +2727,7 @@ var OpenAIResponsesProvider = class {
2497
2727
  cached_tokens: json.usage.input_tokens_details?.cached_tokens,
2498
2728
  cache_write_tokens: json.usage.input_tokens_details?.cache_write_tokens
2499
2729
  } : void 0;
2500
- const cost = usage ? estimateDirectOpenAICost(json.model ?? model, usage, {
2730
+ const cost = usage && !this.requestContainsImages(request) ? estimateDirectOpenAICost(json.model ?? model, usage, {
2501
2731
  baseUrl: this.baseUrl,
2502
2732
  request,
2503
2733
  response: json
@@ -2512,6 +2742,16 @@ var OpenAIResponsesProvider = class {
2512
2742
  providerMetadata
2513
2743
  };
2514
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
+ }
2515
2755
  // ---------------------------------------------------------------------------
2516
2756
  // Internal: parse SSE stream
2517
2757
  // ---------------------------------------------------------------------------
@@ -2599,7 +2839,7 @@ var OpenAIResponsesProvider = class {
2599
2839
  return {
2600
2840
  type: "done",
2601
2841
  usage,
2602
- cost: usage ? estimateDirectOpenAICost(response?.model ?? model, usage, {
2842
+ cost: usage && !this.requestContainsImages(request) ? estimateDirectOpenAICost(response?.model ?? model, usage, {
2603
2843
  baseUrl: this.baseUrl,
2604
2844
  request,
2605
2845
  response
@@ -2636,7 +2876,51 @@ var OpenAIResponsesProvider = class {
2636
2876
  };
2637
2877
 
2638
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
+ }
2639
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
+ }
2640
2924
  var CLAUDE_CAPABILITIES = {
2641
2925
  "claude-fable-5": {
2642
2926
  thinking: "adaptive-always-on",
@@ -2897,6 +3181,11 @@ function isEffortAtOrBelow(effort, ceiling) {
2897
3181
  function pricingContextFromBody(body) {
2898
3182
  return {
2899
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
+ ),
2900
3189
  unpricedModifier: isAnthropicModifier("inference_geo", body.inference_geo) || isAnthropicModifier("speed", body.speed) || hasAnthropicServerTool(body.tools) || body.fallbacks !== void 0
2901
3190
  };
2902
3191
  }
@@ -2991,6 +3280,44 @@ function mergeAnthropicUsage(initial, update) {
2991
3280
  }
2992
3281
  var AnthropicProvider = class {
2993
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
+ }
2994
3321
  /** Anthropic ignores native `json_schema` structurally — Axl uses a system
2995
3322
  * prompt JSON instruction + client-side Zod validation (see buildRequest). */
2996
3323
  nativeStructuredOutputSupport() {
@@ -3028,7 +3355,10 @@ var AnthropicProvider = class {
3028
3355
  // chat - non-streaming completion
3029
3356
  // ---------------------------------------------------------------------------
3030
3357
  async chat(messages, options) {
3031
- const headers = this.buildHeaders(await this.resolveKey());
3358
+ const headers = this.buildHeaders(
3359
+ await this.resolveKey(),
3360
+ hasAnthropicProviderFileImage(messages)
3361
+ );
3032
3362
  const body = this.buildRequestBody(messages, options, false);
3033
3363
  const pricingContext = pricingContextFromBody(body);
3034
3364
  const res = await fetchWithRetry(
@@ -3059,7 +3389,10 @@ var AnthropicProvider = class {
3059
3389
  // stream - SSE streaming completion
3060
3390
  // ---------------------------------------------------------------------------
3061
3391
  async *stream(messages, options) {
3062
- const headers = this.buildHeaders(await this.resolveKey());
3392
+ const headers = this.buildHeaders(
3393
+ await this.resolveKey(),
3394
+ hasAnthropicProviderFileImage(messages)
3395
+ );
3063
3396
  const body = this.buildRequestBody(messages, options, true);
3064
3397
  const pricingContext = pricingContextFromBody(body);
3065
3398
  const res = await fetchWithRetry(
@@ -3091,11 +3424,12 @@ var AnthropicProvider = class {
3091
3424
  // ---------------------------------------------------------------------------
3092
3425
  // Internal: request building
3093
3426
  // ---------------------------------------------------------------------------
3094
- buildHeaders(apiKey) {
3427
+ buildHeaders(apiKey, requiresFilesBeta = false) {
3095
3428
  return {
3096
3429
  "Content-Type": "application/json",
3097
3430
  "x-api-key": apiKey,
3098
- "anthropic-version": ANTHROPIC_API_VERSION
3431
+ "anthropic-version": ANTHROPIC_API_VERSION,
3432
+ ...requiresFilesBeta ? { "anthropic-beta": ANTHROPIC_FILES_BETA } : {}
3099
3433
  };
3100
3434
  }
3101
3435
  /** Extract a human-readable message from an API error response body. */
@@ -3165,12 +3499,13 @@ ${jsonInstruction}` : jsonInstruction;
3165
3499
  mapMessages(messages) {
3166
3500
  const result = [];
3167
3501
  for (const msg of messages) {
3502
+ const text = typeof msg.content === "string" ? msg.content : "";
3168
3503
  if (msg.role === "assistant") {
3169
3504
  const replayedThinking = this.getReplayedThinkingBlocks(msg);
3170
3505
  if (msg.tool_calls && msg.tool_calls.length > 0) {
3171
3506
  const content = [...replayedThinking];
3172
- if (msg.content) {
3173
- content.push({ type: "text", text: msg.content });
3507
+ if (text) {
3508
+ content.push({ type: "text", text });
3174
3509
  }
3175
3510
  for (const tc of msg.tool_calls) {
3176
3511
  let parsedArgs;
@@ -3189,10 +3524,10 @@ ${jsonInstruction}` : jsonInstruction;
3189
3524
  result.push({ role: "assistant", content });
3190
3525
  } else if (replayedThinking.length > 0) {
3191
3526
  const content = [...replayedThinking];
3192
- if (msg.content) content.push({ type: "text", text: msg.content });
3527
+ if (text) content.push({ type: "text", text });
3193
3528
  result.push({ role: "assistant", content });
3194
3529
  } else {
3195
- result.push({ role: "assistant", content: msg.content });
3530
+ result.push({ role: "assistant", content: text });
3196
3531
  }
3197
3532
  } else if (msg.role === "tool") {
3198
3533
  result.push({
@@ -3201,12 +3536,15 @@ ${jsonInstruction}` : jsonInstruction;
3201
3536
  {
3202
3537
  type: "tool_result",
3203
3538
  tool_use_id: msg.tool_call_id,
3204
- content: msg.content
3539
+ content: text
3205
3540
  }
3206
3541
  ]
3207
3542
  });
3208
3543
  } else if (msg.role === "user") {
3209
- 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
+ });
3210
3548
  }
3211
3549
  }
3212
3550
  return this.mergeConsecutiveRoles(result);
@@ -3311,7 +3649,7 @@ ${jsonInstruction}` : jsonInstruction;
3311
3649
  "Anthropic fallback responses cannot be continued safely; retry without fallbacks."
3312
3650
  );
3313
3651
  }
3314
- 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;
3315
3653
  return {
3316
3654
  content,
3317
3655
  thinking_content: thinkingContent || void 0,
@@ -3348,7 +3686,7 @@ ${jsonInstruction}` : jsonInstruction;
3348
3686
  const doneChunk = () => ({
3349
3687
  type: "done",
3350
3688
  usage,
3351
- cost: pricingUsage && !unpricedModifier && !hasFallbackBoundary && !refused ? estimateAnthropicCost(effectiveModel, pricingUsage) : void 0,
3689
+ cost: pricingUsage && !pricingContext.hasRichInput && !unpricedModifier && !hasFallbackBoundary && !refused ? estimateAnthropicCost(effectiveModel, pricingUsage) : void 0,
3352
3690
  providerMetadata: thinkingBlocks.length > 0 && !hasFallbackBoundary && !hasFallbackIterationSignal ? { anthropicThinkingBlocks: thinkingBlocks } : void 0
3353
3691
  });
3354
3692
  try {
@@ -3496,6 +3834,60 @@ function lastFallbackIndex(blocks) {
3496
3834
  }
3497
3835
 
3498
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
+ }
3499
3891
  var GEMINI_DISALLOWED_SCHEMA_KEYS = /* @__PURE__ */ new Set([
3500
3892
  "additionalProperties",
3501
3893
  "$schema",
@@ -3567,6 +3959,7 @@ var GEMINI_PRICING = {
3567
3959
  "gemini-3.5-flash-lite": { input: 3e-7, cached: 3e-8, output: 25e-7 },
3568
3960
  "gemini-3.6-flash": { input: 15e-7, cached: 15e-8, output: 75e-7 }
3569
3961
  };
3962
+ var GEMINI_INTERACTIONS_IMAGE_MODELS = /* @__PURE__ */ new Set(["gemini-3.7-flash"]);
3570
3963
  function isValidTokenCount2(value) {
3571
3964
  return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
3572
3965
  }
@@ -3654,7 +4047,10 @@ var GEMINI_3X_MIN_THINKING_LEVEL = /* @__PURE__ */ new Map([
3654
4047
  ["gemini-3.1-flash-lite-preview", "minimal"],
3655
4048
  ["gemini-3.5-flash", "minimal"],
3656
4049
  ["gemini-3.5-flash-lite", "minimal"],
3657
- ["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"]
3658
4054
  ]);
3659
4055
  var GEMINI_MODELS_WITHOUT_PORTABLE_TEMPERATURE = /* @__PURE__ */ new Set([
3660
4056
  "gemini-3.5-flash-lite",
@@ -3681,6 +4077,61 @@ function warnGemini3xEffortNone(model) {
3681
4077
  }
3682
4078
  var GeminiProvider = class {
3683
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
+ }
3684
4135
  /** Gemini accepts a `responseSchema` but `sanitizeSchemaForGemini` strips
3685
4136
  * keywords it doesn't support (`$ref`/`$defs`/`additionalProperties`/…), so a
3686
4137
  * derived schema can lose constraints — lossy, not faithful. */
@@ -3719,6 +4170,7 @@ var GeminiProvider = class {
3719
4170
  // chat - non-streaming completion
3720
4171
  // ---------------------------------------------------------------------------
3721
4172
  async chat(messages, options) {
4173
+ if (hasRichGeminiMessages(messages)) return this.chatInteraction(messages, options);
3722
4174
  const body = this.buildRequestBody(messages, options);
3723
4175
  const pricingContext = this.pricingContext(this.requestModel(options), body);
3724
4176
  this.assertSafeGemini3Continuation(messages, pricingContext.model);
@@ -3751,6 +4203,10 @@ var GeminiProvider = class {
3751
4203
  // stream - SSE streaming completion
3752
4204
  // ---------------------------------------------------------------------------
3753
4205
  async *stream(messages, options) {
4206
+ if (hasRichGeminiMessages(messages)) {
4207
+ yield* this.streamInteraction(messages, options);
4208
+ return;
4209
+ }
3754
4210
  const body = this.buildRequestBody(messages, options);
3755
4211
  const pricingContext = this.pricingContext(this.requestModel(options), body);
3756
4212
  this.assertSafeGemini3Continuation(messages, pricingContext.model);
@@ -3782,6 +4238,339 @@ var GeminiProvider = class {
3782
4238
  yield* this.parseSSEStream(res.body, pricingContext);
3783
4239
  }
3784
4240
  // ---------------------------------------------------------------------------
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.
4244
+ // ---------------------------------------------------------------------------
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
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;
4354
+ }
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
+ });
4410
+ }
4411
+ }
4412
+ return steps;
4413
+ }
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
+ // ---------------------------------------------------------------------------
3785
4574
  // Internal: request building
3786
4575
  // ---------------------------------------------------------------------------
3787
4576
  buildHeaders(apiKey) {
@@ -4036,14 +4825,15 @@ var GeminiProvider = class {
4036
4825
  }
4037
4826
  const result = [];
4038
4827
  for (const msg of messages) {
4828
+ const text = typeof msg.content === "string" ? msg.content : "";
4039
4829
  if (msg.role === "assistant") {
4040
4830
  const rawParts = msg.providerMetadata?.geminiParts;
4041
4831
  if (rawParts && rawParts.length > 0) {
4042
4832
  result.push({ role: "model", parts: rawParts });
4043
4833
  } else {
4044
4834
  const parts = [];
4045
- if (msg.content) {
4046
- parts.push({ text: msg.content });
4835
+ if (text) {
4836
+ parts.push({ text });
4047
4837
  }
4048
4838
  if (msg.tool_calls && msg.tool_calls.length > 0) {
4049
4839
  for (const tc of msg.tool_calls) {
@@ -4069,7 +4859,7 @@ var GeminiProvider = class {
4069
4859
  const functionName = (msg.tool_call_id ? geminiNativeNames.get(msg.tool_call_id) : void 0) ?? toolCallIdToName.get(msg.tool_call_id) ?? "unknown";
4070
4860
  const functionResponse = {
4071
4861
  name: functionName,
4072
- response: parseGeminiFunctionResponse(msg.content)
4862
+ response: parseGeminiFunctionResponse(text)
4073
4863
  };
4074
4864
  if (msg.tool_call_id && geminiNativeIds.has(msg.tool_call_id)) {
4075
4865
  functionResponse.id = msg.tool_call_id;
@@ -4079,7 +4869,7 @@ var GeminiProvider = class {
4079
4869
  parts: [{ functionResponse }]
4080
4870
  });
4081
4871
  } else if (msg.role === "user") {
4082
- result.push({ role: "user", parts: [{ text: msg.content }] });
4872
+ result.push({ role: "user", parts: [{ text }] });
4083
4873
  }
4084
4874
  }
4085
4875
  return this.mergeConsecutiveRoles(result);
@@ -4267,6 +5057,56 @@ var GeminiProvider = class {
4267
5057
  }
4268
5058
  }
4269
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
+ }
4270
5110
 
4271
5111
  // src/providers/profiles/openrouter.ts
4272
5112
  var OPENROUTER_PROFILE = {
@@ -4698,68 +5538,1220 @@ var ProviderRegistry = class {
4698
5538
  if (cached) return cached;
4699
5539
  const factory = this.factories.get(name);
4700
5540
  if (factory) {
4701
- const instance = factory(config);
4702
- this.instances.set(name, instance);
4703
- return instance;
4704
- }
4705
- if (this.fallbackInstance) {
4706
- return this.fallbackInstance;
4707
- }
4708
- throw new Error(`Unknown provider "${name}". Registered providers: ${this.list().join(", ")}`);
5541
+ const instance = factory(config);
5542
+ this.instances.set(name, instance);
5543
+ return instance;
5544
+ }
5545
+ if (this.fallbackInstance) {
5546
+ return this.fallbackInstance;
5547
+ }
5548
+ throw new Error(`Unknown provider "${name}". Registered providers: ${this.list().join(", ")}`);
5549
+ }
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);
6562
+ }
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
+ });
4709
6577
  }
4710
- /**
4711
- * Resolve a "provider:model" URI string into a Provider instance and model name.
4712
- *
4713
- * Supported formats:
4714
- * - "openai:gpt-4o" -> provider=openai, model=gpt-4o
4715
- * - "anthropic:claude-3" -> provider=anthropic, model=claude-3
4716
- * - "gpt-4o" -> uses defaultProvider from config, model=gpt-4o
4717
- * - undefined / empty -> uses defaultProvider and defaultModel from config
4718
- *
4719
- * @param uri Provider:model string, or just a model name
4720
- * @param config Axl configuration for provider options and defaults
4721
- */
4722
6578
  resolve(uri, config = {}) {
4723
- if (!uri) {
4724
- const providerName2 = config.defaultProvider ?? "openai";
4725
- const model2 = config.defaultModel ?? "gpt-4o";
4726
- return { provider: this.get(providerName2, config), model: model2 };
4727
- }
4728
- const colonIndex = uri.indexOf(":");
4729
- if (colonIndex === -1) {
4730
- const providerName2 = config.defaultProvider ?? "openai";
4731
- return { provider: this.get(providerName2, config), model: uri };
4732
- }
4733
- const providerName = uri.slice(0, colonIndex);
4734
- const model = uri.slice(colonIndex + 1);
4735
- if (!providerName || !model) {
4736
- throw new Error(
4737
- `Invalid provider URI "${uri}". Expected format: "provider:model" (e.g. "openai:gpt-4o")`
4738
- );
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
+ });
4739
6586
  }
4740
- return { provider: this.get(providerName, config), model };
4741
- }
4742
- /**
4743
- * Clear all cached provider instances. Useful for testing or reconfiguration.
4744
- */
4745
- clearCache() {
4746
- this.instances.clear();
4747
- }
4748
- /**
4749
- * Clear all registered factories (including built-ins).
4750
- * Useful for test runtimes where only explicitly registered instances should be used.
4751
- */
4752
- clearFactories() {
4753
- this.factories.clear();
6587
+ const providerName = uri.slice(0, colon);
6588
+ const model = uri.slice(colon + 1);
6589
+ return { provider: this.get(providerName, config), model, providerName };
4754
6590
  }
4755
6591
  };
4756
- var defaultRegistry = new ProviderRegistry();
4757
6592
 
4758
6593
  // src/context.ts
4759
6594
  var import_node_async_hooks = require("async_hooks");
4760
6595
  var import_node_crypto = require("crypto");
4761
6596
  var import_zod2 = require("zod");
4762
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
+
4763
6755
  // src/partial-json.ts
4764
6756
  var MAX_DEPTH = 256;
4765
6757
  function parsePartialJson(input) {
@@ -5331,7 +7323,8 @@ var COST_BEARING_LEAF_TYPES = [
5331
7323
  "agent_call_end",
5332
7324
  "tool_call_end",
5333
7325
  "memory_remember",
5334
- "memory_recall"
7326
+ "memory_recall",
7327
+ "transcription_end"
5335
7328
  ];
5336
7329
  var COST_LEAF_SET = new Set(COST_BEARING_LEAF_TYPES);
5337
7330
  function hasPositiveTokens(event) {
@@ -5339,6 +7332,12 @@ function hasPositiveTokens(event) {
5339
7332
  if (!t) return false;
5340
7333
  return (t.input ?? 0) + (t.output ?? 0) + (t.reasoning ?? 0) + (t.cached ?? 0) + (t.cacheWrite ?? 0) > 0;
5341
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
+ }
5342
7341
  function isUsableCost(cost) {
5343
7342
  return typeof cost === "number" && Number.isFinite(cost) && cost >= 0;
5344
7343
  }
@@ -5351,7 +7350,7 @@ function isCostBearingLeaf(event) {
5351
7350
  return COST_LEAF_SET.has(event.type);
5352
7351
  }
5353
7352
  function isUnpricedLeaf(event) {
5354
- 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);
5355
7354
  }
5356
7355
  function isRootLevel(event) {
5357
7356
  const d = event.depth;
@@ -5366,6 +7365,9 @@ var AXL_EVENT_TYPES = [
5366
7365
  // Workflow lifecycle
5367
7366
  "workflow_start",
5368
7367
  "workflow_end",
7368
+ // Transcription operation lifecycle (separate from chat/agent turns)
7369
+ "transcription_start",
7370
+ "transcription_end",
5369
7371
  // Ask boundary
5370
7372
  "ask_start",
5371
7373
  "ask_end",
@@ -5456,6 +7458,8 @@ var LIFECYCLE_TYPES = /* @__PURE__ */ new Set([
5456
7458
  "schema_diagnostic",
5457
7459
  "workflow_start",
5458
7460
  "workflow_end",
7461
+ "transcription_start",
7462
+ "transcription_end",
5459
7463
  "checkpoint_save",
5460
7464
  "checkpoint_replay",
5461
7465
  "await_human",
@@ -6271,6 +8275,21 @@ function redactGate(event) {
6271
8275
  }
6272
8276
  };
6273
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
+ }
6274
8293
  function redactMemory(event) {
6275
8294
  const d = event.data;
6276
8295
  if (!d || typeof d !== "object") return event;
@@ -6294,7 +8313,11 @@ var LEGACY_REDACTION_RULES = {
6294
8313
  }
6295
8314
  };
6296
8315
  },
6297
- ask_start: (e) => ({ ...e, prompt: REDACTED }),
8316
+ ask_start: (e) => ({
8317
+ ...e,
8318
+ prompt: REDACTED,
8319
+ ...e.input ? { input: redactInputDescriptor(e.input) } : {}
8320
+ }),
6298
8321
  ask_end: (e) => ({
6299
8322
  ...e,
6300
8323
  outcome: e.outcome.ok ? { ok: true, result: REDACTED } : { ok: false, error: REDACTED }
@@ -6302,6 +8325,14 @@ var LEGACY_REDACTION_RULES = {
6302
8325
  agent_call_start: (e) => {
6303
8326
  const d = e.data;
6304
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
+ }));
6305
8336
  if (d.system !== void 0) out.system = REDACTED;
6306
8337
  if (Array.isArray(d.messages)) {
6307
8338
  out.messages = [{ role: "system", content: `[${d.messages.length} messages redacted]` }];
@@ -6457,6 +8488,18 @@ function reuseLegacyRule(type) {
6457
8488
  var COMMON_V2_REDACTION_RULES = {
6458
8489
  workflow_start: reuseLegacyRule("workflow_start"),
6459
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
+ },
6460
8503
  ask_start: reuseLegacyRule("ask_start"),
6461
8504
  ask_end: reuseLegacyRule("ask_end"),
6462
8505
  agent_call_start: reuseLegacyRule("agent_call_start"),
@@ -6651,7 +8694,7 @@ function resolveConfig(config) {
6651
8694
 
6652
8695
  // src/tool-model-output.ts
6653
8696
  var import_node_util = require("util");
6654
- function invalid(path, reason) {
8697
+ function invalid2(path, reason) {
6655
8698
  return new Error(`Invalid tool model output at ${path}: ${reason}`);
6656
8699
  }
6657
8700
  function propertyPath(parent, key) {
@@ -6669,14 +8712,14 @@ function propertyPath(parent, key) {
6669
8712
  function normalize(value, path, stack) {
6670
8713
  if (value === null || typeof value === "string" || typeof value === "boolean") return value;
6671
8714
  if (typeof value === "number") {
6672
- if (!Number.isFinite(value)) throw invalid(path, "number must be finite");
8715
+ if (!Number.isFinite(value)) throw invalid2(path, "number must be finite");
6673
8716
  return value;
6674
8717
  }
6675
- 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");
6676
8719
  if (typeof value === "bigint" || typeof value === "function" || typeof value === "symbol") {
6677
- throw invalid(path, `unsupported ${typeof value}`);
8720
+ throw invalid2(path, `unsupported ${typeof value}`);
6678
8721
  }
6679
- if (typeof value !== "object") throw invalid(path, `unsupported ${typeof value}`);
8722
+ if (typeof value !== "object") throw invalid2(path, `unsupported ${typeof value}`);
6680
8723
  let prototype;
6681
8724
  let descriptors;
6682
8725
  try {
@@ -6684,38 +8727,38 @@ function normalize(value, path, stack) {
6684
8727
  descriptors = Object.getOwnPropertyDescriptors(value);
6685
8728
  Object.setPrototypeOf(descriptors, null);
6686
8729
  } catch (cause) {
6687
- throw invalid(
8730
+ throw invalid2(
6688
8731
  path,
6689
8732
  `object inspection failed (${cause instanceof Error ? cause.name : "error"})`
6690
8733
  );
6691
8734
  }
6692
- if (stack.has(value)) throw invalid(path, "cyclic reference");
8735
+ if (stack.has(value)) throw invalid2(path, "cyclic reference");
6693
8736
  for (const key of Reflect.ownKeys(descriptors)) {
6694
8737
  const descriptor = descriptors[key];
6695
8738
  if (descriptor && !("value" in descriptor)) {
6696
- throw invalid(
8739
+ throw invalid2(
6697
8740
  typeof key === "string" ? propertyPath(path, key) : `${path}[symbol]`,
6698
8741
  "accessor properties are not supported"
6699
8742
  );
6700
8743
  }
6701
8744
  if (typeof key === "symbol") {
6702
- throw invalid(`${path}[symbol]`, "symbol keys are not supported");
8745
+ throw invalid2(`${path}[symbol]`, "symbol keys are not supported");
6703
8746
  }
6704
8747
  }
6705
8748
  if (typeof descriptors.toJSON?.value === "function") {
6706
- throw invalid(propertyPath(path, "toJSON"), "custom toJSON is not supported");
8749
+ throw invalid2(propertyPath(path, "toJSON"), "custom toJSON is not supported");
6707
8750
  }
6708
8751
  const array = Array.isArray(value);
6709
8752
  if (array && prototype !== Array.prototype && prototype !== null) {
6710
- throw invalid(path, "array subclasses are not supported");
8753
+ throw invalid2(path, "array subclasses are not supported");
6711
8754
  }
6712
8755
  const inheritedToJSON = prototype === Array.prototype || prototype === Object.prototype ? Object.getOwnPropertyDescriptor(prototype, "toJSON") : void 0;
6713
8756
  if (typeof inheritedToJSON?.value === "function") {
6714
- throw invalid(propertyPath(path, "toJSON"), "inherited custom toJSON is not supported");
8757
+ throw invalid2(propertyPath(path, "toJSON"), "inherited custom toJSON is not supported");
6715
8758
  }
6716
8759
  const inheritedThen = prototype === Array.prototype || prototype === Object.prototype ? Object.getOwnPropertyDescriptor(prototype, "then") : void 0;
6717
8760
  if (typeof descriptors.then?.value === "function" || inheritedThen !== void 0 && (!("value" in inheritedThen) || typeof inheritedThen.value === "function")) {
6718
- throw invalid(path, "thenable output is not supported");
8761
+ throw invalid2(path, "thenable output is not supported");
6719
8762
  }
6720
8763
  stack.add(value);
6721
8764
  try {
@@ -6723,15 +8766,15 @@ function normalize(value, path, stack) {
6723
8766
  const normalized2 = [];
6724
8767
  const length = descriptors.length?.value;
6725
8768
  if (!Number.isSafeInteger(length) || length < 0) {
6726
- throw invalid(propertyPath(path, "length"), "array length is invalid");
8769
+ throw invalid2(propertyPath(path, "length"), "array length is invalid");
6727
8770
  }
6728
8771
  for (let index = 0; index < length; index++) {
6729
8772
  const descriptor = descriptors[String(index)];
6730
8773
  if (!descriptor || !("value" in descriptor)) {
6731
- throw invalid(`${path}[${index}]`, "arrays must be dense data properties");
8774
+ throw invalid2(`${path}[${index}]`, "arrays must be dense data properties");
6732
8775
  }
6733
8776
  if (descriptor.value === void 0) {
6734
- throw invalid(`${path}[${index}]`, "array values cannot be undefined");
8777
+ throw invalid2(`${path}[${index}]`, "array values cannot be undefined");
6735
8778
  }
6736
8779
  normalized2.push(normalize(descriptor.value, `${path}[${index}]`, stack));
6737
8780
  }
@@ -6740,21 +8783,21 @@ function normalize(value, path, stack) {
6740
8783
  if (!descriptor?.enumerable) continue;
6741
8784
  const index = Number(key);
6742
8785
  if (!Number.isInteger(index) || index < 0 || index >= length || String(index) !== key) {
6743
- 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");
6744
8787
  }
6745
8788
  }
6746
8789
  Object.setPrototypeOf(normalized2, null);
6747
8790
  return normalized2;
6748
8791
  }
6749
8792
  if (prototype !== Object.prototype && prototype !== null) {
6750
- 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");
6751
8794
  }
6752
8795
  const normalized = /* @__PURE__ */ Object.create(null);
6753
8796
  for (const key of Object.keys(descriptors)) {
6754
8797
  const descriptor = descriptors[key];
6755
8798
  if (!descriptor?.enumerable) continue;
6756
8799
  if (!("value" in descriptor)) {
6757
- throw invalid(propertyPath(path, key), "accessor properties are not supported");
8800
+ throw invalid2(propertyPath(path, key), "accessor properties are not supported");
6758
8801
  }
6759
8802
  if (descriptor.value === void 0) continue;
6760
8803
  normalized[key] = normalize(descriptor.value, propertyPath(path, key), stack);
@@ -7262,44 +9305,44 @@ function resolveBranchDrainTimeoutMs(value) {
7262
9305
  return resolved;
7263
9306
  }
7264
9307
  function parseHumanDecision(value) {
7265
- const invalid2 = () => {
9308
+ const invalid3 = () => {
7266
9309
  throw new AxlError(
7267
9310
  "INVALID_HUMAN_DECISION",
7268
9311
  "Human decision must be a plain object with a boolean approved field and only branch-appropriate string data"
7269
9312
  );
7270
9313
  };
7271
- if (typeof value !== "object" || value === null || Array.isArray(value)) invalid2();
9314
+ if (typeof value !== "object" || value === null || Array.isArray(value)) invalid3();
7272
9315
  let prototype;
7273
9316
  try {
7274
9317
  prototype = Object.getPrototypeOf(value);
7275
9318
  } catch {
7276
- return invalid2();
9319
+ return invalid3();
7277
9320
  }
7278
- if (prototype !== Object.prototype && prototype !== null) invalid2();
9321
+ if (prototype !== Object.prototype && prototype !== null) invalid3();
7279
9322
  let ownKeys;
7280
9323
  try {
7281
9324
  ownKeys = Reflect.ownKeys(value);
7282
9325
  } catch {
7283
- return invalid2();
9326
+ return invalid3();
7284
9327
  }
7285
9328
  if (ownKeys.some(
7286
9329
  (key) => typeof key !== "string" || key !== "approved" && key !== "data" && key !== "reason"
7287
9330
  )) {
7288
- invalid2();
9331
+ invalid3();
7289
9332
  }
7290
9333
  const readDataProperty = (key) => {
7291
9334
  let descriptor;
7292
9335
  try {
7293
9336
  descriptor = Object.getOwnPropertyDescriptor(value, key);
7294
9337
  } catch {
7295
- return invalid2();
9338
+ return invalid3();
7296
9339
  }
7297
9340
  if (!descriptor) {
7298
- if (ownKeys.includes(key)) invalid2();
9341
+ if (ownKeys.includes(key)) invalid3();
7299
9342
  return { present: false, value: void 0 };
7300
9343
  }
7301
- if (!ownKeys.includes(key)) return invalid2();
7302
- if (!("value" in descriptor)) return invalid2();
9344
+ if (!ownKeys.includes(key)) return invalid3();
9345
+ if (!("value" in descriptor)) return invalid3();
7303
9346
  return { present: true, value: descriptor.value };
7304
9347
  };
7305
9348
  const approved = readDataProperty("approved");
@@ -7318,7 +9361,7 @@ function parseHumanDecision(value) {
7318
9361
  };
7319
9362
  }
7320
9363
  }
7321
- return invalid2();
9364
+ return invalid3();
7322
9365
  }
7323
9366
  var askStorage = new import_node_async_hooks.AsyncLocalStorage();
7324
9367
  var inlineSchemaCache = /* @__PURE__ */ new WeakMap();
@@ -7426,8 +9469,12 @@ function appendRetryMessages(messages, content, feedbackMessage, providerMetadat
7426
9469
  }
7427
9470
  function estimateMessagesTokens(messages) {
7428
9471
  let total = 0;
9472
+ let unmeasured = false;
7429
9473
  for (const msg of messages) {
7430
- 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
+ }
7431
9478
  if (msg.tool_calls) {
7432
9479
  for (const tc of msg.tool_calls) {
7433
9480
  total += estimateTokens(tc.function.name + tc.function.arguments);
@@ -7435,7 +9482,73 @@ function estimateMessagesTokens(messages) {
7435
9482
  }
7436
9483
  total += 4;
7437
9484
  }
7438
- 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
+ };
7439
9552
  }
7440
9553
  var WorkflowContext = class _WorkflowContext {
7441
9554
  input;
@@ -7451,6 +9564,7 @@ var WorkflowContext = class _WorkflowContext {
7451
9564
  }
7452
9565
  config;
7453
9566
  providerRegistry;
9567
+ transcriptionProviderRegistry;
7454
9568
  sessionHistory;
7455
9569
  onTrace;
7456
9570
  pendingDecisions;
@@ -7476,6 +9590,10 @@ var WorkflowContext = class _WorkflowContext {
7476
9590
  */
7477
9591
  autoCheckpointCounters;
7478
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;
7479
9597
  /** Root-shared idempotency guards for workflow lifecycle emission.
7480
9598
  * Both `runtime.execute()` and `runtime.stream()` have paths where a
7481
9599
  * post-emit side-effect (checkpoint deletion, state-store persistence)
@@ -7559,7 +9677,8 @@ var WorkflowContext = class _WorkflowContext {
7559
9677
  get events() {
7560
9678
  if (!this._busRef.current) {
7561
9679
  this._busRef.current = new AxlEventBus(this._eventStreamOptions);
7562
- if (this.signal?.aborted) {
9680
+ const signal = this.currentSignal;
9681
+ if (signal?.aborted) {
7563
9682
  this._busRef.current._finish();
7564
9683
  }
7565
9684
  }
@@ -7597,6 +9716,7 @@ var WorkflowContext = class _WorkflowContext {
7597
9716
  this.metadata = init.metadata ?? {};
7598
9717
  this.config = init.config;
7599
9718
  this.providerRegistry = init.providerRegistry;
9719
+ this.transcriptionProviderRegistry = init.transcriptionProviderRegistry;
7600
9720
  this.sessionHistory = init.sessionHistory ?? [];
7601
9721
  this.onTrace = init.onTrace;
7602
9722
  this.pendingDecisions = init.pendingDecisions;
@@ -7630,6 +9750,11 @@ var WorkflowContext = class _WorkflowContext {
7630
9750
  byAgent: /* @__PURE__ */ new Map(),
7631
9751
  root: 0
7632
9752
  };
9753
+ this.richFailureState = init.richFailureState ?? {
9754
+ associations: [],
9755
+ overflowed: false,
9756
+ indeterminate: false
9757
+ };
7633
9758
  const branchDrainTimeoutMs = resolveBranchDrainTimeoutMs(init.branchDrainTimeoutMs);
7634
9759
  this.branchDrainState = init.branchDrainState ?? {
7635
9760
  pending: /* @__PURE__ */ new Set(),
@@ -7662,6 +9787,7 @@ var WorkflowContext = class _WorkflowContext {
7662
9787
  executionId: this.executionId,
7663
9788
  config: this.config,
7664
9789
  providerRegistry: this.providerRegistry,
9790
+ transcriptionProviderRegistry: this.transcriptionProviderRegistry,
7665
9791
  metadata: { ...this.metadata },
7666
9792
  // Shared infrastructure
7667
9793
  budgetContext: this.budgetContext,
@@ -7684,6 +9810,7 @@ var WorkflowContext = class _WorkflowContext {
7684
9810
  autoCheckpointCounters: this.autoCheckpointCounters,
7685
9811
  branchDrainState: this.branchDrainState,
7686
9812
  workflowLifecycleState: this.workflowLifecycleState,
9813
+ richFailureState: this.richFailureState,
7687
9814
  // Share the parent's bus slot (mutable ref). Late allocation in
7688
9815
  // either parent or child propagates because both read the same
7689
9816
  // slot. Nested-ask events (agent-as-tool pattern) bubble up to
@@ -7695,6 +9822,52 @@ var WorkflowContext = class _WorkflowContext {
7695
9822
  // Isolated: sessionHistory (empty)
7696
9823
  });
7697
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
+ }
7698
9871
  /**
7699
9872
  * Resolve the current abort signal.
7700
9873
  * Branch-scoped signals (from race/spawn/map/budget) in AsyncLocalStorage
@@ -7896,6 +10069,17 @@ var WorkflowContext = class _WorkflowContext {
7896
10069
  }
7897
10070
  // ── ctx.ask() ─────────────────────────────────────────────────────────
7898
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);
7899
10083
  const agentName = agent2._name;
7900
10084
  return this._checkpoint(
7901
10085
  this._autoCheckpointName("ask", agentName),
@@ -7915,7 +10099,11 @@ var WorkflowContext = class _WorkflowContext {
7915
10099
  };
7916
10100
  return askStorage.run(frame, async () => {
7917
10101
  const askStart = Date.now();
7918
- this.emitEvent({ type: "ask_start", prompt });
10102
+ this.emitEvent({
10103
+ type: "ask_start",
10104
+ prompt: promptText,
10105
+ ...inputDescriptor ? { input: inputDescriptor } : {}
10106
+ });
7919
10107
  const costBefore = this.budgetContext?.totalCost ?? 0;
7920
10108
  const unpricedCountBefore = this.budgetContext?.unpricedCount ?? 0;
7921
10109
  const resolveCtx = options?.metadata ? { metadata: { ...this.metadata, ...options.metadata } } : { metadata: this.metadata };
@@ -7923,10 +10111,11 @@ var WorkflowContext = class _WorkflowContext {
7923
10111
  const doCall = async () => {
7924
10112
  const result2 = await this.executeAgentCall(
7925
10113
  agent2,
7926
- prompt,
10114
+ normalizedInput,
7927
10115
  options,
7928
10116
  void 0,
7929
- usageCapture
10117
+ usageCapture,
10118
+ normalizedHistory
7930
10119
  );
7931
10120
  return result2;
7932
10121
  };
@@ -7943,6 +10132,9 @@ var WorkflowContext = class _WorkflowContext {
7943
10132
  },
7944
10133
  async (span) => {
7945
10134
  const r = await doCall();
10135
+ if (usageCapture.modelUri) {
10136
+ span.setAttribute("axl.agent.model", usageCapture.modelUri);
10137
+ }
7946
10138
  const costAfter2 = this.budgetContext?.totalCost ?? 0;
7947
10139
  span.setAttribute("axl.agent.cost", costAfter2 - costBefore);
7948
10140
  span.setAttribute("axl.agent.duration", Date.now() - askStart);
@@ -7976,9 +10168,10 @@ var WorkflowContext = class _WorkflowContext {
7976
10168
  try {
7977
10169
  this.onAgentCallComplete({
7978
10170
  agent: agent2._name,
7979
- prompt,
10171
+ prompt: promptText,
10172
+ ...inputDescriptor ? { input: inputDescriptor } : {},
7980
10173
  response: typeof result === "string" ? result : JSON.stringify(result),
7981
- model: agent2.resolveModel(resolveCtx),
10174
+ model: usageCapture.modelUri ?? agent2.resolveModel(resolveCtx),
7982
10175
  cost: costAfter - costBefore,
7983
10176
  unpriced: frame.askUnpriced || unpricedCountAfter > unpricedCountBefore,
7984
10177
  duration: Date.now() - askStart,
@@ -8002,9 +10195,10 @@ var WorkflowContext = class _WorkflowContext {
8002
10195
  } catch (err) {
8003
10196
  askFailed = true;
8004
10197
  askFailure = err;
10198
+ if (richAsk) this.recordRichFailure(err);
8005
10199
  outcome = {
8006
10200
  ok: false,
8007
- error: err instanceof Error ? err.message : String(err)
10201
+ error: this._observerErrorProjection(err)?.message ?? legacyErrorMessage(err)
8008
10202
  };
8009
10203
  }
8010
10204
  let terminalFailure;
@@ -8032,7 +10226,7 @@ var WorkflowContext = class _WorkflowContext {
8032
10226
  { agent: agentName }
8033
10227
  );
8034
10228
  }
8035
- async executeAgentCall(agent2, prompt, options, handoffMessages, usageCapture) {
10229
+ async executeAgentCall(agent2, input, options, handoffMessages, usageCapture, sessionHistory = normalizeSessionHistory(this.sessionHistory)) {
8036
10230
  if (this.budgetContext?.exceeded) {
8037
10231
  const { limit, totalCost: spent, policy } = this.budgetContext;
8038
10232
  if (policy === "warn") {
@@ -8049,7 +10243,82 @@ var WorkflowContext = class _WorkflowContext {
8049
10243
  const resolveCtx = options?.metadata ? { metadata: { ...this.metadata, ...options.metadata } } : { metadata: this.metadata };
8050
10244
  const modelUri = agent2.resolveModel(resolveCtx);
8051
10245
  const systemPrompt = agent2.resolveSystem(resolveCtx);
8052
- 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);
8053
10322
  let resolvedHandoffs;
8054
10323
  if (typeof agent2._config.handoffs === "function") {
8055
10324
  try {
@@ -8070,50 +10339,60 @@ var WorkflowContext = class _WorkflowContext {
8070
10339
  messages.push({ role: "system", content: systemPrompt });
8071
10340
  }
8072
10341
  const maxContext = agent2._config.maxContext;
8073
- if (maxContext && this.sessionHistory.length > 0) {
10342
+ if (maxContext && sessionHistory.length > 0) {
8074
10343
  const reserveTokens = this.config.contextManagement?.reserveTokens ?? 2e3;
8075
10344
  const systemTokens = systemPrompt ? estimateTokens(systemPrompt) : 0;
8076
10345
  const toolTokens = toolDefs.length > 0 ? estimateTokens(JSON.stringify(toolDefs)) : 0;
8077
10346
  const overhead = systemTokens + toolTokens + reserveTokens;
8078
10347
  const availableForHistory = maxContext - overhead;
8079
- const historyTokens = estimateMessagesTokens(this.sessionHistory);
8080
- 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) {
8081
10358
  const summarizedMessages = await this.summarizeHistory(
8082
10359
  provider,
8083
10360
  model,
8084
- this.sessionHistory,
10361
+ sessionHistory,
8085
10362
  availableForHistory
8086
10363
  );
8087
10364
  for (const msg of summarizedMessages) {
8088
10365
  messages.push(msg);
8089
10366
  }
8090
10367
  } else {
8091
- for (const msg of this.sessionHistory) {
10368
+ for (const msg of sessionHistory) {
8092
10369
  messages.push(msg);
8093
10370
  }
8094
10371
  }
8095
10372
  } else {
8096
- for (const msg of this.sessionHistory) {
10373
+ for (const msg of sessionHistory) {
8097
10374
  messages.push(msg);
8098
10375
  }
8099
10376
  }
8100
10377
  const schemaPromptMode = options?.schemaPrompt ?? agent2._config.schemaPrompt ?? "json-schema";
8101
- let userContent = prompt;
10378
+ let userContent = input;
8102
10379
  let appendedSchemaText;
8103
10380
  if (options?.schema) {
8104
10381
  const schema = options.schema;
8105
10382
  if (schemaPromptMode === "none") {
8106
10383
  } else if (schemaPromptMode === "json-schema") {
8107
10384
  appendedSchemaText = renderSchemaForPrompt(schema);
8108
- userContent += `
10385
+ const guidance = `
8109
10386
 
8110
10387
  Respond with valid JSON matching this schema:
8111
10388
  ${appendedSchemaText}`;
10389
+ userContent = typeof input === "string" ? input + guidance : [...input, { type: "text", text: guidance }];
8112
10390
  } else {
8113
10391
  appendedSchemaText = typeof schemaPromptMode.render === "function" ? schemaPromptMode.render(schema) : schemaPromptMode.render;
8114
- userContent += `
10392
+ const guidance = `
8115
10393
 
8116
10394
  ${appendedSchemaText}`;
10395
+ userContent = typeof input === "string" ? input + guidance : [...input, { type: "text", text: guidance }];
8117
10396
  }
8118
10397
  }
8119
10398
  messages.push({ role: "user", content: userContent });
@@ -8141,7 +10420,10 @@ ${appendedSchemaText}`;
8141
10420
  }
8142
10421
  const guardrails = agent2._config.guardrails;
8143
10422
  if (guardrails?.input) {
8144
- 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
+ });
8145
10427
  this.emitEvent({
8146
10428
  type: "guardrail",
8147
10429
  agent: agent2._name,
@@ -8232,7 +10514,7 @@ ${appendedSchemaText}`;
8232
10514
  includeThoughts: options?.includeThoughts ?? agent2._config.includeThoughts,
8233
10515
  toolChoice: options?.toolChoice ?? agent2._config.toolChoice,
8234
10516
  stop: options?.stop ?? agent2._config.stop,
8235
- providerOptions: options?.providerOptions ?? agent2._config.providerOptions,
10517
+ providerOptions,
8236
10518
  signal: this.currentSignal
8237
10519
  };
8238
10520
  if (options?.schema && toolDefs.length === 0) {
@@ -8258,15 +10540,26 @@ ${appendedSchemaText}`;
8258
10540
  ...chatOptions.stop !== void 0 ? { stop: chatOptions.stop } : {}
8259
10541
  };
8260
10542
  let messagesSnapshot;
10543
+ let messageInputs;
8261
10544
  if (verboseTrace) {
8262
10545
  try {
8263
- 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
+ }));
8264
10554
  } catch (err) {
8265
10555
  console.warn(
8266
10556
  "[axl] verbose trace messages snapshot failed to clone; emitting shallow copy:",
8267
10557
  err instanceof Error ? err.message : String(err)
8268
10558
  );
8269
- messagesSnapshot = [...currentMessages];
10559
+ messagesSnapshot = currentMessages.map((message) => ({
10560
+ ...message,
10561
+ content: sanitizeModelInputForTrace(message.content)
10562
+ }));
8270
10563
  }
8271
10564
  }
8272
10565
  const retryReason = pendingRetryReason;
@@ -8274,16 +10567,18 @@ ${appendedSchemaText}`;
8274
10567
  this.emitEvent({
8275
10568
  type: "agent_call_start",
8276
10569
  agent: agent2._name,
8277
- model: modelUri,
10570
+ model: effectiveModelUri,
8278
10571
  turn: turns,
8279
10572
  data: {
8280
10573
  prompt,
10574
+ ...typeof input === "string" ? {} : { input: describeModelInput(input) },
8281
10575
  ...systemPrompt ? { system: systemPrompt } : {},
8282
10576
  params: callParams,
8283
10577
  turn: turns,
8284
10578
  ...retryReason ? { retryReason } : {},
8285
10579
  ...toolDefs.length > 0 ? { toolNames: toolDefs.map((t) => t.function.name) } : {},
8286
- ...messagesSnapshot ? { messages: messagesSnapshot } : {}
10580
+ ...messagesSnapshot ? { messages: messagesSnapshot } : {},
10581
+ ...messageInputs && messageInputs.length > 0 ? { messageInputs } : {}
8287
10582
  }
8288
10583
  });
8289
10584
  let response;
@@ -8307,7 +10602,10 @@ ${appendedSchemaText}`;
8307
10602
  });
8308
10603
  }
8309
10604
  }) : void 0;
8310
- for await (const chunk of provider.stream(currentMessages, chatOptions)) {
10605
+ for await (const chunk of provider.stream(
10606
+ cloneMessagesForProvider(currentMessages),
10607
+ chatOptions
10608
+ )) {
8311
10609
  if (chunk.type === "text_delta") {
8312
10610
  content2 += chunk.content;
8313
10611
  this.emitEvent({ type: "token", data: chunk.content });
@@ -8373,24 +10671,25 @@ ${appendedSchemaText}`;
8373
10671
  response.thinking_content = thinkingContent;
8374
10672
  }
8375
10673
  } else {
8376
- response = await provider.chat(currentMessages, chatOptions);
10674
+ response = await provider.chat(cloneMessagesForProvider(currentMessages), chatOptions);
8377
10675
  }
8378
10676
  } catch (err) {
10677
+ if (richRequest) this.recordRichFailure(err);
8379
10678
  this.emitEvent({
8380
10679
  type: "agent_call_end",
8381
10680
  agent: agent2._name,
8382
- model: modelUri,
10681
+ model: effectiveModelUri,
8383
10682
  promptVersion: agent2._config.version,
8384
10683
  duration: Date.now() - turnStart,
8385
10684
  data: {
8386
10685
  response: "",
8387
10686
  turn: turns,
8388
10687
  ...retryReason ? { retryReason } : {},
8389
- error: err instanceof Error ? err.message : String(err),
10688
+ error: this._observerErrorProjection(err)?.message ?? legacyErrorMessage(err),
8390
10689
  // Enrich with typed-error metadata when available. `body` is
8391
10690
  // deliberately NOT emitted — it's redaction-eligible (see
8392
10691
  // docs/security.md). `data.error` already carries the message.
8393
- ...err instanceof ProviderError ? { status: err.status, retryable: err.retryable } : {}
10692
+ ...providerErrorEventMetadata(err, richRequest)
8394
10693
  }
8395
10694
  });
8396
10695
  throw err;
@@ -8404,7 +10703,7 @@ ${appendedSchemaText}`;
8404
10703
  this.emitEvent({
8405
10704
  type: "agent_call_end",
8406
10705
  agent: agent2._name,
8407
- model: modelUri,
10706
+ model: effectiveModelUri,
8408
10707
  promptVersion: agent2._config.version,
8409
10708
  cost: response.cost,
8410
10709
  tokens: response.usage ? {
@@ -8438,12 +10737,12 @@ ${appendedSchemaText}`;
8438
10737
  if (descriptor) {
8439
10738
  const mode = descriptor.mode ?? "oneway";
8440
10739
  let handoffPrompt = prompt;
8441
- if (mode === "roundtrip") {
8442
- try {
8443
- const args = JSON.parse(toolCall.function.arguments);
8444
- if (args.message) handoffPrompt = args.message;
8445
- } 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;
8446
10744
  }
10745
+ } catch {
8447
10746
  }
8448
10747
  const handoffStart = Date.now();
8449
10748
  const sourceFrame = askStorage.getStore();
@@ -8470,15 +10769,27 @@ ${appendedSchemaText}`;
8470
10769
  validateRetries: options.validateRetries
8471
10770
  } : void 0;
8472
10771
  const handoffFn = () => askStorage.run(targetFrame, async () => {
8473
- 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
+ });
8474
10784
  const targetAskStart = Date.now();
8475
10785
  try {
8476
10786
  const result = await this.executeAgentCall(
8477
10787
  descriptor.agent,
8478
- handoffPrompt,
10788
+ enrichedHandoffInput,
8479
10789
  handoffOptions,
8480
10790
  currentMessages,
8481
- usageCapture
10791
+ usageCapture,
10792
+ sessionHistory
8482
10793
  );
8483
10794
  this.emitEvent({
8484
10795
  type: "ask_end",
@@ -8489,11 +10800,13 @@ ${appendedSchemaText}`;
8489
10800
  });
8490
10801
  return result;
8491
10802
  } catch (err) {
10803
+ const richHandoff = typeof enrichedHandoffInput !== "string" || hasRichMessage(sessionHistory);
10804
+ if (richHandoff) this.recordRichFailure(err);
8492
10805
  this.emitEvent({
8493
10806
  type: "ask_end",
8494
10807
  outcome: {
8495
10808
  ok: false,
8496
- error: err instanceof Error ? err.message : String(err)
10809
+ error: this._observerErrorProjection(err)?.message ?? legacyErrorMessage(err)
8497
10810
  },
8498
10811
  cost: targetFrame.askCost.value,
8499
10812
  ...targetFrame.askUnpriced ? { unpriced: true } : {},
@@ -9002,12 +11315,12 @@ ${appendedSchemaText}`;
9002
11315
  content: `Summary of earlier conversation:
9003
11316
  ${this.summaryCache}`
9004
11317
  };
9005
- const summaryTokens = estimateTokens(summaryMsg2.content) + 4;
11318
+ const summaryTokens = estimateTokens(summarizeModelInput(summaryMsg2.content)) + 4;
9006
11319
  const remaining = availableTokens - summaryTokens;
9007
11320
  let recentTokens2 = 0;
9008
11321
  let splitIdx2 = history.length;
9009
11322
  for (let i = history.length - 1; i >= 0; i--) {
9010
- const msgTokens = estimateTokens(history[i].content) + 4;
11323
+ const msgTokens = estimateTokens(summarizeModelInput(history[i].content)) + 4;
9011
11324
  if (recentTokens2 + msgTokens > remaining) break;
9012
11325
  recentTokens2 += msgTokens;
9013
11326
  splitIdx2 = i;
@@ -9020,7 +11333,7 @@ ${this.summaryCache}`
9020
11333
  let splitIdx = history.length;
9021
11334
  const targetRecent = Math.floor(availableTokens * 0.6);
9022
11335
  for (let i = history.length - 1; i >= 0; i--) {
9023
- const msgTokens = estimateTokens(history[i].content) + 4;
11336
+ const msgTokens = estimateTokens(summarizeModelInput(history[i].content)) + 4;
9024
11337
  if (recentTokens + msgTokens > targetRecent) break;
9025
11338
  recentTokens += msgTokens;
9026
11339
  splitIdx = i;
@@ -9038,7 +11351,7 @@ ${this.summaryCache}`
9038
11351
  summaryProvider = provider;
9039
11352
  summaryModel = model;
9040
11353
  }
9041
- const oldContent = oldMessages.map((m) => `${m.role}: ${m.content}`).join("\n");
11354
+ const oldContent = oldMessages.map((m) => `${m.role}: ${summarizeModelInput(m.content)}`).join("\n");
9042
11355
  const summaryResponse = await summaryProvider.chat(
9043
11356
  [
9044
11357
  {
@@ -10272,7 +12585,9 @@ ${summaryResponse.content}`
10272
12585
  reason: "routed"
10273
12586
  }
10274
12587
  });
10275
- return this.ask(routerAgent, prompt, {
12588
+ const routerInput = options?.routerInput === "text" ? inputText(prompt) : prompt;
12589
+ routerAgent._delegateOriginalInput = prompt;
12590
+ return this.ask(routerAgent, routerInput, {
10276
12591
  schema: options?.schema,
10277
12592
  schemaPrompt: options?.schemaPrompt,
10278
12593
  nativeStructuredOutput: options?.nativeStructuredOutput,
@@ -10282,6 +12597,278 @@ ${summaryResponse.content}`
10282
12597
  validateRetries: options?.validateRetries
10283
12598
  });
10284
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
+ }
10285
12872
  // ── Private ───────────────────────────────────────────────────────────
10286
12873
  /**
10287
12874
  * Internal emitter input — intentionally loose so call sites don't need to
@@ -10297,7 +12884,7 @@ ${summaryResponse.content}`
10297
12884
  const cost = partial.cost;
10298
12885
  if (COST_BEARING_LEAF_TYPES.includes(partial.type)) {
10299
12886
  const usable = isUsableCost(cost);
10300
- const unpriced = !usable && hasPositiveTokens(
12887
+ const unpriced = !usable && hasPositiveBillableWork(
10301
12888
  partial
10302
12889
  );
10303
12890
  if (frame) {
@@ -10402,11 +12989,13 @@ function agent(config) {
10402
12989
  _name: defaultName,
10403
12990
  async ask(prompt, options) {
10404
12991
  const registry = new ProviderRegistry();
12992
+ const transcriptionRegistry = new TranscriptionProviderRegistry();
10405
12993
  const ctx = new WorkflowContext({
10406
12994
  input: prompt,
10407
12995
  executionId: (0, import_node_crypto2.randomUUID)(),
10408
12996
  config: {},
10409
- providerRegistry: registry
12997
+ providerRegistry: registry,
12998
+ transcriptionProviderRegistry: transcriptionRegistry
10410
12999
  });
10411
13000
  return ctx.ask(agentInstance, prompt, options);
10412
13001
  },
@@ -10466,7 +13055,7 @@ var MemoryStore = class {
10466
13055
  return data !== void 0 ? structuredClone(data) : null;
10467
13056
  }
10468
13057
  async saveSession(sessionId, history) {
10469
- this.sessions.set(sessionId, structuredClone(history));
13058
+ this.sessions.set(sessionId, structuredClone(normalizePersistedSessionHistory(history)));
10470
13059
  }
10471
13060
  async getSession(sessionId) {
10472
13061
  const history = this.sessions.get(sessionId);
@@ -10890,7 +13479,7 @@ var SQLiteStore = class {
10890
13479
  const stmt = this.db.prepare(
10891
13480
  "INSERT OR REPLACE INTO sessions (session_id, history) VALUES (?, ?)"
10892
13481
  );
10893
- stmt.run(sessionId, JSON.stringify(history));
13482
+ stmt.run(sessionId, JSON.stringify(normalizePersistedSessionHistory(history)));
10894
13483
  }
10895
13484
  async getSession(sessionId) {
10896
13485
  const stmt = this.db.prepare("SELECT history FROM sessions WHERE session_id = ?");
@@ -11458,7 +14047,7 @@ var AxlStream = class extends import_node_stream.Readable {
11458
14047
  /** Signal an error. `executionId` is required for the same reason as
11459
14048
  * `_done`: terminal events must carry a real id even when the
11460
14049
  * failure happens before any real trace event fires (review S4). */
11461
- _error(error, executionId) {
14050
+ _error(error, executionId, observerMessage = error.message, observerName = error.name) {
11462
14051
  if (this.events.isFinished) return;
11463
14052
  const errorEvent = {
11464
14053
  schemaVersion: 2,
@@ -11466,7 +14055,7 @@ var AxlStream = class extends import_node_stream.Readable {
11466
14055
  executionId,
11467
14056
  step: Number.MAX_SAFE_INTEGER,
11468
14057
  timestamp: Date.now(),
11469
- data: { message: error.message, name: error.name }
14058
+ data: { message: observerMessage, name: observerName }
11470
14059
  };
11471
14060
  this.events._push(errorEvent);
11472
14061
  this.push(errorEvent);
@@ -12193,6 +14782,10 @@ var StreamingFlusher = class {
12193
14782
  function isAbortError4(err) {
12194
14783
  return typeof err === "object" && err !== null && err.name === "AbortError";
12195
14784
  }
14785
+ function executionErrorProjection(ctx, error) {
14786
+ const safe = ctx?._observerErrorProjection(error);
14787
+ return safe ?? { message: error instanceof Error ? error.message : String(error) };
14788
+ }
12196
14789
  function pushEventBounded(execInfo, event, cap) {
12197
14790
  if (STREAMING_EXCLUDED_TYPES.has(event.type)) return;
12198
14791
  if (cap === Infinity || execInfo.events.length < cap) {
@@ -12287,6 +14880,7 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
12287
14880
  tools = /* @__PURE__ */ new Map();
12288
14881
  agents = /* @__PURE__ */ new Map();
12289
14882
  providerRegistry;
14883
+ transcriptionProviderRegistry;
12290
14884
  stateStore;
12291
14885
  executions = /* @__PURE__ */ new Map();
12292
14886
  pendingDecisionResolvers = /* @__PURE__ */ new Map();
@@ -12342,6 +14936,7 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
12342
14936
  super();
12343
14937
  this.config = resolveConfig(config ?? {});
12344
14938
  this.providerRegistry = new ProviderRegistry();
14939
+ this.transcriptionProviderRegistry = new TranscriptionProviderRegistry();
12345
14940
  this.stateStore = this.createStateStore();
12346
14941
  const requested = this.config.state?.maxEventsPerExecution;
12347
14942
  if (requested === void 0) {
@@ -12490,7 +15085,7 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
12490
15085
  execInfo.status = "failed";
12491
15086
  execInfo.completedAt = Date.now();
12492
15087
  execInfo.duration = execInfo.completedAt - execInfo.startedAt;
12493
- execInfo.error = terminalError instanceof Error ? terminalError.message : String(terminalError);
15088
+ execInfo.error = executionErrorProjection(ctx, terminalError).message;
12494
15089
  execInfo.observation = observation;
12495
15090
  ctx._emitWorkflowEnd({
12496
15091
  status: "failed",
@@ -12745,6 +15340,7 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
12745
15340
  metadata: options?.metadata,
12746
15341
  config: this.config,
12747
15342
  providerRegistry: this.providerRegistry,
15343
+ transcriptionProviderRegistry: this.transcriptionProviderRegistry,
12748
15344
  stateStore: this.stateStore,
12749
15345
  mcpManager: this.mcpManager,
12750
15346
  spanManager: this.spanManager,
@@ -12773,6 +15369,10 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
12773
15369
  registerProvider(name, provider) {
12774
15370
  this.providerRegistry.registerInstance(name, provider);
12775
15371
  }
15372
+ /** Register a dedicated transcription adapter. It is never used for chat. */
15373
+ registerTranscriptionProvider(name, provider) {
15374
+ this.transcriptionProviderRegistry.registerInstance(name, provider);
15375
+ }
12776
15376
  /** Resolve a provider:model URI to a Provider instance and model name. */
12777
15377
  resolveProvider(uri) {
12778
15378
  return this.providerRegistry.resolve(uri, this.config);
@@ -12816,6 +15416,7 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
12816
15416
  metadata: options?.metadata,
12817
15417
  config: this.config,
12818
15418
  providerRegistry: this.providerRegistry,
15419
+ transcriptionProviderRegistry: this.transcriptionProviderRegistry,
12819
15420
  sessionHistory,
12820
15421
  signal: controller.signal,
12821
15422
  eventStreamOptions: options?.events,
@@ -12903,6 +15504,7 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
12903
15504
  const executionId = (0, import_node_crypto3.randomUUID)();
12904
15505
  this.abortControllers.set(executionId, controller);
12905
15506
  let execInfo;
15507
+ let wfCtx;
12906
15508
  const run = async () => {
12907
15509
  const workflow2 = this.workflows.get(name);
12908
15510
  if (!workflow2) throw new Error(`Workflow "${name}" not registered`);
@@ -12926,12 +15528,13 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
12926
15528
  metadata: liftPersistedMetadata(options?.metadata)
12927
15529
  };
12928
15530
  this.executions.set(executionId, execInfo);
12929
- const wfCtx = new WorkflowContext({
15531
+ const context = new WorkflowContext({
12930
15532
  input: validated,
12931
15533
  executionId,
12932
15534
  metadata: options?.metadata,
12933
15535
  config: this.config,
12934
15536
  providerRegistry: this.providerRegistry,
15537
+ transcriptionProviderRegistry: this.transcriptionProviderRegistry,
12935
15538
  sessionHistory,
12936
15539
  signal: controller.signal,
12937
15540
  eventStreamOptions: options?.events,
@@ -12996,6 +15599,7 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
12996
15599
  unpricedWarned: false
12997
15600
  }
12998
15601
  });
15602
+ wfCtx = context;
12999
15603
  return this.spanManager.withSpanAsync(
13000
15604
  "axl.workflow.execute",
13001
15605
  {
@@ -13005,7 +15609,7 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
13005
15609
  },
13006
15610
  (span) => this.runWorkflowBody({
13007
15611
  workflow: workflow2,
13008
- ctx: wfCtx,
15612
+ ctx: context,
13009
15613
  execInfo,
13010
15614
  validated,
13011
15615
  span,
@@ -13018,12 +15622,14 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
13018
15622
  execInfo.status = "failed";
13019
15623
  execInfo.completedAt = Date.now();
13020
15624
  execInfo.duration = execInfo.completedAt - execInfo.startedAt;
13021
- execInfo.error = err instanceof Error ? err.message : String(err);
15625
+ execInfo.error = executionErrorProjection(wfCtx, err).message;
13022
15626
  this.persistExecution(execInfo);
13023
15627
  }
13024
15628
  this.abortControllers.delete(executionId);
13025
15629
  cleanupAbortForwarder();
13026
- 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);
13027
15633
  });
13028
15634
  return axlStream;
13029
15635
  }
@@ -13434,7 +16040,7 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
13434
16040
  },
13435
16041
  {
13436
16042
  role: "user",
13437
- content: messages.map((m) => `${m.role}: ${m.content}`).join("\n")
16043
+ content: messages.map((m) => `${m.role}: ${summarizeModelInput(m.content)}`).join("\n")
13438
16044
  }
13439
16045
  ],
13440
16046
  { model, maxTokens: 1024 }
@@ -13881,7 +16487,7 @@ async function registerConditions(conditions) {
13881
16487
  ].join("\n");
13882
16488
  nodeModule.register(`data:text/javascript,${encodeURIComponent(hookCode)}`);
13883
16489
  } catch {
13884
- console.warn("Warning: --conditions requires Node.js 20.6+");
16490
+ console.warn("Warning: this Node.js runtime does not support --conditions");
13885
16491
  }
13886
16492
  }
13887
16493
 
@@ -14190,7 +16796,11 @@ var RedisStore = class _RedisStore {
14190
16796
  async saveSession(sessionId, history) {
14191
16797
  const ttl = this.ttlFor("session");
14192
16798
  const setOptions = ttl !== void 0 ? { EX: ttl } : void 0;
14193
- 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();
14194
16804
  }
14195
16805
  async getSession(sessionId) {
14196
16806
  const raw = await this.client.get(this.sessionKey(sessionId));
@@ -14679,10 +17289,15 @@ function cosineSimilarity2(a, b) {
14679
17289
  EventStreamOverflowError,
14680
17290
  GROQ_PROFILE,
14681
17291
  GeminiProvider,
17292
+ GeminiTranscriptionProvider,
14682
17293
  GuardrailError,
14683
17294
  InMemoryVectorStore,
17295
+ InvalidModelInputError,
17296
+ InvalidTranscriptionInputError,
14684
17297
  LLAMACPP_PROFILE,
14685
17298
  LMSTUDIO_PROFILE,
17299
+ MAX_INLINE_MODEL_INPUT_BYTES,
17300
+ MAX_INLINE_TRANSCRIPTION_BYTES,
14686
17301
  MISTRAL_PROFILE,
14687
17302
  MaxTurnsError,
14688
17303
  McpManager,
@@ -14698,6 +17313,8 @@ function cosineSimilarity2(a, b) {
14698
17313
  OpenAIEmbedder,
14699
17314
  OpenAIProvider,
14700
17315
  OpenAIResponsesProvider,
17316
+ OpenAITranscriptionProvider,
17317
+ OpenRouterTranscriptionProvider,
14701
17318
  ProviderError,
14702
17319
  ProviderRegistry,
14703
17320
  QuorumNotMet,
@@ -14713,7 +17330,11 @@ function cosineSimilarity2(a, b) {
14713
17330
  TimeoutError,
14714
17331
  ToolFailure,
14715
17332
  ToolModelOutputError,
17333
+ TranscriptionOperationError,
17334
+ TranscriptionProviderRegistry,
14716
17335
  UnsupportedEventSchemaVersionError,
17336
+ UnsupportedModelInputError,
17337
+ UnsupportedTranscriptionInputError,
14717
17338
  VLLM_PROFILE,
14718
17339
  ValidationError,
14719
17340
  VerifyError,
@@ -14730,8 +17351,10 @@ function cosineSimilarity2(a, b) {
14730
17351
  findConfig,
14731
17352
  getEventSchemaVersion,
14732
17353
  getExecutionEventSchemaVersion,
17354
+ hasPositiveBillableWork,
14733
17355
  hasPositiveTokens,
14734
17356
  importModule,
17357
+ inputText,
14735
17358
  isCostBearingLeaf,
14736
17359
  isRetryableStatus,
14737
17360
  isRootLevel,