@axlsdk/axl 0.21.1 → 0.22.1

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,37 @@ function unsafeTransportError(surface, remediation, exposedData) {
1195
1277
  }
1196
1278
 
1197
1279
  // src/providers/openai-compatible.ts
1280
+ function compatibleImageBase64(source) {
1281
+ return source.type === "base64" ? source.data : Buffer.from(source.data.buffer, source.data.byteOffset, source.data.byteLength).toString(
1282
+ "base64"
1283
+ );
1284
+ }
1285
+ function openRouterImageParts(parts, model) {
1286
+ const content = [];
1287
+ for (const part of parts) {
1288
+ if (part.type === "text") {
1289
+ content.push({ type: "text", text: part.text });
1290
+ continue;
1291
+ }
1292
+ const { source } = part;
1293
+ if (source.type === "provider-file") {
1294
+ throw new UnsupportedModelInputError({
1295
+ provider: "openrouter",
1296
+ model,
1297
+ modality: "image",
1298
+ source: "provider-file"
1299
+ });
1300
+ }
1301
+ content.push({
1302
+ type: "image_url",
1303
+ image_url: {
1304
+ url: source.type === "url" ? source.url : `data:${source.mediaType};base64,${compatibleImageBase64(source)}`
1305
+ }
1306
+ });
1307
+ if (part.label) content.push({ type: "text", text: `[Image: ${part.label}]` });
1308
+ }
1309
+ return content;
1310
+ }
1198
1311
  function resolvePerModel(value, model, fallback) {
1199
1312
  if (value === void 0) return fallback;
1200
1313
  return typeof value === "function" ? value(model) : value;
@@ -1310,6 +1423,40 @@ function normalizeMessageContent(content) {
1310
1423
  }
1311
1424
  var OpenAICompatibleProvider = class {
1312
1425
  name;
1426
+ inputCapabilities(model) {
1427
+ return this.name === "openrouter" && model.trim().length > 0 ? { image: { sources: ["url", "bytes", "base64"] } } : {};
1428
+ }
1429
+ validateInput(request) {
1430
+ const effectiveModel = typeof request.providerOptions?.model === "string" ? request.providerOptions.model : request.model;
1431
+ const fail = (source, feature) => {
1432
+ throw new UnsupportedModelInputError({
1433
+ provider: this.name,
1434
+ model: effectiveModel || request.model,
1435
+ modality: "image",
1436
+ ...source ? { source } : {},
1437
+ ...feature ? { feature } : {}
1438
+ });
1439
+ };
1440
+ if (this.name !== "openrouter" || effectiveModel.trim().length === 0) {
1441
+ fail(void 0, "image input for this model");
1442
+ }
1443
+ if (request.providerOptions && "messages" in request.providerOptions) {
1444
+ fail(void 0, "raw messages providerOptions");
1445
+ }
1446
+ for (const message of request.history) {
1447
+ if (!Array.isArray(message.content)) continue;
1448
+ if (message.role !== "user") fail(void 0, "rich non-user history");
1449
+ if (message.content.some(
1450
+ (part) => part.type === "image" && part.source.type === "provider-file"
1451
+ )) {
1452
+ fail("provider-file");
1453
+ }
1454
+ }
1455
+ if (Array.isArray(request.input) && request.input.some((part) => part.type === "image" && part.source.type === "provider-file")) {
1456
+ fail("provider-file");
1457
+ }
1458
+ return { effectiveModel };
1459
+ }
1313
1460
  /** `json_schema` is honored natively when the profile's `supportsJsonSchema`
1314
1461
  * capability is true for this model (the default); otherwise the engine
1315
1462
  * downgrades the request to plain `json_object` (see `buildRequestBody`),
@@ -1483,7 +1630,7 @@ var OpenAICompatibleProvider = class {
1483
1630
  formatMessage(msg, model) {
1484
1631
  const out = {
1485
1632
  role: this.profile.roleFor ? this.profile.roleFor(msg.role, model) : msg.role,
1486
- content: msg.content
1633
+ content: Array.isArray(msg.content) && this.name === "openrouter" ? openRouterImageParts(msg.content, model) : msg.content
1487
1634
  };
1488
1635
  if (msg.name && (this.profile.capabilities?.emitsMessageName ?? true)) out.name = msg.name;
1489
1636
  if (msg.tool_calls) out.tool_calls = msg.tool_calls;
@@ -2230,8 +2377,88 @@ var OpenAIProvider = class extends OpenAICompatibleProvider {
2230
2377
  };
2231
2378
 
2232
2379
  // src/providers/openai-responses.ts
2380
+ var OPENAI_RESPONSES_IMAGE_MODELS = /* @__PURE__ */ new Set([
2381
+ "gpt-4o",
2382
+ "gpt-4o-2024-08-06",
2383
+ "gpt-4o-2024-11-20",
2384
+ "gpt-4o-mini",
2385
+ "gpt-4o-mini-2024-07-18"
2386
+ ]);
2387
+ function base64FromSource(source) {
2388
+ return source.type === "base64" ? source.data : Buffer.from(source.data.buffer, source.data.byteOffset, source.data.byteLength).toString(
2389
+ "base64"
2390
+ );
2391
+ }
2392
+ function responseImageParts(parts) {
2393
+ const mapped = [];
2394
+ for (const part of parts) {
2395
+ if (part.type === "text") {
2396
+ mapped.push({ type: "input_text", text: part.text });
2397
+ continue;
2398
+ }
2399
+ const { source } = part;
2400
+ if (source.type === "provider-file") {
2401
+ if (source.provider !== "openai-responses") {
2402
+ throw new UnsupportedModelInputError({
2403
+ provider: "openai-responses",
2404
+ model: "unknown",
2405
+ modality: "image",
2406
+ source: "provider-file"
2407
+ });
2408
+ }
2409
+ mapped.push({ type: "input_image", file_id: source.reference });
2410
+ } else if (source.type === "url") {
2411
+ mapped.push({ type: "input_image", image_url: source.url });
2412
+ } else {
2413
+ mapped.push({
2414
+ type: "input_image",
2415
+ image_url: `data:${source.mediaType};base64,${base64FromSource(source)}`
2416
+ });
2417
+ }
2418
+ if (part.label) mapped.push({ type: "input_text", text: `[Image: ${part.label}]` });
2419
+ }
2420
+ return mapped;
2421
+ }
2233
2422
  var OpenAIResponsesProvider = class {
2234
2423
  name = "openai-responses";
2424
+ inputCapabilities(model) {
2425
+ return OPENAI_RESPONSES_IMAGE_MODELS.has(model) ? { image: { sources: ["url", "bytes", "base64", "provider-file"] } } : {};
2426
+ }
2427
+ validateInput(request) {
2428
+ const effectiveModel = typeof request.providerOptions?.model === "string" ? request.providerOptions.model : request.model;
2429
+ const fail = (source, feature) => {
2430
+ throw new UnsupportedModelInputError({
2431
+ provider: this.name,
2432
+ model: effectiveModel || request.model,
2433
+ modality: "image",
2434
+ ...source ? { source } : {},
2435
+ ...feature ? { feature } : {}
2436
+ });
2437
+ };
2438
+ if (!OPENAI_RESPONSES_IMAGE_MODELS.has(effectiveModel)) {
2439
+ fail(void 0, "image input for this model");
2440
+ }
2441
+ if (request.providerOptions && "input" in request.providerOptions) {
2442
+ fail(void 0, "raw input-container providerOptions");
2443
+ }
2444
+ for (const message of request.history) {
2445
+ if (!Array.isArray(message.content)) continue;
2446
+ if (message.role !== "user") fail(void 0, "rich non-user history");
2447
+ for (const part of message.content) {
2448
+ if (part.type === "image" && part.source.type === "provider-file" && part.source.provider !== this.name) {
2449
+ fail("provider-file");
2450
+ }
2451
+ }
2452
+ }
2453
+ if (Array.isArray(request.input)) {
2454
+ for (const part of request.input) {
2455
+ if (part.type === "image" && part.source.type === "provider-file" && part.source.provider !== this.name) {
2456
+ fail("provider-file");
2457
+ }
2458
+ }
2459
+ }
2460
+ return { effectiveModel };
2461
+ }
2235
2462
  /** The Responses API honors `json_schema` (structured outputs) natively. */
2236
2463
  nativeStructuredOutputSupport() {
2237
2464
  return "schema";
@@ -2396,11 +2623,12 @@ var OpenAIResponsesProvider = class {
2396
2623
  buildInput(messages) {
2397
2624
  const input = [];
2398
2625
  for (const msg of messages) {
2626
+ const text = typeof msg.content === "string" ? msg.content : "";
2399
2627
  if (msg.role === "tool") {
2400
2628
  input.push({
2401
2629
  type: "function_call_output",
2402
2630
  call_id: msg.tool_call_id ?? "",
2403
- output: msg.content
2631
+ output: text
2404
2632
  });
2405
2633
  } else if (msg.role === "assistant" && msg.tool_calls && msg.tool_calls.length > 0) {
2406
2634
  const reasoningItems = msg.providerMetadata?.openaiReasoningItems;
@@ -2409,8 +2637,8 @@ var OpenAIResponsesProvider = class {
2409
2637
  input.push(item);
2410
2638
  }
2411
2639
  }
2412
- if (msg.content) {
2413
- input.push({ type: "message", role: "assistant", content: msg.content });
2640
+ if (text) {
2641
+ input.push({ type: "message", role: "assistant", content: text });
2414
2642
  }
2415
2643
  for (const tc of msg.tool_calls) {
2416
2644
  input.push({
@@ -2430,7 +2658,7 @@ var OpenAIResponsesProvider = class {
2430
2658
  input.push({
2431
2659
  type: "message",
2432
2660
  role: msg.role,
2433
- content: msg.content
2661
+ content: typeof msg.content === "string" ? msg.content : responseImageParts(msg.content)
2434
2662
  });
2435
2663
  }
2436
2664
  }
@@ -2497,7 +2725,7 @@ var OpenAIResponsesProvider = class {
2497
2725
  cached_tokens: json.usage.input_tokens_details?.cached_tokens,
2498
2726
  cache_write_tokens: json.usage.input_tokens_details?.cache_write_tokens
2499
2727
  } : void 0;
2500
- const cost = usage ? estimateDirectOpenAICost(json.model ?? model, usage, {
2728
+ const cost = usage && !this.requestContainsImages(request) ? estimateDirectOpenAICost(json.model ?? model, usage, {
2501
2729
  baseUrl: this.baseUrl,
2502
2730
  request,
2503
2731
  response: json
@@ -2512,6 +2740,16 @@ var OpenAIResponsesProvider = class {
2512
2740
  providerMetadata
2513
2741
  };
2514
2742
  }
2743
+ /** Responses usage does not report a media-inclusive price, so rich calls stay unpriced. */
2744
+ requestContainsImages(request) {
2745
+ return Array.isArray(request?.input) && request.input.some((item) => {
2746
+ if (!item || typeof item !== "object") return false;
2747
+ const content = item.content;
2748
+ return Array.isArray(content) && content.some(
2749
+ (part) => part !== null && typeof part === "object" && part.type === "input_image"
2750
+ );
2751
+ });
2752
+ }
2515
2753
  // ---------------------------------------------------------------------------
2516
2754
  // Internal: parse SSE stream
2517
2755
  // ---------------------------------------------------------------------------
@@ -2599,7 +2837,7 @@ var OpenAIResponsesProvider = class {
2599
2837
  return {
2600
2838
  type: "done",
2601
2839
  usage,
2602
- cost: usage ? estimateDirectOpenAICost(response?.model ?? model, usage, {
2840
+ cost: usage && !this.requestContainsImages(request) ? estimateDirectOpenAICost(response?.model ?? model, usage, {
2603
2841
  baseUrl: this.baseUrl,
2604
2842
  request,
2605
2843
  response
@@ -2636,7 +2874,51 @@ var OpenAIResponsesProvider = class {
2636
2874
  };
2637
2875
 
2638
2876
  // src/providers/anthropic.ts
2877
+ function anthropicBase64(source) {
2878
+ return source.type === "base64" ? source.data : Buffer.from(source.data.buffer, source.data.byteOffset, source.data.byteLength).toString(
2879
+ "base64"
2880
+ );
2881
+ }
2882
+ function anthropicImageBlocks(parts) {
2883
+ const blocks = [];
2884
+ for (const part of parts) {
2885
+ if (part.type === "text") {
2886
+ blocks.push({ type: "text", text: part.text });
2887
+ continue;
2888
+ }
2889
+ const { source } = part;
2890
+ if (source.type === "provider-file") {
2891
+ if (source.provider !== "anthropic") {
2892
+ throw new UnsupportedModelInputError({
2893
+ provider: "anthropic",
2894
+ model: "unknown",
2895
+ modality: "image",
2896
+ source: "provider-file"
2897
+ });
2898
+ }
2899
+ blocks.push({ type: "image", source: { type: "file", file_id: source.reference } });
2900
+ } else if (source.type === "url") {
2901
+ blocks.push({ type: "image", source: { type: "url", url: source.url } });
2902
+ } else {
2903
+ blocks.push({
2904
+ type: "image",
2905
+ source: { type: "base64", media_type: source.mediaType, data: anthropicBase64(source) }
2906
+ });
2907
+ }
2908
+ if (part.label) blocks.push({ type: "text", text: `[Image: ${part.label}]` });
2909
+ }
2910
+ return blocks;
2911
+ }
2639
2912
  var ANTHROPIC_API_VERSION = "2023-06-01";
2913
+ var ANTHROPIC_FILES_BETA = "files-api-2025-04-14";
2914
+ var ANTHROPIC_IMAGE_MODELS = /* @__PURE__ */ new Set(["claude-sonnet-4-5", "claude-opus-4-8"]);
2915
+ function hasAnthropicProviderFileImage(messages) {
2916
+ return messages.some(
2917
+ (message) => Array.isArray(message.content) && message.content.some(
2918
+ (part) => part.type === "image" && part.source.type === "provider-file" && part.source.provider === "anthropic"
2919
+ )
2920
+ );
2921
+ }
2640
2922
  var CLAUDE_CAPABILITIES = {
2641
2923
  "claude-fable-5": {
2642
2924
  thinking: "adaptive-always-on",
@@ -2897,6 +3179,11 @@ function isEffortAtOrBelow(effort, ceiling) {
2897
3179
  function pricingContextFromBody(body) {
2898
3180
  return {
2899
3181
  model: typeof body.model === "string" ? body.model : void 0,
3182
+ hasRichInput: Array.isArray(body.messages) && body.messages.some(
3183
+ (message) => message !== null && typeof message === "object" && Array.isArray(message.content) && message.content.some(
3184
+ (part) => part !== null && typeof part === "object" && part.type === "image"
3185
+ )
3186
+ ),
2900
3187
  unpricedModifier: isAnthropicModifier("inference_geo", body.inference_geo) || isAnthropicModifier("speed", body.speed) || hasAnthropicServerTool(body.tools) || body.fallbacks !== void 0
2901
3188
  };
2902
3189
  }
@@ -2991,6 +3278,44 @@ function mergeAnthropicUsage(initial, update) {
2991
3278
  }
2992
3279
  var AnthropicProvider = class {
2993
3280
  name = "anthropic";
3281
+ inputCapabilities(model) {
3282
+ return ANTHROPIC_IMAGE_MODELS.has(model) ? { image: { sources: ["url", "bytes", "base64", "provider-file"] } } : {};
3283
+ }
3284
+ validateInput(request) {
3285
+ const effectiveModel = typeof request.providerOptions?.model === "string" ? request.providerOptions.model : request.model;
3286
+ const fail = (source, feature) => {
3287
+ throw new UnsupportedModelInputError({
3288
+ provider: this.name,
3289
+ model: effectiveModel || request.model,
3290
+ modality: "image",
3291
+ ...source ? { source } : {},
3292
+ ...feature ? { feature } : {}
3293
+ });
3294
+ };
3295
+ if (!ANTHROPIC_IMAGE_MODELS.has(effectiveModel)) {
3296
+ fail(void 0, "image input for this model");
3297
+ }
3298
+ if (request.providerOptions && "messages" in request.providerOptions) {
3299
+ fail(void 0, "raw messages providerOptions");
3300
+ }
3301
+ for (const message of request.history) {
3302
+ if (!Array.isArray(message.content)) continue;
3303
+ if (message.role !== "user") fail(void 0, "rich non-user history");
3304
+ for (const part of message.content) {
3305
+ if (part.type === "image" && part.source.type === "provider-file" && part.source.provider !== this.name) {
3306
+ fail("provider-file");
3307
+ }
3308
+ }
3309
+ }
3310
+ if (Array.isArray(request.input)) {
3311
+ for (const part of request.input) {
3312
+ if (part.type === "image" && part.source.type === "provider-file" && part.source.provider !== this.name) {
3313
+ fail("provider-file");
3314
+ }
3315
+ }
3316
+ }
3317
+ return { effectiveModel };
3318
+ }
2994
3319
  /** Anthropic ignores native `json_schema` structurally — Axl uses a system
2995
3320
  * prompt JSON instruction + client-side Zod validation (see buildRequest). */
2996
3321
  nativeStructuredOutputSupport() {
@@ -3028,7 +3353,10 @@ var AnthropicProvider = class {
3028
3353
  // chat - non-streaming completion
3029
3354
  // ---------------------------------------------------------------------------
3030
3355
  async chat(messages, options) {
3031
- const headers = this.buildHeaders(await this.resolveKey());
3356
+ const headers = this.buildHeaders(
3357
+ await this.resolveKey(),
3358
+ hasAnthropicProviderFileImage(messages)
3359
+ );
3032
3360
  const body = this.buildRequestBody(messages, options, false);
3033
3361
  const pricingContext = pricingContextFromBody(body);
3034
3362
  const res = await fetchWithRetry(
@@ -3059,7 +3387,10 @@ var AnthropicProvider = class {
3059
3387
  // stream - SSE streaming completion
3060
3388
  // ---------------------------------------------------------------------------
3061
3389
  async *stream(messages, options) {
3062
- const headers = this.buildHeaders(await this.resolveKey());
3390
+ const headers = this.buildHeaders(
3391
+ await this.resolveKey(),
3392
+ hasAnthropicProviderFileImage(messages)
3393
+ );
3063
3394
  const body = this.buildRequestBody(messages, options, true);
3064
3395
  const pricingContext = pricingContextFromBody(body);
3065
3396
  const res = await fetchWithRetry(
@@ -3091,11 +3422,12 @@ var AnthropicProvider = class {
3091
3422
  // ---------------------------------------------------------------------------
3092
3423
  // Internal: request building
3093
3424
  // ---------------------------------------------------------------------------
3094
- buildHeaders(apiKey) {
3425
+ buildHeaders(apiKey, requiresFilesBeta = false) {
3095
3426
  return {
3096
3427
  "Content-Type": "application/json",
3097
3428
  "x-api-key": apiKey,
3098
- "anthropic-version": ANTHROPIC_API_VERSION
3429
+ "anthropic-version": ANTHROPIC_API_VERSION,
3430
+ ...requiresFilesBeta ? { "anthropic-beta": ANTHROPIC_FILES_BETA } : {}
3099
3431
  };
3100
3432
  }
3101
3433
  /** Extract a human-readable message from an API error response body. */
@@ -3165,12 +3497,13 @@ ${jsonInstruction}` : jsonInstruction;
3165
3497
  mapMessages(messages) {
3166
3498
  const result = [];
3167
3499
  for (const msg of messages) {
3500
+ const text = typeof msg.content === "string" ? msg.content : "";
3168
3501
  if (msg.role === "assistant") {
3169
3502
  const replayedThinking = this.getReplayedThinkingBlocks(msg);
3170
3503
  if (msg.tool_calls && msg.tool_calls.length > 0) {
3171
3504
  const content = [...replayedThinking];
3172
- if (msg.content) {
3173
- content.push({ type: "text", text: msg.content });
3505
+ if (text) {
3506
+ content.push({ type: "text", text });
3174
3507
  }
3175
3508
  for (const tc of msg.tool_calls) {
3176
3509
  let parsedArgs;
@@ -3189,10 +3522,10 @@ ${jsonInstruction}` : jsonInstruction;
3189
3522
  result.push({ role: "assistant", content });
3190
3523
  } else if (replayedThinking.length > 0) {
3191
3524
  const content = [...replayedThinking];
3192
- if (msg.content) content.push({ type: "text", text: msg.content });
3525
+ if (text) content.push({ type: "text", text });
3193
3526
  result.push({ role: "assistant", content });
3194
3527
  } else {
3195
- result.push({ role: "assistant", content: msg.content });
3528
+ result.push({ role: "assistant", content: text });
3196
3529
  }
3197
3530
  } else if (msg.role === "tool") {
3198
3531
  result.push({
@@ -3201,12 +3534,15 @@ ${jsonInstruction}` : jsonInstruction;
3201
3534
  {
3202
3535
  type: "tool_result",
3203
3536
  tool_use_id: msg.tool_call_id,
3204
- content: msg.content
3537
+ content: text
3205
3538
  }
3206
3539
  ]
3207
3540
  });
3208
3541
  } else if (msg.role === "user") {
3209
- result.push({ role: "user", content: msg.content });
3542
+ result.push({
3543
+ role: "user",
3544
+ content: typeof msg.content === "string" ? msg.content : anthropicImageBlocks(msg.content)
3545
+ });
3210
3546
  }
3211
3547
  }
3212
3548
  return this.mergeConsecutiveRoles(result);
@@ -3311,7 +3647,7 @@ ${jsonInstruction}` : jsonInstruction;
3311
3647
  "Anthropic fallback responses cannot be continued safely; retry without fallbacks."
3312
3648
  );
3313
3649
  }
3314
- const cost = normalized && !pricingContext.unpricedModifier && !isModifiedAnthropicResponse(json, effectiveModel) && !hasFallbackBoundary && !refused ? estimateAnthropicCost(effectiveModel, normalized.pricingUsage) : void 0;
3650
+ const cost = normalized && !pricingContext.hasRichInput && !pricingContext.unpricedModifier && !isModifiedAnthropicResponse(json, effectiveModel) && !hasFallbackBoundary && !refused ? estimateAnthropicCost(effectiveModel, normalized.pricingUsage) : void 0;
3315
3651
  return {
3316
3652
  content,
3317
3653
  thinking_content: thinkingContent || void 0,
@@ -3348,7 +3684,7 @@ ${jsonInstruction}` : jsonInstruction;
3348
3684
  const doneChunk = () => ({
3349
3685
  type: "done",
3350
3686
  usage,
3351
- cost: pricingUsage && !unpricedModifier && !hasFallbackBoundary && !refused ? estimateAnthropicCost(effectiveModel, pricingUsage) : void 0,
3687
+ cost: pricingUsage && !pricingContext.hasRichInput && !unpricedModifier && !hasFallbackBoundary && !refused ? estimateAnthropicCost(effectiveModel, pricingUsage) : void 0,
3352
3688
  providerMetadata: thinkingBlocks.length > 0 && !hasFallbackBoundary && !hasFallbackIterationSignal ? { anthropicThinkingBlocks: thinkingBlocks } : void 0
3353
3689
  });
3354
3690
  try {
@@ -3496,6 +3832,60 @@ function lastFallbackIndex(blocks) {
3496
3832
  }
3497
3833
 
3498
3834
  // src/providers/gemini.ts
3835
+ function hasRichGeminiMessages(messages) {
3836
+ return messages.some((message) => Array.isArray(message.content));
3837
+ }
3838
+ function geminiImageBase64(source) {
3839
+ return source.type === "base64" ? source.data : Buffer.from(source.data.buffer, source.data.byteOffset, source.data.byteLength).toString(
3840
+ "base64"
3841
+ );
3842
+ }
3843
+ function geminiInteractionContent(parts, model) {
3844
+ const content = [];
3845
+ for (const part of parts) {
3846
+ if (part.type === "text") {
3847
+ content.push({ type: "text", text: part.text });
3848
+ continue;
3849
+ }
3850
+ const { source } = part;
3851
+ if (source.type === "provider-file") {
3852
+ if (source.provider !== "google") {
3853
+ throw new UnsupportedModelInputError({
3854
+ provider: "google",
3855
+ model,
3856
+ modality: "image",
3857
+ source: "provider-file"
3858
+ });
3859
+ }
3860
+ if (!source.mediaType) {
3861
+ throw new UnsupportedModelInputError({
3862
+ provider: "google",
3863
+ model,
3864
+ modality: "image",
3865
+ source: "provider-file",
3866
+ feature: "Interactions URI image mediaType"
3867
+ });
3868
+ }
3869
+ content.push({
3870
+ type: "image",
3871
+ uri: source.reference,
3872
+ mime_type: source.mediaType
3873
+ });
3874
+ } else if (source.type === "url") {
3875
+ throw new UnsupportedModelInputError({
3876
+ provider: "google",
3877
+ model,
3878
+ modality: "image",
3879
+ source: "url",
3880
+ feature: "direct URL image input; pass bytes/base64 or a Gemini provider-file"
3881
+ });
3882
+ } else {
3883
+ content.push({ type: "image", data: geminiImageBase64(source), mime_type: source.mediaType });
3884
+ }
3885
+ if (part.label) content.push({ type: "text", text: `[Image: ${part.label}]` });
3886
+ }
3887
+ return content;
3888
+ }
3499
3889
  var GEMINI_DISALLOWED_SCHEMA_KEYS = /* @__PURE__ */ new Set([
3500
3890
  "additionalProperties",
3501
3891
  "$schema",
@@ -3567,6 +3957,7 @@ var GEMINI_PRICING = {
3567
3957
  "gemini-3.5-flash-lite": { input: 3e-7, cached: 3e-8, output: 25e-7 },
3568
3958
  "gemini-3.6-flash": { input: 15e-7, cached: 15e-8, output: 75e-7 }
3569
3959
  };
3960
+ var GEMINI_INTERACTIONS_IMAGE_MODELS = /* @__PURE__ */ new Set(["gemini-3.7-flash"]);
3570
3961
  function isValidTokenCount2(value) {
3571
3962
  return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
3572
3963
  }
@@ -3654,7 +4045,10 @@ var GEMINI_3X_MIN_THINKING_LEVEL = /* @__PURE__ */ new Map([
3654
4045
  ["gemini-3.1-flash-lite-preview", "minimal"],
3655
4046
  ["gemini-3.5-flash", "minimal"],
3656
4047
  ["gemini-3.5-flash-lite", "minimal"],
3657
- ["gemini-3.6-flash", "minimal"]
4048
+ ["gemini-3.6-flash", "minimal"],
4049
+ // L4 live evidence: Interactions gemini-3.7-flash accepts low/medium/high,
4050
+ // but rejects minimal. Keep this exact entry isolated from legacy aliases.
4051
+ ["gemini-3.7-flash", "low"]
3658
4052
  ]);
3659
4053
  var GEMINI_MODELS_WITHOUT_PORTABLE_TEMPERATURE = /* @__PURE__ */ new Set([
3660
4054
  "gemini-3.5-flash-lite",
@@ -3681,6 +4075,61 @@ function warnGemini3xEffortNone(model) {
3681
4075
  }
3682
4076
  var GeminiProvider = class {
3683
4077
  name = "google";
4078
+ inputCapabilities(model) {
4079
+ return GEMINI_INTERACTIONS_IMAGE_MODELS.has(model) ? { image: { sources: ["bytes", "base64", "provider-file"] } } : {};
4080
+ }
4081
+ validateInput(request) {
4082
+ const effectiveModel = typeof request.providerOptions?.model === "string" ? request.providerOptions.model : request.model;
4083
+ const fail = (source, feature) => {
4084
+ throw new UnsupportedModelInputError({
4085
+ provider: this.name,
4086
+ model: effectiveModel || request.model,
4087
+ modality: "image",
4088
+ ...source ? { source } : {},
4089
+ ...feature ? { feature } : {}
4090
+ });
4091
+ };
4092
+ if (!GEMINI_INTERACTIONS_IMAGE_MODELS.has(effectiveModel)) {
4093
+ fail(void 0, "image input for this model");
4094
+ }
4095
+ if (request.providerOptions) {
4096
+ const forbidden = [
4097
+ "input",
4098
+ "contents",
4099
+ "messages",
4100
+ "previous_interaction_id",
4101
+ "background",
4102
+ "stream",
4103
+ "store"
4104
+ ].find((key) => key in request.providerOptions);
4105
+ if (forbidden) fail(void 0, `raw ${forbidden} providerOptions`);
4106
+ }
4107
+ for (const message of request.history) {
4108
+ if (!Array.isArray(message.content)) continue;
4109
+ if (message.role !== "user") fail(void 0, "rich non-user history");
4110
+ for (const part of message.content) {
4111
+ if (part.type !== "image") continue;
4112
+ if (part.source.type === "url")
4113
+ fail("url", "direct URL image input; pass bytes/base64 or a Gemini provider-file");
4114
+ if (part.source.type === "provider-file" && part.source.provider !== this.name)
4115
+ fail("provider-file");
4116
+ if (part.source.type === "provider-file" && !part.source.mediaType)
4117
+ fail("provider-file", "Interactions URI image mediaType");
4118
+ }
4119
+ }
4120
+ if (Array.isArray(request.input)) {
4121
+ for (const part of request.input) {
4122
+ if (part.type !== "image") continue;
4123
+ if (part.source.type === "url")
4124
+ fail("url", "direct URL image input; pass bytes/base64 or a Gemini provider-file");
4125
+ if (part.source.type === "provider-file" && part.source.provider !== this.name)
4126
+ fail("provider-file");
4127
+ if (part.source.type === "provider-file" && !part.source.mediaType)
4128
+ fail("provider-file", "Interactions URI image mediaType");
4129
+ }
4130
+ }
4131
+ return { effectiveModel };
4132
+ }
3684
4133
  /** Gemini accepts a `responseSchema` but `sanitizeSchemaForGemini` strips
3685
4134
  * keywords it doesn't support (`$ref`/`$defs`/`additionalProperties`/…), so a
3686
4135
  * derived schema can lose constraints — lossy, not faithful. */
@@ -3719,6 +4168,7 @@ var GeminiProvider = class {
3719
4168
  // chat - non-streaming completion
3720
4169
  // ---------------------------------------------------------------------------
3721
4170
  async chat(messages, options) {
4171
+ if (hasRichGeminiMessages(messages)) return this.chatInteraction(messages, options);
3722
4172
  const body = this.buildRequestBody(messages, options);
3723
4173
  const pricingContext = this.pricingContext(this.requestModel(options), body);
3724
4174
  this.assertSafeGemini3Continuation(messages, pricingContext.model);
@@ -3751,6 +4201,10 @@ var GeminiProvider = class {
3751
4201
  // stream - SSE streaming completion
3752
4202
  // ---------------------------------------------------------------------------
3753
4203
  async *stream(messages, options) {
4204
+ if (hasRichGeminiMessages(messages)) {
4205
+ yield* this.streamInteraction(messages, options);
4206
+ return;
4207
+ }
3754
4208
  const body = this.buildRequestBody(messages, options);
3755
4209
  const pricingContext = this.pricingContext(this.requestModel(options), body);
3756
4210
  this.assertSafeGemini3Continuation(messages, pricingContext.model);
@@ -3782,82 +4236,415 @@ var GeminiProvider = class {
3782
4236
  yield* this.parseSSEStream(res.body, pricingContext);
3783
4237
  }
3784
4238
  // ---------------------------------------------------------------------------
3785
- // Internal: request building
4239
+ // Rich image requests use Gemini Interactions. It is deliberately stateless:
4240
+ // Axl sends the complete application-owned history and never asks Google to
4241
+ // retain an interaction or follow a previous_interaction_id.
3786
4242
  // ---------------------------------------------------------------------------
3787
- buildHeaders(apiKey) {
3788
- return {
3789
- "Content-Type": "application/json",
3790
- "x-goog-api-key": apiKey
3791
- };
4243
+ async chatInteraction(messages, options) {
4244
+ this.assertSafeInteractionOptions(options);
4245
+ const body = this.buildInteractionRequestBody(messages, options, false);
4246
+ const headers = this.buildHeaders(await this.resolveKey());
4247
+ const res = await fetchWithRetry(
4248
+ `${this.baseUrl}/interactions`,
4249
+ { method: "POST", headers, body: JSON.stringify(body), signal: options.signal },
4250
+ { governor: this.governor, provider: this.name }
4251
+ );
4252
+ if (!res.ok) {
4253
+ const errorBody = await res.text();
4254
+ throw buildProviderError({
4255
+ provider: this.name,
4256
+ status: res.status,
4257
+ headers: res.headers,
4258
+ message: this.extractErrorMessage(errorBody, res.status),
4259
+ body: errorBody
4260
+ });
4261
+ }
4262
+ return this.parseInteractionResponse(await res.json());
3792
4263
  }
3793
- extractErrorMessage(body, status) {
3794
- try {
3795
- const json = JSON.parse(body);
3796
- if (json.error?.message) {
3797
- return `Gemini API error (${status}): ${json.error.message}`;
3798
- }
3799
- } catch {
4264
+ async *streamInteraction(messages, options) {
4265
+ this.assertSafeInteractionOptions(options);
4266
+ const body = this.buildInteractionRequestBody(messages, options, true);
4267
+ const headers = this.buildHeaders(await this.resolveKey());
4268
+ const res = await fetchWithRetry(
4269
+ `${this.baseUrl}/interactions?alt=sse`,
4270
+ { method: "POST", headers, body: JSON.stringify(body), signal: options.signal },
4271
+ { governor: this.governor, provider: this.name }
4272
+ );
4273
+ if (!res.ok) {
4274
+ const errorBody = await res.text();
4275
+ throw buildProviderError({
4276
+ provider: this.name,
4277
+ status: res.status,
4278
+ headers: res.headers,
4279
+ message: this.extractErrorMessage(errorBody, res.status),
4280
+ body: errorBody
4281
+ });
3800
4282
  }
3801
- return `Gemini API error (${status}): ${body}`;
4283
+ if (!res.body) throw new Error("Gemini Interactions stream has no body");
4284
+ yield* this.parseInteractionSSEStream(res.body);
3802
4285
  }
3803
- pricingContext(fallbackModel, body) {
3804
- return {
3805
- // Gemini's model is normally selected in the URL. providerOptions is an
3806
- // explicit raw escape hatch, so honor a post-merge model override for both
3807
- // the URL and pricing rather than pricing a different request.
3808
- model: typeof body.model === "string" ? body.model : fallbackModel,
3809
- serviceTier: body.serviceTier,
3810
- eligibleRequest: this.baseUrl === "https://generativelanguage.googleapis.com/v1beta" && this.isTextOnlyClientRequest(body)
4286
+ buildInteractionRequestBody(messages, options, stream) {
4287
+ const model = this.requestModel(options);
4288
+ const systemInstruction = messages.filter((message) => message.role === "system").map((message) => typeof message.content === "string" ? message.content : "").filter(Boolean).join("\n\n");
4289
+ const body = {
4290
+ model,
4291
+ input: this.mapInteractionInput(
4292
+ messages.filter((message) => message.role !== "system"),
4293
+ model
4294
+ ),
4295
+ stream
3811
4296
  };
3812
- }
3813
- requestModel(options) {
3814
- return typeof options.providerOptions?.model === "string" ? options.providerOptions.model : options.model;
3815
- }
3816
- assertSafeGemini3Continuation(messages, model) {
3817
- if (GEMINI_MODELS_WITHOUT_PORTABLE_TEMPERATURE.has(model)) {
3818
- const mapped = this.mapMessages(messages.filter((message) => message.role !== "system"));
3819
- if (mapped.at(-1)?.role === "model") {
3820
- throw new Error(
3821
- `${model} does not support a terminal assistant/model prefill; end the request with a user or tool message.`
3822
- );
3823
- }
4297
+ if (systemInstruction) body.system_instruction = systemInstruction;
4298
+ if (options.tools?.length) {
4299
+ body.tools = options.tools.map((tool2) => ({
4300
+ type: "function",
4301
+ name: tool2.function.name,
4302
+ description: tool2.function.description,
4303
+ parameters: sanitizeSchemaForGemini(tool2.function.parameters)
4304
+ }));
3824
4305
  }
3825
- if (!isGemini3x(model)) return;
3826
- const replayedNativeCallIds = /* @__PURE__ */ new Set();
4306
+ const generationConfig = {};
4307
+ if (options.maxTokens !== void 0) generationConfig.max_output_tokens = options.maxTokens;
4308
+ if (options.stop) generationConfig.stop_sequences = options.stop;
4309
+ if (options.toolChoice !== void 0) {
4310
+ generationConfig.tool_choice = typeof options.toolChoice === "string" ? options.toolChoice === "required" ? "any" : options.toolChoice : { allowed_tools: { mode: "any", tools: [options.toolChoice.function.name] } };
4311
+ }
4312
+ const thinking = resolveThinkingOptions(options);
4313
+ if (thinking.thinkingDisabled) {
4314
+ generationConfig.thinking_level = isGemini3x(model) ? minThinkingLevel(model) : "minimal";
4315
+ } else if (thinking.hasBudgetOverride) {
4316
+ generationConfig.thinking_level = budgetToThinkingLevel(thinking.thinkingBudget);
4317
+ } else if (thinking.activeEffort) {
4318
+ generationConfig.thinking_level = isGemini3x(model) ? THINKING_LEVELS[thinking.activeEffort] ?? "medium" : budgetToThinkingLevel(THINKING_BUDGETS2[thinking.activeEffort] ?? 5e3);
4319
+ }
4320
+ if (thinking.includeThoughts) generationConfig.thinking_summaries = "auto";
4321
+ if (Object.keys(generationConfig).length) body.generation_config = generationConfig;
4322
+ if (options.responseFormat?.type && options.responseFormat.type !== "text") {
4323
+ body.response_format = {
4324
+ type: "text",
4325
+ mime_type: "application/json",
4326
+ ...options.responseFormat.type === "json_schema" ? { schema: sanitizeSchemaForGemini(options.responseFormat.json_schema.schema) } : {}
4327
+ };
4328
+ }
4329
+ if (options.providerOptions) {
4330
+ const overrides = { ...options.providerOptions };
4331
+ delete overrides.model;
4332
+ delete overrides.input;
4333
+ delete overrides.contents;
4334
+ delete overrides.messages;
4335
+ delete overrides.previous_interaction_id;
4336
+ delete overrides.background;
4337
+ delete overrides.stream;
4338
+ delete overrides.store;
4339
+ const rawGenerationConfig = overrides.generation_config;
4340
+ if (rawGenerationConfig && typeof rawGenerationConfig === "object" && !Array.isArray(rawGenerationConfig)) {
4341
+ const generationConfigOverrides = { ...rawGenerationConfig };
4342
+ delete generationConfigOverrides.temperature;
4343
+ if (Object.keys(generationConfigOverrides).length)
4344
+ overrides.generation_config = generationConfigOverrides;
4345
+ else delete overrides.generation_config;
4346
+ }
4347
+ Object.assign(body, overrides);
4348
+ }
4349
+ body.stream = stream;
4350
+ body.store = false;
4351
+ return body;
4352
+ }
4353
+ assertSafeInteractionOptions(options) {
4354
+ const model = this.requestModel(options);
4355
+ const fail = (feature) => {
4356
+ throw new UnsupportedModelInputError({
4357
+ provider: this.name,
4358
+ model,
4359
+ modality: "image",
4360
+ feature
4361
+ });
4362
+ };
4363
+ if (!GEMINI_INTERACTIONS_IMAGE_MODELS.has(model)) fail("image input for this model");
4364
+ const forbidden = [
4365
+ "input",
4366
+ "contents",
4367
+ "messages",
4368
+ "previous_interaction_id",
4369
+ "background",
4370
+ "stream",
4371
+ "store"
4372
+ ].find((key) => key in (options.providerOptions ?? {}));
4373
+ if (forbidden) fail(`raw ${forbidden} providerOptions`);
4374
+ }
4375
+ mapInteractionInput(messages, model) {
4376
+ const steps = [];
3827
4377
  for (const message of messages) {
3828
- if (message.role !== "assistant" || !Array.isArray(message.providerMetadata?.geminiParts)) {
3829
- continue;
3830
- }
3831
- for (const part of message.providerMetadata.geminiParts) {
3832
- const id = part !== null && typeof part === "object" ? part.functionCall?.id : void 0;
3833
- if (!id) continue;
3834
- if (replayedNativeCallIds.has(id)) {
3835
- throw new Error(
3836
- `${model} native functionCall ids must be globally unique across a replay.`
3837
- );
4378
+ if (message.role === "user") {
4379
+ steps.push({
4380
+ type: "user_input",
4381
+ content: Array.isArray(message.content) ? geminiInteractionContent(message.content, model) : [{ type: "text", text: message.content }]
4382
+ });
4383
+ } else if (message.role === "assistant") {
4384
+ const nativeSteps = message.providerMetadata?.geminiInteractionSteps;
4385
+ if (Array.isArray(nativeSteps) && nativeSteps.every(isGeminiInteractionStep)) {
4386
+ steps.push(...nativeSteps);
4387
+ continue;
3838
4388
  }
3839
- replayedNativeCallIds.add(id);
4389
+ if (typeof message.content === "string" && message.content) {
4390
+ steps.push({ type: "model_output", content: [{ type: "text", text: message.content }] });
4391
+ }
4392
+ for (const call of message.tool_calls ?? []) {
4393
+ steps.push({
4394
+ type: "function_call",
4395
+ id: call.id,
4396
+ name: call.function.name,
4397
+ arguments: safeJsonObject(call.function.arguments)
4398
+ });
4399
+ }
4400
+ } else if (message.role === "tool") {
4401
+ steps.push({
4402
+ type: "function_result",
4403
+ call_id: message.tool_call_id ?? "",
4404
+ result: [
4405
+ { type: "text", text: typeof message.content === "string" ? message.content : "" }
4406
+ ]
4407
+ });
3840
4408
  }
3841
4409
  }
3842
- const consumedToolMessages = /* @__PURE__ */ new Set();
3843
- for (const [assistantIndex, message] of messages.entries()) {
3844
- if (message.role !== "assistant" || !message.tool_calls?.length) continue;
3845
- const rawParts = message.providerMetadata?.geminiParts;
3846
- if (!Array.isArray(rawParts)) {
3847
- throw new Error(
3848
- `${model} tool continuations require providerMetadata.geminiParts with thought signatures.`
3849
- );
4410
+ return steps;
4411
+ }
4412
+ parseInteractionResponse(json) {
4413
+ let content = "";
4414
+ let thinkingContent = "";
4415
+ const toolCalls = [];
4416
+ for (const step of json.steps ?? []) {
4417
+ if (step.type === "model_output") content += interactionText(step.content);
4418
+ else if (step.type === "thought") thinkingContent += interactionText(step.summary);
4419
+ else if (step.type === "function_call" && step.id && step.name) {
4420
+ toolCalls.push({
4421
+ id: step.id,
4422
+ type: "function",
4423
+ function: { name: step.name, arguments: JSON.stringify(step.arguments ?? {}) }
4424
+ });
3850
4425
  }
3851
- const nativeCalls = rawParts.flatMap((part) => {
3852
- const functionCall = part !== null && typeof part === "object" ? part.functionCall : void 0;
3853
- return functionCall?.id ? [{ id: functionCall.id, name: functionCall.name, part }] : [];
3854
- });
3855
- const expectedCalls = new Map(
3856
- message.tool_calls.map((call) => [call.id, call.function.name])
3857
- );
3858
- if (expectedCalls.size !== message.tool_calls.length || nativeCalls.length !== message.tool_calls.length || new Set(nativeCalls.map((call) => call.id)).size !== nativeCalls.length || typeof nativeCalls[0]?.part.thoughtSignature !== "string" || nativeCalls[0].part.thoughtSignature.length === 0 || nativeCalls.some((call) => expectedCalls.get(call.id) !== call.name)) {
3859
- throw new Error(
3860
- `${model} tool continuations require exact native functionCall id/name and a thoughtSignature on the first function call.`
4426
+ }
4427
+ this.assertInteractionTerminalStatus(json.status, toolCalls.length);
4428
+ const usage = normalizeInteractionUsage(json.usage);
4429
+ return {
4430
+ content,
4431
+ thinking_content: thinkingContent || void 0,
4432
+ tool_calls: toolCalls.length ? toolCalls : void 0,
4433
+ usage,
4434
+ // Interactions returns modality usage but no authoritative monetary cost.
4435
+ cost: void 0,
4436
+ providerMetadata: json.steps?.length ? { geminiInteractionSteps: json.steps.filter(isGeminiInteractionStep) } : void 0
4437
+ };
4438
+ }
4439
+ async *parseInteractionSSEStream(body) {
4440
+ const reader = body.getReader();
4441
+ const decoder = new TextDecoder();
4442
+ let buffer = "";
4443
+ const steps = /* @__PURE__ */ new Map();
4444
+ const argumentBuffers = /* @__PURE__ */ new Map();
4445
+ const stopped = /* @__PURE__ */ new Set();
4446
+ let completed = false;
4447
+ try {
4448
+ while (true) {
4449
+ const { done, value } = await reader.read();
4450
+ if (done) break;
4451
+ buffer += decoder.decode(value, { stream: true });
4452
+ const lines = buffer.split("\n");
4453
+ buffer = lines.pop() ?? "";
4454
+ for (const line of lines) {
4455
+ const trimmed = line.trim();
4456
+ if (!trimmed.startsWith("data: ")) continue;
4457
+ if (trimmed.slice(6) === "[DONE]") continue;
4458
+ let event;
4459
+ try {
4460
+ event = JSON.parse(trimmed.slice(6));
4461
+ } catch {
4462
+ continue;
4463
+ }
4464
+ if (event.event_type === "error") {
4465
+ throw new Error(event.error?.message ?? "Gemini Interactions stream failed");
4466
+ }
4467
+ if (event.event_type === "step.start" && event.step) {
4468
+ const index = event.index;
4469
+ if (index === void 0 || steps.has(index)) {
4470
+ throw new Error("Gemini Interactions stream contained an invalid step.start index");
4471
+ }
4472
+ const step = cloneInteractionStep(event.step);
4473
+ steps.set(index, step);
4474
+ if (step.type === "model_output") {
4475
+ for (const text of interactionTextChunks(step.content)) {
4476
+ yield { type: "text_delta", content: text };
4477
+ }
4478
+ } else if (step.type === "thought") {
4479
+ for (const text of interactionTextChunks(step.summary)) {
4480
+ yield { type: "thinking_delta", content: text };
4481
+ }
4482
+ }
4483
+ if (step.type === "function_call" && step.id && step.name) {
4484
+ argumentBuffers.set(index, "");
4485
+ yield { type: "tool_call_delta", id: step.id, name: step.name };
4486
+ }
4487
+ } else if (event.event_type === "step.delta" && event.delta) {
4488
+ const delta = event.delta;
4489
+ const index = event.index;
4490
+ if (index === void 0 || stopped.has(index)) {
4491
+ throw new Error("Gemini Interactions stream delta arrived outside an active step");
4492
+ }
4493
+ const step = steps.get(index);
4494
+ if (!step)
4495
+ throw new Error("Gemini Interactions stream delta arrived before step.start");
4496
+ if (delta.type === "text" && typeof delta.text === "string" && step.type === "model_output") {
4497
+ appendInteractionContent(step, "content", { type: "text", text: delta.text });
4498
+ yield { type: "text_delta", content: delta.text };
4499
+ } else if (delta.type === "thought_summary" && step.type === "thought" && delta.content) {
4500
+ appendInteractionContent(step, "summary", delta.content);
4501
+ for (const text of interactionTextChunks([delta.content])) {
4502
+ yield { type: "thinking_delta", content: text };
4503
+ }
4504
+ } else if (delta.type === "thought_signature" && step.type === "thought" && typeof delta.signature === "string") {
4505
+ step.signature = delta.signature;
4506
+ } else if (delta.type === "arguments_delta" && step.type === "function_call" && typeof delta.arguments === "string") {
4507
+ argumentBuffers.set(index, (argumentBuffers.get(index) ?? "") + delta.arguments);
4508
+ if (step.id)
4509
+ yield { type: "tool_call_delta", id: step.id, arguments: delta.arguments };
4510
+ }
4511
+ } else if (event.event_type === "step.stop") {
4512
+ const index = event.index;
4513
+ const step = index === void 0 ? void 0 : steps.get(index);
4514
+ if (step === void 0 || stopped.has(index)) {
4515
+ throw new Error("Gemini Interactions stream contained an invalid step.stop index");
4516
+ }
4517
+ if (step.type === "function_call") {
4518
+ const rawArguments = argumentBuffers.get(index) ?? "";
4519
+ if (rawArguments) {
4520
+ try {
4521
+ const parsed = JSON.parse(rawArguments);
4522
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
4523
+ throw new Error("not an object");
4524
+ }
4525
+ step.arguments = parsed;
4526
+ } catch {
4527
+ throw new Error(
4528
+ "Gemini Interactions function_call arguments were not valid JSON"
4529
+ );
4530
+ }
4531
+ }
4532
+ }
4533
+ stopped.add(index);
4534
+ } else if (event.event_type === "interaction.completed") {
4535
+ if (steps.size !== stopped.size) {
4536
+ throw new Error("Gemini Interactions stream completed before all steps stopped");
4537
+ }
4538
+ const toolCallCount = [...steps.values()].filter(
4539
+ (step) => step.type === "function_call" && !!step.id && !!step.name
4540
+ ).length;
4541
+ this.assertInteractionTerminalStatus(event.interaction?.status, toolCallCount);
4542
+ completed = true;
4543
+ yield {
4544
+ type: "done",
4545
+ usage: normalizeInteractionUsage(event.interaction?.usage),
4546
+ providerMetadata: steps.size ? {
4547
+ geminiInteractionSteps: [...steps.entries()].sort(([a], [b]) => a - b).map(([, step]) => step)
4548
+ } : void 0
4549
+ };
4550
+ return;
4551
+ }
4552
+ }
4553
+ }
4554
+ if (!completed)
4555
+ throw new Error("Gemini Interactions stream ended before interaction.completed");
4556
+ } finally {
4557
+ reader.releaseLock();
4558
+ }
4559
+ }
4560
+ assertInteractionTerminalStatus(status, toolCallCount) {
4561
+ if (status === "completed") return;
4562
+ if (status === "requires_action" && toolCallCount > 0) return;
4563
+ const normalizedStatus = typeof status === "string" ? status : "unknown";
4564
+ throw new ProviderError({
4565
+ provider: this.name,
4566
+ status: 0,
4567
+ retryable: false,
4568
+ message: `Gemini Interactions ended with non-success status: ${normalizedStatus}`
4569
+ });
4570
+ }
4571
+ // ---------------------------------------------------------------------------
4572
+ // Internal: request building
4573
+ // ---------------------------------------------------------------------------
4574
+ buildHeaders(apiKey) {
4575
+ return {
4576
+ "Content-Type": "application/json",
4577
+ "x-goog-api-key": apiKey
4578
+ };
4579
+ }
4580
+ extractErrorMessage(body, status) {
4581
+ try {
4582
+ const json = JSON.parse(body);
4583
+ if (json.error?.message) {
4584
+ return `Gemini API error (${status}): ${json.error.message}`;
4585
+ }
4586
+ } catch {
4587
+ }
4588
+ return `Gemini API error (${status}): ${body}`;
4589
+ }
4590
+ pricingContext(fallbackModel, body) {
4591
+ return {
4592
+ // Gemini's model is normally selected in the URL. providerOptions is an
4593
+ // explicit raw escape hatch, so honor a post-merge model override for both
4594
+ // the URL and pricing rather than pricing a different request.
4595
+ model: typeof body.model === "string" ? body.model : fallbackModel,
4596
+ serviceTier: body.serviceTier,
4597
+ eligibleRequest: this.baseUrl === "https://generativelanguage.googleapis.com/v1beta" && this.isTextOnlyClientRequest(body)
4598
+ };
4599
+ }
4600
+ requestModel(options) {
4601
+ return typeof options.providerOptions?.model === "string" ? options.providerOptions.model : options.model;
4602
+ }
4603
+ assertSafeGemini3Continuation(messages, model) {
4604
+ if (GEMINI_MODELS_WITHOUT_PORTABLE_TEMPERATURE.has(model)) {
4605
+ const mapped = this.mapMessages(messages.filter((message) => message.role !== "system"));
4606
+ if (mapped.at(-1)?.role === "model") {
4607
+ throw new Error(
4608
+ `${model} does not support a terminal assistant/model prefill; end the request with a user or tool message.`
4609
+ );
4610
+ }
4611
+ }
4612
+ if (!isGemini3x(model)) return;
4613
+ const replayedNativeCallIds = /* @__PURE__ */ new Set();
4614
+ for (const message of messages) {
4615
+ if (message.role !== "assistant" || !Array.isArray(message.providerMetadata?.geminiParts)) {
4616
+ continue;
4617
+ }
4618
+ for (const part of message.providerMetadata.geminiParts) {
4619
+ const id = part !== null && typeof part === "object" ? part.functionCall?.id : void 0;
4620
+ if (!id) continue;
4621
+ if (replayedNativeCallIds.has(id)) {
4622
+ throw new Error(
4623
+ `${model} native functionCall ids must be globally unique across a replay.`
4624
+ );
4625
+ }
4626
+ replayedNativeCallIds.add(id);
4627
+ }
4628
+ }
4629
+ const consumedToolMessages = /* @__PURE__ */ new Set();
4630
+ for (const [assistantIndex, message] of messages.entries()) {
4631
+ if (message.role !== "assistant" || !message.tool_calls?.length) continue;
4632
+ const rawParts = message.providerMetadata?.geminiParts;
4633
+ if (!Array.isArray(rawParts)) {
4634
+ throw new Error(
4635
+ `${model} tool continuations require providerMetadata.geminiParts with thought signatures.`
4636
+ );
4637
+ }
4638
+ const nativeCalls = rawParts.flatMap((part) => {
4639
+ const functionCall = part !== null && typeof part === "object" ? part.functionCall : void 0;
4640
+ return functionCall?.id ? [{ id: functionCall.id, name: functionCall.name, part }] : [];
4641
+ });
4642
+ const expectedCalls = new Map(
4643
+ message.tool_calls.map((call) => [call.id, call.function.name])
4644
+ );
4645
+ if (expectedCalls.size !== message.tool_calls.length || nativeCalls.length !== message.tool_calls.length || new Set(nativeCalls.map((call) => call.id)).size !== nativeCalls.length || typeof nativeCalls[0]?.part.thoughtSignature !== "string" || nativeCalls[0].part.thoughtSignature.length === 0 || nativeCalls.some((call) => expectedCalls.get(call.id) !== call.name)) {
4646
+ throw new Error(
4647
+ `${model} tool continuations require exact native functionCall id/name and a thoughtSignature on the first function call.`
3861
4648
  );
3862
4649
  }
3863
4650
  const seenResponses = /* @__PURE__ */ new Set();
@@ -4036,14 +4823,15 @@ var GeminiProvider = class {
4036
4823
  }
4037
4824
  const result = [];
4038
4825
  for (const msg of messages) {
4826
+ const text = typeof msg.content === "string" ? msg.content : "";
4039
4827
  if (msg.role === "assistant") {
4040
4828
  const rawParts = msg.providerMetadata?.geminiParts;
4041
4829
  if (rawParts && rawParts.length > 0) {
4042
4830
  result.push({ role: "model", parts: rawParts });
4043
4831
  } else {
4044
4832
  const parts = [];
4045
- if (msg.content) {
4046
- parts.push({ text: msg.content });
4833
+ if (text) {
4834
+ parts.push({ text });
4047
4835
  }
4048
4836
  if (msg.tool_calls && msg.tool_calls.length > 0) {
4049
4837
  for (const tc of msg.tool_calls) {
@@ -4069,7 +4857,7 @@ var GeminiProvider = class {
4069
4857
  const functionName = (msg.tool_call_id ? geminiNativeNames.get(msg.tool_call_id) : void 0) ?? toolCallIdToName.get(msg.tool_call_id) ?? "unknown";
4070
4858
  const functionResponse = {
4071
4859
  name: functionName,
4072
- response: parseGeminiFunctionResponse(msg.content)
4860
+ response: parseGeminiFunctionResponse(text)
4073
4861
  };
4074
4862
  if (msg.tool_call_id && geminiNativeIds.has(msg.tool_call_id)) {
4075
4863
  functionResponse.id = msg.tool_call_id;
@@ -4079,7 +4867,7 @@ var GeminiProvider = class {
4079
4867
  parts: [{ functionResponse }]
4080
4868
  });
4081
4869
  } else if (msg.role === "user") {
4082
- result.push({ role: "user", parts: [{ text: msg.content }] });
4870
+ result.push({ role: "user", parts: [{ text }] });
4083
4871
  }
4084
4872
  }
4085
4873
  return this.mergeConsecutiveRoles(result);
@@ -4267,6 +5055,56 @@ var GeminiProvider = class {
4267
5055
  }
4268
5056
  }
4269
5057
  };
5058
+ function isGeminiInteractionStep(value) {
5059
+ return value !== null && typeof value === "object" && ["user_input", "model_output", "thought", "function_call", "function_result"].includes(
5060
+ value.type
5061
+ );
5062
+ }
5063
+ function interactionText(content) {
5064
+ return content?.map((part) => part.type === "text" && typeof part.text === "string" ? part.text : "").join("") ?? "";
5065
+ }
5066
+ function interactionTextChunks(content) {
5067
+ return content?.flatMap(
5068
+ (part) => part.type === "text" && typeof part.text === "string" ? [part.text] : []
5069
+ ) ?? [];
5070
+ }
5071
+ function cloneInteractionStep(step) {
5072
+ return JSON.parse(JSON.stringify(step));
5073
+ }
5074
+ function appendInteractionContent(step, field, content) {
5075
+ const existing = step[field] ?? [];
5076
+ const last = existing.at(-1);
5077
+ if (content.type === "text" && last?.type === "text" && typeof content.text === "string" && typeof last.text === "string") {
5078
+ last.text += content.text;
5079
+ } else {
5080
+ existing.push(content);
5081
+ }
5082
+ step[field] = existing;
5083
+ }
5084
+ function safeJsonObject(value) {
5085
+ try {
5086
+ const parsed = JSON.parse(value);
5087
+ return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
5088
+ } catch {
5089
+ return {};
5090
+ }
5091
+ }
5092
+ function normalizeInteractionUsage(usage) {
5093
+ if (!usage || !isValidTokenCount2(usage.total_input_tokens) || !isValidTokenCount2(usage.total_output_tokens) || !isValidTokenCount2(usage.total_tokens)) {
5094
+ return void 0;
5095
+ }
5096
+ const cached = usage.total_cached_tokens;
5097
+ const thinking = usage.total_thought_tokens;
5098
+ if (cached !== void 0 && (!isValidTokenCount2(cached) || cached > usage.total_input_tokens) || thinking !== void 0 && !isValidTokenCount2(thinking))
5099
+ return void 0;
5100
+ return {
5101
+ prompt_tokens: usage.total_input_tokens,
5102
+ completion_tokens: usage.total_output_tokens,
5103
+ total_tokens: usage.total_tokens,
5104
+ ...cached ? { cached_tokens: cached } : {},
5105
+ ...thinking ? { reasoning_tokens: thinking } : {}
5106
+ };
5107
+ }
4270
5108
 
4271
5109
  // src/providers/profiles/openrouter.ts
4272
5110
  var OPENROUTER_PROFILE = {
@@ -4698,68 +5536,1225 @@ var ProviderRegistry = class {
4698
5536
  if (cached) return cached;
4699
5537
  const factory = this.factories.get(name);
4700
5538
  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(", ")}`);
5539
+ const instance = factory(config);
5540
+ this.instances.set(name, instance);
5541
+ return instance;
5542
+ }
5543
+ if (this.fallbackInstance) {
5544
+ return this.fallbackInstance;
5545
+ }
5546
+ throw new Error(`Unknown provider "${name}". Registered providers: ${this.list().join(", ")}`);
5547
+ }
5548
+ /**
5549
+ * Resolve a "provider:model" URI string into a Provider instance and model name.
5550
+ *
5551
+ * Supported formats:
5552
+ * - "openai:gpt-4o" -> provider=openai, model=gpt-4o
5553
+ * - "anthropic:claude-3" -> provider=anthropic, model=claude-3
5554
+ * - "gpt-4o" -> uses defaultProvider from config, model=gpt-4o
5555
+ * - undefined / empty -> uses defaultProvider and defaultModel from config
5556
+ *
5557
+ * @param uri Provider:model string, or just a model name
5558
+ * @param config Axl configuration for provider options and defaults
5559
+ */
5560
+ resolve(uri, config = {}) {
5561
+ if (!uri) {
5562
+ const providerName2 = config.defaultProvider ?? "openai";
5563
+ const model2 = config.defaultModel ?? "gpt-4o";
5564
+ return { provider: this.get(providerName2, config), model: model2 };
5565
+ }
5566
+ const colonIndex = uri.indexOf(":");
5567
+ if (colonIndex === -1) {
5568
+ const providerName2 = config.defaultProvider ?? "openai";
5569
+ return { provider: this.get(providerName2, config), model: uri };
5570
+ }
5571
+ const providerName = uri.slice(0, colonIndex);
5572
+ const model = uri.slice(colonIndex + 1);
5573
+ if (!providerName || !model) {
5574
+ throw new Error(
5575
+ `Invalid provider URI "${uri}". Expected format: "provider:model" (e.g. "openai:gpt-4o")`
5576
+ );
5577
+ }
5578
+ return { provider: this.get(providerName, config), model };
5579
+ }
5580
+ /**
5581
+ * Clear all cached provider instances. Useful for testing or reconfiguration.
5582
+ */
5583
+ clearCache() {
5584
+ this.instances.clear();
5585
+ }
5586
+ /**
5587
+ * Clear all registered factories (including built-ins).
5588
+ * Useful for test runtimes where only explicitly registered instances should be used.
5589
+ */
5590
+ clearFactories() {
5591
+ this.factories.clear();
5592
+ }
5593
+ };
5594
+ var defaultRegistry = new ProviderRegistry();
5595
+
5596
+ // src/transcription.ts
5597
+ var MAX_INLINE_TRANSCRIPTION_BYTES = 25 * 1024 * 1024;
5598
+ var RESERVED_PROVIDER_OPTION_KEYS = /* @__PURE__ */ new Set([
5599
+ "model",
5600
+ "audio",
5601
+ "signal",
5602
+ "cleanup",
5603
+ "cleanupStatus",
5604
+ "usage",
5605
+ "cost",
5606
+ "pricingStatus",
5607
+ "accounting"
5608
+ ]);
5609
+ function nonEmptyString(value) {
5610
+ return typeof value === "string" && value.trim().length > 0;
5611
+ }
5612
+ function cloneValue(value) {
5613
+ try {
5614
+ return structuredClone(value);
5615
+ } catch {
5616
+ throw new InvalidTranscriptionInputError("Transcription options must be structured-cloneable");
5617
+ }
5618
+ }
5619
+ function normalizeTranscriptionRequest(request) {
5620
+ if (!request || typeof request !== "object" || !nonEmptyString(request.model)) {
5621
+ throw new InvalidTranscriptionInputError("A transcription model URI is required");
5622
+ }
5623
+ const audio = request.audio;
5624
+ if (!audio || typeof audio !== "object") {
5625
+ throw new InvalidTranscriptionInputError("A transcription audio source is required");
5626
+ }
5627
+ let normalizedAudio;
5628
+ if (audio.type === "bytes") {
5629
+ if (!(audio.data instanceof Uint8Array) || audio.data.byteLength === 0 || !nonEmptyString(audio.mediaType)) {
5630
+ throw new InvalidTranscriptionInputError(
5631
+ "Inline transcription bytes require nonempty data and mediaType"
5632
+ );
5633
+ }
5634
+ if (audio.data.byteLength > MAX_INLINE_TRANSCRIPTION_BYTES) {
5635
+ throw new InvalidTranscriptionInputError(
5636
+ "Inline transcription audio must not exceed 25 MiB; chunk it or use a supported provider-file source"
5637
+ );
5638
+ }
5639
+ normalizedAudio = {
5640
+ type: "bytes",
5641
+ data: new Uint8Array(audio.data),
5642
+ mediaType: audio.mediaType
5643
+ };
5644
+ } else if (audio.type === "base64") {
5645
+ if (!nonEmptyString(audio.data) || !nonEmptyString(audio.mediaType)) {
5646
+ throw new InvalidTranscriptionInputError(
5647
+ "Inline base64 transcription audio requires data and mediaType"
5648
+ );
5649
+ }
5650
+ if (audio.data.length > 4 * Math.ceil(MAX_INLINE_TRANSCRIPTION_BYTES / 3)) {
5651
+ throw new InvalidTranscriptionInputError(
5652
+ "Inline transcription audio must not exceed 25 MiB; chunk it or use a supported provider-file source"
5653
+ );
5654
+ }
5655
+ let decoded;
5656
+ try {
5657
+ decoded = Uint8Array.from(Buffer.from(audio.data, "base64"));
5658
+ } catch {
5659
+ throw new InvalidTranscriptionInputError(
5660
+ "Transcription base64 must be canonical and decode to audio"
5661
+ );
5662
+ }
5663
+ if (!decoded.byteLength || Buffer.from(decoded).toString("base64") !== audio.data) {
5664
+ throw new InvalidTranscriptionInputError(
5665
+ "Transcription base64 must be canonical and decode to nonempty audio"
5666
+ );
5667
+ }
5668
+ if (decoded.byteLength > MAX_INLINE_TRANSCRIPTION_BYTES) {
5669
+ throw new InvalidTranscriptionInputError(
5670
+ "Inline transcription audio must not exceed 25 MiB; chunk it or use a supported provider-file source"
5671
+ );
5672
+ }
5673
+ normalizedAudio = { type: "base64", data: audio.data, mediaType: audio.mediaType };
5674
+ } else if (audio.type === "provider-file") {
5675
+ if (!nonEmptyString(audio.provider) || !nonEmptyString(audio.reference)) {
5676
+ throw new InvalidTranscriptionInputError(
5677
+ "A provider-file transcription source requires provider and reference"
5678
+ );
5679
+ }
5680
+ if (audio.mediaType !== void 0 && !nonEmptyString(audio.mediaType)) {
5681
+ throw new InvalidTranscriptionInputError(
5682
+ "Transcription provider-file mediaType must be nonempty when supplied"
5683
+ );
5684
+ }
5685
+ normalizedAudio = {
5686
+ type: "provider-file",
5687
+ provider: audio.provider,
5688
+ reference: audio.reference,
5689
+ ...audio.mediaType !== void 0 ? { mediaType: audio.mediaType } : {}
5690
+ };
5691
+ } else {
5692
+ throw new InvalidTranscriptionInputError(
5693
+ "Transcription audio must be bytes, base64, or provider-file"
5694
+ );
5695
+ }
5696
+ if (request.language !== void 0 && !nonEmptyString(request.language)) {
5697
+ throw new InvalidTranscriptionInputError(
5698
+ "Transcription language must be nonempty when supplied"
5699
+ );
5700
+ }
5701
+ if (request.timestamps !== void 0 && request.timestamps !== "segment" && request.timestamps !== "word") {
5702
+ throw new InvalidTranscriptionInputError("Transcription timestamps must be segment or word");
5703
+ }
5704
+ if (request.diarization !== void 0 && typeof request.diarization !== "boolean") {
5705
+ throw new InvalidTranscriptionInputError("Transcription diarization must be boolean");
5706
+ }
5707
+ if (request.providerOptions !== void 0) {
5708
+ if (!request.providerOptions || Array.isArray(request.providerOptions) || typeof request.providerOptions !== "object") {
5709
+ throw new InvalidTranscriptionInputError("Transcription providerOptions must be an object");
5710
+ }
5711
+ for (const key of Object.keys(request.providerOptions)) {
5712
+ if (RESERVED_PROVIDER_OPTION_KEYS.has(key)) {
5713
+ throw new InvalidTranscriptionInputError(
5714
+ "Transcription providerOptions cannot override reserved operation fields"
5715
+ );
5716
+ }
5717
+ }
5718
+ }
5719
+ return {
5720
+ model: request.model,
5721
+ audio: normalizedAudio,
5722
+ ...request.language !== void 0 ? { language: request.language } : {},
5723
+ ...request.timestamps !== void 0 ? { timestamps: request.timestamps } : {},
5724
+ ...request.diarization !== void 0 ? { diarization: request.diarization } : {},
5725
+ ...request.providerOptions !== void 0 ? { providerOptions: cloneValue(request.providerOptions) } : {}
5726
+ };
5727
+ }
5728
+ function cloneTranscript(transcript) {
5729
+ if (!transcript || typeof transcript !== "object" || typeof transcript.text !== "string") {
5730
+ throw new InvalidTranscriptionInputError(
5731
+ "Transcription provider returned an invalid transcript"
5732
+ );
5733
+ }
5734
+ const cloned = cloneValue(transcript);
5735
+ const accounting = normalizeTranscriptionAccounting(cloned.usage, cloned.pricingStatus);
5736
+ return {
5737
+ text: cloned.text,
5738
+ ...cloned.detectedLanguages ? { detectedLanguages: cloned.detectedLanguages } : {},
5739
+ ...cloned.segments ? { segments: cloned.segments } : {},
5740
+ ...cloned.words ? { words: cloned.words } : {},
5741
+ ...accounting.usage ? { usage: accounting.usage } : {},
5742
+ ...accounting.pricingStatus ? { pricingStatus: accounting.pricingStatus } : {},
5743
+ ...cloned.providerMetadata ? { providerMetadata: cloned.providerMetadata } : {}
5744
+ };
5745
+ }
5746
+ function normalizeTranscriptionAccounting(usageValue, pricingValue) {
5747
+ const value = usageValue && typeof usageValue === "object" ? usageValue : void 0;
5748
+ const number = (key) => {
5749
+ const candidate = value?.[key];
5750
+ return typeof candidate === "number" && Number.isFinite(candidate) && candidate >= 0 ? candidate : void 0;
5751
+ };
5752
+ const audioSeconds = number("audioSeconds");
5753
+ const inputTokens = number("inputTokens");
5754
+ const outputTokens = number("outputTokens");
5755
+ const totalTokens = number("totalTokens");
5756
+ const reportedCost = number("cost");
5757
+ const usage = value && (audioSeconds !== void 0 || inputTokens !== void 0 || outputTokens !== void 0 || totalTokens !== void 0 || reportedCost !== void 0) ? {
5758
+ ...audioSeconds !== void 0 ? { audioSeconds } : {},
5759
+ ...inputTokens !== void 0 ? { inputTokens } : {},
5760
+ ...outputTokens !== void 0 ? { outputTokens } : {},
5761
+ ...totalTokens !== void 0 ? { totalTokens } : {},
5762
+ ...reportedCost !== void 0 ? { cost: reportedCost } : {}
5763
+ } : void 0;
5764
+ const hasWork = (audioSeconds ?? 0) > 0 || (inputTokens ?? 0) > 0 || (outputTokens ?? 0) > 0 || (totalTokens ?? 0) > 0;
5765
+ const requested = pricingValue === "priced" || pricingValue === "unpriced" || pricingValue === "zero" ? pricingValue : void 0;
5766
+ const cost = requested === "zero" && (reportedCost === void 0 || reportedCost === 0) ? 0 : reportedCost;
5767
+ 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;
5768
+ const tokens = inputTokens !== void 0 || outputTokens !== void 0 ? {
5769
+ ...inputTokens !== void 0 ? { input: inputTokens } : {},
5770
+ ...outputTokens !== void 0 ? { output: outputTokens } : {}
5771
+ } : void 0;
5772
+ return {
5773
+ ...usage ? { usage } : {},
5774
+ ...pricingStatus ? { pricingStatus } : {},
5775
+ ...cost !== void 0 ? { cost } : {},
5776
+ ...tokens ? { tokens } : {},
5777
+ hasWork
5778
+ };
5779
+ }
5780
+
5781
+ // src/providers/transcription-utils.ts
5782
+ var OPENAI_TRANSCRIPTION_MODEL = "gpt-transcribe";
5783
+ var GEMINI_TRANSCRIPTION_MODEL = "gemini-3.5-transcribe";
5784
+ function requireExactTranscriptionModel(provider, model, expected) {
5785
+ if (model !== expected) {
5786
+ throw new UnsupportedTranscriptionInputError({
5787
+ provider,
5788
+ model,
5789
+ feature: "this transcription model"
5790
+ });
5791
+ }
5792
+ }
5793
+ function inlineAudioBytes(audio) {
5794
+ if (audio.type === "bytes") return new Uint8Array(audio.data);
5795
+ if (audio.type === "base64") return Uint8Array.from(Buffer.from(audio.data, "base64"));
5796
+ throw new UnsupportedTranscriptionInputError({
5797
+ provider: audio.provider,
5798
+ model: "",
5799
+ feature: "provider-file transcription input"
5800
+ });
5801
+ }
5802
+ function requireNoProviderFile(audio, provider, model) {
5803
+ if (audio.type === "provider-file") {
5804
+ throw new UnsupportedTranscriptionInputError({
5805
+ provider,
5806
+ model,
5807
+ feature: "provider-file transcription input"
5808
+ });
5809
+ }
5810
+ }
5811
+ function assertOnlyProviderOptions(request, provider, model, allowed) {
5812
+ const options = request.providerOptions ?? {};
5813
+ for (const key of Object.keys(options)) {
5814
+ if (!allowed.includes(key)) {
5815
+ throw new InvalidTranscriptionInputError(
5816
+ `Unsupported ${provider} transcription provider option for model '${model}'`
5817
+ );
5818
+ }
5819
+ }
5820
+ return options;
5821
+ }
5822
+ function transcriptUsage(value) {
5823
+ const source = value && typeof value === "object" ? value : {};
5824
+ const number = (...keys) => {
5825
+ for (const key of keys) {
5826
+ const candidate = source[key];
5827
+ if (typeof candidate === "number" && Number.isFinite(candidate) && candidate >= 0)
5828
+ return candidate;
5829
+ }
5830
+ return void 0;
5831
+ };
5832
+ const inputTokens = number("total_input_tokens", "input_tokens", "prompt_tokens", "inputTokens");
5833
+ const outputTokens = number(
5834
+ "total_output_tokens",
5835
+ "output_tokens",
5836
+ "completion_tokens",
5837
+ "outputTokens"
5838
+ );
5839
+ const totalTokens = number("total_tokens", "totalTokens");
5840
+ const audioSeconds = number("seconds", "audio_seconds", "audioSeconds", "duration_seconds");
5841
+ const cost = number("cost");
5842
+ const usage = inputTokens !== void 0 || outputTokens !== void 0 || totalTokens !== void 0 || audioSeconds !== void 0 || cost !== void 0 ? {
5843
+ ...audioSeconds !== void 0 ? { audioSeconds } : {},
5844
+ ...inputTokens !== void 0 ? { inputTokens } : {},
5845
+ ...outputTokens !== void 0 ? { outputTokens } : {},
5846
+ ...totalTokens !== void 0 ? { totalTokens } : {},
5847
+ ...cost !== void 0 ? { cost } : {}
5848
+ } : void 0;
5849
+ const hasWork = (audioSeconds ?? 0) > 0 || (inputTokens ?? 0) > 0 || (outputTokens ?? 0) > 0 || (totalTokens ?? 0) > 0;
5850
+ return {
5851
+ ...usage ? { usage } : {},
5852
+ ...cost !== void 0 ? { pricingStatus: "priced" } : hasWork ? { pricingStatus: "unpriced" } : {}
5853
+ };
5854
+ }
5855
+ function safeProviderMessage(provider, status, body) {
5856
+ try {
5857
+ const parsed = JSON.parse(body);
5858
+ const message = parsed.error?.message ?? parsed.message;
5859
+ if (typeof message === "string")
5860
+ return `${provider} transcription API error (${status}): ${message}`;
5861
+ } catch {
5862
+ }
5863
+ return `${provider} transcription API error (${status})`;
5864
+ }
5865
+ function attachTranscriptionFailureMetadata(error, metadata) {
5866
+ for (const [key, value] of Object.entries(metadata)) {
5867
+ if (value !== void 0) Object.defineProperty(error, key, { enumerable: true, value });
5868
+ }
5869
+ return error;
5870
+ }
5871
+
5872
+ // src/providers/openai-transcription.ts
5873
+ var OPENAI_FILE_EXTENSIONS = {
5874
+ "audio/flac": "flac",
5875
+ "audio/m4a": "m4a",
5876
+ "audio/mp3": "mp3",
5877
+ "audio/mp4": "mp4",
5878
+ "audio/mpeg": "mp3",
5879
+ "audio/mpga": "mpga",
5880
+ "audio/ogg": "ogg",
5881
+ "audio/wav": "wav",
5882
+ "audio/webm": "webm"
5883
+ };
5884
+ var OpenAITranscriptionProvider = class {
5885
+ name = "openai-transcription";
5886
+ baseUrl;
5887
+ apiKeySource;
5888
+ governor;
5889
+ constructor(options = {}) {
5890
+ this.apiKeySource = options.apiKey ?? process.env.OPENAI_API_KEY ?? "";
5891
+ this.baseUrl = (options.baseUrl ?? process.env.OPENAI_BASE_URL ?? "https://api.openai.com/v1").replace(/\/$/, "");
5892
+ assertSafeProviderBaseUrl(
5893
+ this.baseUrl,
5894
+ "OpenAI transcription provider",
5895
+ options.dangerouslyAllowInsecureHttp
5896
+ );
5897
+ this.governor = options.rateLimit ? new RateLimiter(options.rateLimit) : void 0;
5898
+ }
5899
+ capabilities(model) {
5900
+ return model === OPENAI_TRANSCRIPTION_MODEL ? { sources: ["bytes", "base64"] } : void 0;
5901
+ }
5902
+ async transcribe(request) {
5903
+ requireExactTranscriptionModel(this.name, request.model, OPENAI_TRANSCRIPTION_MODEL);
5904
+ requireNoProviderFile(request.audio, this.name, request.model);
5905
+ if (request.audio.type === "provider-file")
5906
+ throw new UnsupportedTranscriptionInputError({
5907
+ provider: this.name,
5908
+ model: request.model,
5909
+ feature: "provider-file transcription input"
5910
+ });
5911
+ const options = assertOnlyProviderOptions(request, this.name, request.model, [
5912
+ "temperature",
5913
+ "prompt"
5914
+ ]);
5915
+ if (options.temperature !== void 0 && (typeof options.temperature !== "number" || !Number.isFinite(options.temperature) || options.temperature < 0 || options.temperature > 1))
5916
+ throw new InvalidTranscriptionInputError(
5917
+ "OpenAI transcription temperature must be a number between 0 and 1"
5918
+ );
5919
+ if (options.prompt !== void 0 && typeof options.prompt !== "string")
5920
+ throw new InvalidTranscriptionInputError("OpenAI transcription prompt must be a string");
5921
+ const mediaType2 = request.audio.mediaType;
5922
+ const extension = OPENAI_FILE_EXTENSIONS[mediaType2];
5923
+ if (!extension)
5924
+ throw new UnsupportedTranscriptionInputError({
5925
+ provider: this.name,
5926
+ model: request.model,
5927
+ feature: "this audio media type"
5928
+ });
5929
+ const bytes = inlineAudioBytes(request.audio);
5930
+ if (bytes.byteLength > MAX_INLINE_TRANSCRIPTION_BYTES)
5931
+ throw new InvalidTranscriptionInputError("OpenAI transcription audio must not exceed 25 MiB");
5932
+ const form = new FormData();
5933
+ form.set("file", new Blob([Buffer.from(bytes)], { type: mediaType2 }), `audio.${extension}`);
5934
+ form.set("model", request.model);
5935
+ form.set("response_format", "json");
5936
+ if (request.language) form.set("language", request.language);
5937
+ if (options.temperature !== void 0) form.set("temperature", String(options.temperature));
5938
+ if (options.prompt !== void 0) form.set("prompt", options.prompt);
5939
+ const key = await this.resolveKey();
5940
+ const res = await fetchWithRetry(
5941
+ `${this.baseUrl}/audio/transcriptions`,
5942
+ {
5943
+ method: "POST",
5944
+ headers: { Authorization: `Bearer ${key}` },
5945
+ body: form,
5946
+ signal: request.signal
5947
+ },
5948
+ { governor: this.governor, provider: this.name }
5949
+ );
5950
+ if (!res.ok) {
5951
+ const body = await res.text();
5952
+ throw buildProviderError({
5953
+ provider: this.name,
5954
+ status: res.status,
5955
+ headers: res.headers,
5956
+ message: safeProviderMessage("OpenAI", res.status, body),
5957
+ body
5958
+ });
5959
+ }
5960
+ const json = await res.json();
5961
+ if (typeof json.text !== "string")
5962
+ throw new InvalidTranscriptionInputError(
5963
+ "OpenAI transcription response did not contain text"
5964
+ );
5965
+ return {
5966
+ transcript: { text: json.text, ...transcriptUsage(json.usage) },
5967
+ cleanupStatus: "not_required"
5968
+ };
5969
+ }
5970
+ async resolveKey() {
5971
+ const key = await resolveApiKey(this.apiKeySource);
5972
+ if (!key)
5973
+ throw new Error("OpenAI API key is required. Set OPENAI_API_KEY or pass apiKey in options.");
5974
+ return key;
5975
+ }
5976
+ };
5977
+
5978
+ // src/providers/gemini-transcription.ts
5979
+ var CLEANUP_TIMEOUT_MS = 3e3;
5980
+ var READY_TIMEOUT_MS = 3e4;
5981
+ var READY_POLL_MS = 250;
5982
+ var GEMINI_AUDIO_MEDIA_TYPES = /* @__PURE__ */ new Set([
5983
+ "audio/wav",
5984
+ "audio/mp3",
5985
+ "audio/aiff",
5986
+ "audio/aac",
5987
+ "audio/ogg",
5988
+ "audio/flac",
5989
+ "audio/mpeg",
5990
+ "audio/m4a",
5991
+ "audio/l16",
5992
+ "audio/opus",
5993
+ "audio/alaw",
5994
+ "audio/mulaw",
5995
+ "audio/webm"
5996
+ ]);
5997
+ function abortError2() {
5998
+ return new DOMException("The transcription operation was aborted", "AbortError");
5999
+ }
6000
+ function audioMime(audio) {
6001
+ if (!audio.mediaType)
6002
+ throw new UnsupportedTranscriptionInputError({
6003
+ provider: "gemini-transcription",
6004
+ model: GEMINI_TRANSCRIPTION_MODEL,
6005
+ feature: "provider-file audio mediaType"
6006
+ });
6007
+ return audio.mediaType;
6008
+ }
6009
+ function assertGeminiAudioMime(mediaType2) {
6010
+ if (!mediaType2 || !GEMINI_AUDIO_MEDIA_TYPES.has(mediaType2)) {
6011
+ throw new UnsupportedTranscriptionInputError({
6012
+ provider: "gemini-transcription",
6013
+ model: GEMINI_TRANSCRIPTION_MODEL,
6014
+ feature: "this audio media type"
6015
+ });
6016
+ }
6017
+ return mediaType2;
6018
+ }
6019
+ function secondsOffset(value) {
6020
+ if (typeof value !== "string") return void 0;
6021
+ const match = /^(\d+(?:\.\d+)?)s$/.exec(value);
6022
+ if (!match) return void 0;
6023
+ const seconds = Number(match[1]);
6024
+ return Number.isFinite(seconds) && seconds >= 0 ? seconds : void 0;
6025
+ }
6026
+ function sleep3(ms, signal) {
6027
+ return new Promise((resolve2, reject) => {
6028
+ if (signal?.aborted) {
6029
+ reject(abortError2());
6030
+ return;
6031
+ }
6032
+ const timer = setTimeout(done, ms);
6033
+ const onAbort = () => {
6034
+ clearTimeout(timer);
6035
+ done(abortError2());
6036
+ };
6037
+ function done(error) {
6038
+ signal?.removeEventListener("abort", onAbort);
6039
+ if (error) reject(error);
6040
+ else resolve2();
6041
+ }
6042
+ signal?.addEventListener("abort", onAbort, { once: true });
6043
+ });
6044
+ }
6045
+ var GeminiTranscriptionProvider = class {
6046
+ name = "gemini-transcription";
6047
+ baseUrl;
6048
+ apiKeySource;
6049
+ governor;
6050
+ constructor(options = {}) {
6051
+ this.apiKeySource = options.apiKey ?? process.env.GOOGLE_API_KEY ?? process.env.GEMINI_API_KEY ?? "";
6052
+ this.baseUrl = (options.baseUrl ?? "https://generativelanguage.googleapis.com/v1beta").replace(
6053
+ /\/$/,
6054
+ ""
6055
+ );
6056
+ assertSafeProviderBaseUrl(
6057
+ this.baseUrl,
6058
+ "Gemini transcription provider",
6059
+ options.dangerouslyAllowInsecureHttp
6060
+ );
6061
+ this.governor = options.rateLimit ? new RateLimiter(options.rateLimit) : void 0;
6062
+ }
6063
+ capabilities(model) {
6064
+ return model === GEMINI_TRANSCRIPTION_MODEL ? { sources: ["bytes", "base64", "provider-file"], timestamps: ["word"], diarization: true } : void 0;
6065
+ }
6066
+ async transcribe(request) {
6067
+ requireExactTranscriptionModel(this.name, request.model, GEMINI_TRANSCRIPTION_MODEL);
6068
+ const options = assertOnlyProviderOptions(request, this.name, request.model, [
6069
+ "mode",
6070
+ "customVocabulary"
6071
+ ]);
6072
+ const mode = options.mode ?? "verbatim";
6073
+ if (mode !== "smart" && mode !== "verbatim")
6074
+ throw new InvalidTranscriptionInputError(
6075
+ "Gemini transcription mode must be smart or verbatim"
6076
+ );
6077
+ if (options.customVocabulary !== void 0 && (!Array.isArray(options.customVocabulary) || options.customVocabulary.length > 1e3 || options.customVocabulary.some(
6078
+ (value) => typeof value !== "string" || !value.trim() || value.length > 256
6079
+ )))
6080
+ throw new InvalidTranscriptionInputError(
6081
+ "Gemini transcription customVocabulary must contain at most 1000 nonempty strings up to 256 characters"
6082
+ );
6083
+ if (mode === "smart" && (request.timestamps || request.diarization))
6084
+ throw new InvalidTranscriptionInputError(
6085
+ "Gemini smart transcription does not support timestamps or diarization"
6086
+ );
6087
+ if (request.timestamps && options.customVocabulary !== void 0)
6088
+ throw new InvalidTranscriptionInputError(
6089
+ "Gemini transcription customVocabulary cannot be combined with timestamps"
6090
+ );
6091
+ if (request.timestamps && request.timestamps !== "word")
6092
+ throw new UnsupportedTranscriptionInputError({
6093
+ provider: this.name,
6094
+ model: request.model,
6095
+ feature: "segment timestamps"
6096
+ });
6097
+ if (request.diarization && request.timestamps !== "word")
6098
+ throw new InvalidTranscriptionInputError("Gemini diarization requires word timestamps");
6099
+ if (request.audio.type === "provider-file") {
6100
+ if (request.audio.provider !== this.name || typeof request.audio.reference !== "string" || !request.audio.reference.trim()) {
6101
+ throw new UnsupportedTranscriptionInputError({
6102
+ provider: this.name,
6103
+ model: request.model,
6104
+ feature: "a matching nonempty provider-file reference"
6105
+ });
6106
+ }
6107
+ assertGeminiAudioMime(request.audio.mediaType);
6108
+ } else {
6109
+ assertGeminiAudioMime(request.audio.mediaType);
6110
+ }
6111
+ const key = await this.resolveKey();
6112
+ if (request.audio.type === "provider-file")
6113
+ return this.transcribeReference(request, key, mode, options);
6114
+ return this.transcribeUpload(request, key, mode, options);
6115
+ }
6116
+ async transcribeReference(request, key, mode, options) {
6117
+ const audio = request.audio;
6118
+ if (audio.type !== "provider-file" || audio.provider !== this.name)
6119
+ throw new UnsupportedTranscriptionInputError({
6120
+ provider: this.name,
6121
+ model: request.model,
6122
+ feature: "a provider-file owned by another provider"
6123
+ });
6124
+ const mimeType = audioMime(audio);
6125
+ const transcript = await this.infer(request, key, audio.reference, mimeType, mode, options);
6126
+ return { transcript, cleanupStatus: "not_required" };
6127
+ }
6128
+ async transcribeUpload(request, key, mode, options) {
6129
+ let created = false;
6130
+ let fileName;
6131
+ let primaryError;
6132
+ let transcript;
6133
+ let cleanupStatus = "not_required";
6134
+ try {
6135
+ const uploaded = await this.upload(request, key);
6136
+ created = true;
6137
+ fileName = uploaded.name;
6138
+ const ready = await this.waitForReady(
6139
+ this.fileFromJson(uploaded.file, uploaded.mimeType),
6140
+ key,
6141
+ request.signal
6142
+ );
6143
+ transcript = await this.infer(request, key, ready.uri, ready.mimeType, mode, options);
6144
+ } catch (error) {
6145
+ primaryError = error;
6146
+ } finally {
6147
+ if (created && fileName) cleanupStatus = await this.cleanup(fileName, key);
6148
+ }
6149
+ if (primaryError) {
6150
+ throw attachTranscriptionFailureMetadata(
6151
+ primaryError instanceof Error ? primaryError : new Error("Gemini transcription failed"),
6152
+ { cleanupStatus }
6153
+ );
6154
+ }
6155
+ if (!transcript)
6156
+ throw new InvalidTranscriptionInputError(
6157
+ "Gemini transcription response did not contain text"
6158
+ );
6159
+ return { transcript, cleanupStatus };
6160
+ }
6161
+ /** A finalization response with a name creates a caller-owned cleanup obligation
6162
+ * even when its URI/state is malformed. `transcribeUpload` stores the name before
6163
+ * strictly parsing the rest of the file representation. */
6164
+ async upload(request, key) {
6165
+ if (request.audio.type === "provider-file")
6166
+ throw new UnsupportedTranscriptionInputError({
6167
+ provider: this.name,
6168
+ model: request.model,
6169
+ feature: "provider-file upload"
6170
+ });
6171
+ const mimeType = assertGeminiAudioMime(request.audio.mediaType);
6172
+ const bytes = inlineAudioBytes(request.audio);
6173
+ const start = await fetchWithRetry(
6174
+ `${this.baseUrl.replace(/\/v1beta$/, "")}/upload/v1beta/files`,
6175
+ {
6176
+ method: "POST",
6177
+ headers: {
6178
+ "x-goog-api-key": key,
6179
+ "X-Goog-Upload-Protocol": "resumable",
6180
+ "X-Goog-Upload-Command": "start",
6181
+ "X-Goog-Upload-Header-Content-Length": String(bytes.byteLength),
6182
+ "X-Goog-Upload-Header-Content-Type": mimeType,
6183
+ "Content-Type": "application/json"
6184
+ },
6185
+ body: JSON.stringify({ file: { display_name: "axl-transcription" } }),
6186
+ signal: request.signal
6187
+ },
6188
+ { governor: this.governor, provider: this.name, maxRetries: 0 }
6189
+ );
6190
+ if (!start.ok) {
6191
+ const body = await start.text();
6192
+ throw buildProviderError({
6193
+ provider: this.name,
6194
+ status: start.status,
6195
+ headers: start.headers,
6196
+ message: safeProviderMessage("Gemini", start.status, body),
6197
+ body
6198
+ });
6199
+ }
6200
+ const uploadUrl = start.headers.get("x-goog-upload-url");
6201
+ if (!uploadUrl)
6202
+ throw new InvalidTranscriptionInputError(
6203
+ "Gemini transcription upload did not return an upload URL"
6204
+ );
6205
+ let parsedUploadUrl;
6206
+ try {
6207
+ parsedUploadUrl = new URL(uploadUrl);
6208
+ } catch {
6209
+ throw new InvalidTranscriptionInputError(
6210
+ "Gemini transcription upload returned an invalid upload URL"
6211
+ );
6212
+ }
6213
+ const baseOrigin = new URL(this.baseUrl).origin;
6214
+ if (parsedUploadUrl.username || parsedUploadUrl.password || parsedUploadUrl.origin !== baseOrigin) {
6215
+ throw new InvalidTranscriptionInputError(
6216
+ "Gemini transcription upload returned an untrusted upload URL"
6217
+ );
6218
+ }
6219
+ assertSafeProviderBaseUrl(
6220
+ parsedUploadUrl.toString(),
6221
+ "Gemini transcription upload endpoint",
6222
+ new URL(this.baseUrl).protocol === "http:"
6223
+ );
6224
+ const finalize = await fetchWithRetry(
6225
+ parsedUploadUrl.toString(),
6226
+ {
6227
+ method: "POST",
6228
+ headers: {
6229
+ "X-Goog-Upload-Command": "upload, finalize",
6230
+ "X-Goog-Upload-Offset": "0",
6231
+ "Content-Length": String(bytes.byteLength)
6232
+ },
6233
+ body: bytes,
6234
+ signal: request.signal
6235
+ },
6236
+ { governor: this.governor, provider: this.name, maxRetries: 0 }
6237
+ );
6238
+ if (!finalize.ok) {
6239
+ const body = await finalize.text();
6240
+ throw buildProviderError({
6241
+ provider: this.name,
6242
+ status: finalize.status,
6243
+ headers: finalize.headers,
6244
+ message: safeProviderMessage("Gemini", finalize.status, body),
6245
+ body
6246
+ });
6247
+ }
6248
+ const payload = await finalize.json();
6249
+ const file = payload.file;
6250
+ if (!file || typeof file.name !== "string") {
6251
+ throw new InvalidTranscriptionInputError(
6252
+ "Gemini transcription upload did not return a file name"
6253
+ );
6254
+ }
6255
+ return { name: file.name, file, mimeType };
6256
+ }
6257
+ async waitForReady(file, key, signal) {
6258
+ let current = file;
6259
+ const deadline = Date.now() + READY_TIMEOUT_MS;
6260
+ if (!current.state) {
6261
+ throw new InvalidTranscriptionInputError(
6262
+ "Gemini transcription file readiness state is missing"
6263
+ );
6264
+ }
6265
+ while (current.state && current.state !== "ACTIVE") {
6266
+ if (current.state === "FAILED")
6267
+ throw new InvalidTranscriptionInputError("Gemini transcription file processing failed");
6268
+ if (Date.now() >= deadline)
6269
+ throw new InvalidTranscriptionInputError("Gemini transcription file readiness timed out");
6270
+ await sleep3(Math.min(READY_POLL_MS, deadline - Date.now()), signal);
6271
+ const res = await fetchWithRetry(
6272
+ `${this.baseUrl}/${current.name}`,
6273
+ { method: "GET", headers: { "x-goog-api-key": key }, signal },
6274
+ { governor: this.governor, provider: this.name, maxRetries: 0 }
6275
+ );
6276
+ if (!res.ok) {
6277
+ const body = await res.text();
6278
+ throw buildProviderError({
6279
+ provider: this.name,
6280
+ status: res.status,
6281
+ headers: res.headers,
6282
+ message: safeProviderMessage("Gemini", res.status, body),
6283
+ body
6284
+ });
6285
+ }
6286
+ current = this.fileFromJson(await res.json(), current.mimeType);
6287
+ }
6288
+ return current;
6289
+ }
6290
+ async infer(request, key, uri, mimeType, mode, options) {
6291
+ const transcriptionConfig = {
6292
+ mode: mode === "smart" ? "smart" : {
6293
+ type: "verbatim",
6294
+ ...request.timestamps ? { timestamp_granularities: ["word"] } : {},
6295
+ ...request.diarization ? { diarization_mode: "speaker" } : {}
6296
+ },
6297
+ ...request.language ? { language_codes: [request.language] } : {},
6298
+ ...options.customVocabulary ? { custom_vocabulary: options.customVocabulary } : {}
6299
+ };
6300
+ const body = {
6301
+ model: request.model,
6302
+ input: [{ type: "audio", uri, mime_type: mimeType }],
6303
+ store: false,
6304
+ generation_config: { transcription_config: transcriptionConfig }
6305
+ };
6306
+ const res = await fetchWithRetry(
6307
+ `${this.baseUrl}/interactions`,
6308
+ {
6309
+ method: "POST",
6310
+ headers: { "x-goog-api-key": key, "Content-Type": "application/json" },
6311
+ body: JSON.stringify(body),
6312
+ signal: request.signal
6313
+ },
6314
+ { governor: this.governor, provider: this.name }
6315
+ );
6316
+ if (!res.ok) {
6317
+ const errorBody = await res.text();
6318
+ throw buildProviderError({
6319
+ provider: this.name,
6320
+ status: res.status,
6321
+ headers: res.headers,
6322
+ message: safeProviderMessage("Gemini", res.status, errorBody),
6323
+ body: errorBody
6324
+ });
6325
+ }
6326
+ const json = await res.json();
6327
+ if (json.status !== "completed") {
6328
+ throw new InvalidTranscriptionInputError("Gemini transcription interaction did not complete");
6329
+ }
6330
+ const words = [];
6331
+ let text = "";
6332
+ let hasModelOutputText = false;
6333
+ if (Array.isArray(json.steps)) {
6334
+ for (const step of json.steps) {
6335
+ if (!step || typeof step !== "object") continue;
6336
+ const modelOutput = step;
6337
+ if (modelOutput.type !== "model_output" || !Array.isArray(modelOutput.content)) continue;
6338
+ for (const block of modelOutput.content) {
6339
+ if (!block || typeof block !== "object") continue;
6340
+ const content = block;
6341
+ if (content.type !== "text" || typeof content.text !== "string") continue;
6342
+ hasModelOutputText = true;
6343
+ text += content.text;
6344
+ if (!Array.isArray(content.annotations)) continue;
6345
+ for (const raw of content.annotations) {
6346
+ if (!raw || typeof raw !== "object") continue;
6347
+ const annotation = raw;
6348
+ if (annotation.type !== "word_info") continue;
6349
+ if (typeof annotation.text !== "string") continue;
6350
+ const start = secondsOffset(annotation.start_offset);
6351
+ const end = secondsOffset(annotation.end_offset);
6352
+ if (start === void 0 || end === void 0 || end < start) continue;
6353
+ words.push({
6354
+ text: annotation.text,
6355
+ start,
6356
+ end,
6357
+ ...typeof annotation.speaker === "string" ? { speaker: annotation.speaker } : {}
6358
+ });
6359
+ }
6360
+ }
6361
+ }
6362
+ }
6363
+ if (!hasModelOutputText)
6364
+ throw new InvalidTranscriptionInputError(
6365
+ "Gemini transcription response did not contain model output text"
6366
+ );
6367
+ return { text, ...words.length ? { words } : {}, ...transcriptUsage(json.usage) };
6368
+ }
6369
+ fileFromJson(value, fallbackMimeType) {
6370
+ const file = "file" in value && value.file ? value.file : value;
6371
+ if (typeof file.name !== "string" || typeof file.uri !== "string")
6372
+ throw new InvalidTranscriptionInputError(
6373
+ "Gemini transcription upload did not return a file reference"
6374
+ );
6375
+ return {
6376
+ name: file.name,
6377
+ uri: file.uri,
6378
+ mimeType: typeof file.mimeType === "string" ? file.mimeType : typeof file.mime_type === "string" ? file.mime_type : fallbackMimeType,
6379
+ ...typeof file.state === "string" ? { state: file.state } : {}
6380
+ };
6381
+ }
6382
+ async cleanup(fileName, key) {
6383
+ const controller = new AbortController();
6384
+ const timer = setTimeout(() => controller.abort(), CLEANUP_TIMEOUT_MS);
6385
+ try {
6386
+ const res = await fetchWithRetry(
6387
+ `${this.baseUrl}/${fileName}`,
6388
+ { method: "DELETE", headers: { "x-goog-api-key": key }, signal: controller.signal },
6389
+ { governor: this.governor, provider: this.name, maxRetries: 0 }
6390
+ );
6391
+ return res.ok ? "deleted" : "failed";
6392
+ } catch {
6393
+ return controller.signal.aborted ? "timed_out" : "failed";
6394
+ } finally {
6395
+ clearTimeout(timer);
6396
+ }
6397
+ }
6398
+ async resolveKey() {
6399
+ const key = await resolveApiKey(this.apiKeySource);
6400
+ if (!key)
6401
+ throw new Error("Google API key is required. Set GOOGLE_API_KEY or pass apiKey in options.");
6402
+ return key;
6403
+ }
6404
+ };
6405
+
6406
+ // src/providers/openrouter-transcription.ts
6407
+ var FORMAT_BY_MIME = {
6408
+ "audio/aac": "aac",
6409
+ "audio/flac": "flac",
6410
+ "audio/m4a": "m4a",
6411
+ "audio/mp3": "mp3",
6412
+ "audio/mp4": "mp4",
6413
+ "audio/mpeg": "mp3",
6414
+ "audio/ogg": "ogg",
6415
+ "audio/wav": "wav",
6416
+ "audio/webm": "webm"
6417
+ };
6418
+ var OpenRouterTranscriptionProvider = class {
6419
+ name = "openrouter-transcription";
6420
+ baseUrl;
6421
+ apiKeySource;
6422
+ governor;
6423
+ constructor(options = {}) {
6424
+ this.apiKeySource = options.apiKey ?? process.env.OPENROUTER_API_KEY ?? "";
6425
+ this.baseUrl = (options.baseUrl ?? process.env.OPENROUTER_BASE_URL ?? "https://openrouter.ai/api/v1").replace(/\/$/, "");
6426
+ assertSafeProviderBaseUrl(
6427
+ this.baseUrl,
6428
+ "OpenRouter transcription provider",
6429
+ options.dangerouslyAllowInsecureHttp
6430
+ );
6431
+ this.governor = options.rateLimit ? new RateLimiter(options.rateLimit) : void 0;
6432
+ }
6433
+ capabilities(model) {
6434
+ return model.trim().length > 0 ? { sources: ["bytes", "base64"] } : void 0;
6435
+ }
6436
+ async transcribe(request) {
6437
+ if (request.model.trim().length === 0) {
6438
+ throw new UnsupportedTranscriptionInputError({
6439
+ provider: this.name,
6440
+ model: request.model,
6441
+ feature: "this transcription model"
6442
+ });
6443
+ }
6444
+ requireNoProviderFile(request.audio, this.name, request.model);
6445
+ if (request.audio.type === "provider-file")
6446
+ throw new UnsupportedTranscriptionInputError({
6447
+ provider: this.name,
6448
+ model: request.model,
6449
+ feature: "provider-file transcription input"
6450
+ });
6451
+ const options = assertOnlyProviderOptions(request, this.name, request.model, [
6452
+ "temperature",
6453
+ "provider"
6454
+ ]);
6455
+ if (options.temperature !== void 0 && (typeof options.temperature !== "number" || !Number.isFinite(options.temperature) || options.temperature < 0 || options.temperature > 1))
6456
+ throw new InvalidTranscriptionInputError(
6457
+ "OpenRouter transcription temperature must be a number between 0 and 1"
6458
+ );
6459
+ if (options.provider !== void 0 && (!options.provider || Array.isArray(options.provider) || typeof options.provider !== "object"))
6460
+ throw new InvalidTranscriptionInputError(
6461
+ "OpenRouter transcription provider routing must be an object"
6462
+ );
6463
+ const format = FORMAT_BY_MIME[request.audio.mediaType];
6464
+ if (!format)
6465
+ throw new UnsupportedTranscriptionInputError({
6466
+ provider: this.name,
6467
+ model: request.model,
6468
+ feature: "this audio media type"
6469
+ });
6470
+ const bytes = inlineAudioBytes(request.audio);
6471
+ const body = {
6472
+ model: request.model,
6473
+ input_audio: { data: Buffer.from(bytes).toString("base64"), format },
6474
+ ...request.language ? { language: request.language } : {},
6475
+ ...options.temperature !== void 0 ? { temperature: options.temperature } : {},
6476
+ ...options.provider !== void 0 ? { provider: options.provider } : {}
6477
+ };
6478
+ const key = await this.resolveKey();
6479
+ const res = await fetchWithRetry(
6480
+ `${this.baseUrl}/audio/transcriptions`,
6481
+ {
6482
+ method: "POST",
6483
+ headers: { Authorization: `Bearer ${key}`, "Content-Type": "application/json" },
6484
+ body: JSON.stringify(body),
6485
+ signal: request.signal
6486
+ },
6487
+ { governor: this.governor, provider: this.name }
6488
+ );
6489
+ if (!res.ok) {
6490
+ const errorBody = await res.text();
6491
+ throw buildProviderError({
6492
+ provider: this.name,
6493
+ status: res.status,
6494
+ headers: res.headers,
6495
+ message: safeProviderMessage("OpenRouter", res.status, errorBody),
6496
+ body: errorBody
6497
+ });
6498
+ }
6499
+ const json = await res.json();
6500
+ if (typeof json.text !== "string")
6501
+ throw new InvalidTranscriptionInputError(
6502
+ "OpenRouter transcription response did not contain text"
6503
+ );
6504
+ return {
6505
+ transcript: { text: json.text, ...transcriptUsage(json.usage) },
6506
+ cleanupStatus: "not_required"
6507
+ };
6508
+ }
6509
+ async resolveKey() {
6510
+ const key = await resolveApiKey(this.apiKeySource);
6511
+ if (!key)
6512
+ throw new Error(
6513
+ "OpenRouter API key is required. Set OPENROUTER_API_KEY or pass apiKey in options."
6514
+ );
6515
+ return key;
6516
+ }
6517
+ };
6518
+
6519
+ // src/providers/transcription-registry.ts
6520
+ var builtinFactories2 = {
6521
+ "openai-transcription": (config) => {
6522
+ const opts = config.providers?.openai ?? {};
6523
+ return new OpenAITranscriptionProvider({
6524
+ apiKey: opts.apiKey,
6525
+ baseUrl: opts.baseUrl,
6526
+ dangerouslyAllowInsecureHttp: opts.dangerouslyAllowInsecureHttp,
6527
+ rateLimit: opts.rateLimit
6528
+ });
6529
+ },
6530
+ "gemini-transcription": (config) => {
6531
+ const opts = config.providers?.google ?? {};
6532
+ return new GeminiTranscriptionProvider({
6533
+ apiKey: opts.apiKey,
6534
+ baseUrl: opts.baseUrl,
6535
+ dangerouslyAllowInsecureHttp: opts.dangerouslyAllowInsecureHttp,
6536
+ rateLimit: opts.rateLimit
6537
+ });
6538
+ },
6539
+ "openrouter-transcription": (config) => {
6540
+ const opts = config.providers?.openrouter ?? {};
6541
+ return new OpenRouterTranscriptionProvider({
6542
+ apiKey: opts.apiKey,
6543
+ baseUrl: opts.baseUrl,
6544
+ dangerouslyAllowInsecureHttp: opts.dangerouslyAllowInsecureHttp,
6545
+ rateLimit: opts.rateLimit
6546
+ });
6547
+ }
6548
+ };
6549
+ var TranscriptionProviderRegistry = class {
6550
+ instances = /* @__PURE__ */ new Map();
6551
+ factories = /* @__PURE__ */ new Map();
6552
+ constructor() {
6553
+ for (const [name, factory] of Object.entries(builtinFactories2))
6554
+ this.factories.set(name, factory);
6555
+ }
6556
+ register(name, factory) {
6557
+ this.factories.set(name, factory);
6558
+ this.instances.delete(name);
6559
+ }
6560
+ registerInstance(name, provider) {
6561
+ this.instances.set(name, provider);
6562
+ }
6563
+ has(name) {
6564
+ return this.instances.has(name) || this.factories.has(name);
6565
+ }
6566
+ get(name, config = {}) {
6567
+ const instance = this.instances.get(name);
6568
+ if (instance) return instance;
6569
+ const factory = this.factories.get(name);
6570
+ if (factory) {
6571
+ const created = factory(config);
6572
+ this.instances.set(name, created);
6573
+ return created;
6574
+ }
6575
+ throw new UnsupportedTranscriptionInputError({
6576
+ provider: name,
6577
+ model: "",
6578
+ feature: "transcription"
6579
+ });
4709
6580
  }
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
6581
  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
- );
6582
+ const colon = typeof uri === "string" ? uri.indexOf(":") : -1;
6583
+ if (colon <= 0 || colon === uri.length - 1) {
6584
+ throw new UnsupportedTranscriptionInputError({
6585
+ provider: "unknown",
6586
+ model: "",
6587
+ feature: "an explicit transcription provider:model URI"
6588
+ });
4739
6589
  }
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();
6590
+ const providerName = uri.slice(0, colon);
6591
+ const model = uri.slice(colon + 1);
6592
+ return { provider: this.get(providerName, config), model, providerName };
4754
6593
  }
4755
6594
  };
4756
- var defaultRegistry = new ProviderRegistry();
4757
6595
 
4758
6596
  // src/context.ts
4759
6597
  var import_node_async_hooks = require("async_hooks");
4760
6598
  var import_node_crypto = require("crypto");
4761
6599
  var import_zod2 = require("zod");
4762
6600
 
6601
+ // src/input.ts
6602
+ var MAX_INLINE_MODEL_INPUT_BYTES = 25 * 1024 * 1024;
6603
+ function invalid(message) {
6604
+ throw new InvalidModelInputError(message);
6605
+ }
6606
+ function nonEmptyString2(value, field) {
6607
+ if (typeof value !== "string" || value.length === 0)
6608
+ invalid(`${field} must be a non-empty string`);
6609
+ return value;
6610
+ }
6611
+ function mediaType(value, field) {
6612
+ return nonEmptyString2(value, field);
6613
+ }
6614
+ function validBase64(value) {
6615
+ return value.length > 0 && value.length % 4 === 0 && /^[A-Za-z0-9+/]*={0,2}$/.test(value);
6616
+ }
6617
+ function decodedBase64Bytes(value) {
6618
+ return Math.floor(value.length * 3 / 4) - (value.endsWith("==") ? 2 : value.endsWith("=") ? 1 : 0);
6619
+ }
6620
+ function normalizeModelInput(input) {
6621
+ if (typeof input === "string") return input;
6622
+ if (!Array.isArray(input) || input.length === 0)
6623
+ invalid("ModelInput parts must be a non-empty array");
6624
+ let inlineBytes = 0;
6625
+ const reserveInlineBytes = (bytes) => {
6626
+ if (bytes > MAX_INLINE_MODEL_INPUT_BYTES - inlineBytes) {
6627
+ invalid(
6628
+ "Inline image data must not exceed 25 MiB total; use a URL or provider-file source where supported"
6629
+ );
6630
+ }
6631
+ inlineBytes += bytes;
6632
+ };
6633
+ return input.map((part, index) => {
6634
+ if (!part || typeof part !== "object") invalid(`ModelInput part ${index} must be an object`);
6635
+ if (part.type === "text")
6636
+ return { type: "text", text: nonEmptyString2(part.text, `part ${index}.text`) };
6637
+ if (part.type !== "image") invalid(`part ${index}.type must be 'text' or 'image'`);
6638
+ const source = part.source;
6639
+ if (!source || typeof source !== "object") invalid(`part ${index}.source must be an object`);
6640
+ let clone;
6641
+ switch (source.type) {
6642
+ case "url": {
6643
+ const url = nonEmptyString2(source.url, `part ${index}.source.url`);
6644
+ let parsed;
6645
+ try {
6646
+ parsed = new URL(url);
6647
+ } catch {
6648
+ invalid(`part ${index}.source.url must be an http(s) URL`);
6649
+ }
6650
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:")
6651
+ invalid(`part ${index}.source.url must be an http(s) URL`);
6652
+ clone = {
6653
+ type: "url",
6654
+ url,
6655
+ ...source.mediaType === void 0 ? {} : { mediaType: mediaType(source.mediaType, `part ${index}.source.mediaType`) }
6656
+ };
6657
+ break;
6658
+ }
6659
+ case "bytes":
6660
+ if (!(source.data instanceof Uint8Array) || source.data.byteLength === 0)
6661
+ invalid(`part ${index}.source.data must be a non-empty Uint8Array`);
6662
+ reserveInlineBytes(source.data.byteLength);
6663
+ clone = {
6664
+ type: "bytes",
6665
+ data: source.data.slice(),
6666
+ mediaType: mediaType(source.mediaType, `part ${index}.source.mediaType`)
6667
+ };
6668
+ break;
6669
+ case "base64": {
6670
+ const data = nonEmptyString2(source.data, `part ${index}.source.data`);
6671
+ if (data.length > 4 * Math.ceil(MAX_INLINE_MODEL_INPUT_BYTES / 3)) {
6672
+ invalid(
6673
+ "Inline image data must not exceed 25 MiB total; use a URL or provider-file source where supported"
6674
+ );
6675
+ }
6676
+ if (!validBase64(data)) invalid(`part ${index}.source.data must be valid base64`);
6677
+ reserveInlineBytes(decodedBase64Bytes(data));
6678
+ clone = {
6679
+ type: "base64",
6680
+ data,
6681
+ mediaType: mediaType(source.mediaType, `part ${index}.source.mediaType`)
6682
+ };
6683
+ break;
6684
+ }
6685
+ case "provider-file":
6686
+ clone = {
6687
+ type: "provider-file",
6688
+ provider: nonEmptyString2(source.provider, `part ${index}.source.provider`),
6689
+ reference: nonEmptyString2(source.reference, `part ${index}.source.reference`),
6690
+ ...source.mediaType === void 0 ? {} : { mediaType: mediaType(source.mediaType, `part ${index}.source.mediaType`) }
6691
+ };
6692
+ break;
6693
+ default:
6694
+ invalid(`part ${index}.source.type is unsupported`);
6695
+ }
6696
+ return {
6697
+ type: "image",
6698
+ source: clone,
6699
+ ...part.label === void 0 ? {} : { label: nonEmptyString2(part.label, `part ${index}.label`) }
6700
+ };
6701
+ });
6702
+ }
6703
+ function inputText(input) {
6704
+ return typeof input === "string" ? input : input.filter((part) => part.type === "text").map((part) => part.text).join("\n");
6705
+ }
6706
+ function cloneModelInput(input) {
6707
+ return typeof input === "string" ? input : normalizeModelInput(input);
6708
+ }
6709
+ function describeModelInput(input) {
6710
+ if (typeof input === "string") return void 0;
6711
+ return {
6712
+ parts: input.map((part) => {
6713
+ if (part.type === "text") return { type: "text", characters: part.text.length };
6714
+ const { source } = part;
6715
+ return {
6716
+ type: "image",
6717
+ source: source.type,
6718
+ ...source.mediaType ? { mediaType: source.mediaType } : {},
6719
+ ...source.type === "bytes" ? { bytes: source.data.byteLength } : {},
6720
+ ...source.type === "base64" ? {
6721
+ bytes: decodedBase64Bytes(source.data)
6722
+ } : {},
6723
+ ...(source.type === "url" ? source.url : source.type === "provider-file" ? source.reference : void 0) ? {
6724
+ locator: source.type === "url" ? source.url : source.type === "provider-file" ? source.reference : void 0
6725
+ } : {},
6726
+ ...part.label ? { label: part.label } : {}
6727
+ };
6728
+ })
6729
+ };
6730
+ }
6731
+ function summarizeModelInput(input) {
6732
+ if (typeof input === "string") return input;
6733
+ return input.map(
6734
+ (part) => part.type === "text" ? part.text : `[image ${part.source.mediaType ?? "media"}]`
6735
+ ).join("\n");
6736
+ }
6737
+ function sanitizeModelInputForTrace(input) {
6738
+ return inputText(input);
6739
+ }
6740
+ function normalizePersistedSessionHistory(history) {
6741
+ return history.map((message, index) => {
6742
+ if (typeof message.content === "string") return { ...message };
6743
+ if (message.role !== "user") {
6744
+ throw new InvalidModelInputError(
6745
+ `History message ${index} has non-text content on a non-user role`
6746
+ );
6747
+ }
6748
+ const content = normalizeModelInput(message.content);
6749
+ if (typeof content !== "string" && content.some((part) => part.type === "image" && part.source.type === "bytes")) {
6750
+ throw new InvalidModelInputError(
6751
+ "Uint8Array image input cannot be persisted in session history"
6752
+ );
6753
+ }
6754
+ return { ...message, content };
6755
+ });
6756
+ }
6757
+
4763
6758
  // src/partial-json.ts
4764
6759
  var MAX_DEPTH = 256;
4765
6760
  function parsePartialJson(input) {
@@ -5331,7 +7326,8 @@ var COST_BEARING_LEAF_TYPES = [
5331
7326
  "agent_call_end",
5332
7327
  "tool_call_end",
5333
7328
  "memory_remember",
5334
- "memory_recall"
7329
+ "memory_recall",
7330
+ "transcription_end"
5335
7331
  ];
5336
7332
  var COST_LEAF_SET = new Set(COST_BEARING_LEAF_TYPES);
5337
7333
  function hasPositiveTokens(event) {
@@ -5339,6 +7335,12 @@ function hasPositiveTokens(event) {
5339
7335
  if (!t) return false;
5340
7336
  return (t.input ?? 0) + (t.output ?? 0) + (t.reasoning ?? 0) + (t.cached ?? 0) + (t.cacheWrite ?? 0) > 0;
5341
7337
  }
7338
+ function hasPositiveBillableWork(event) {
7339
+ if (hasPositiveTokens(event)) return true;
7340
+ const seconds = event.data?.usage?.audioSeconds;
7341
+ const totalTokens = event.data?.usage?.totalTokens;
7342
+ return typeof seconds === "number" && Number.isFinite(seconds) && seconds > 0 || typeof totalTokens === "number" && Number.isFinite(totalTokens) && totalTokens > 0;
7343
+ }
5342
7344
  function isUsableCost(cost) {
5343
7345
  return typeof cost === "number" && Number.isFinite(cost) && cost >= 0;
5344
7346
  }
@@ -5351,7 +7353,7 @@ function isCostBearingLeaf(event) {
5351
7353
  return COST_LEAF_SET.has(event.type);
5352
7354
  }
5353
7355
  function isUnpricedLeaf(event) {
5354
- return event.type !== void 0 && COST_LEAF_SET.has(event.type) && !isUsableCost(event.cost) && hasPositiveTokens(event);
7356
+ return event.type !== void 0 && COST_LEAF_SET.has(event.type) && !isUsableCost(event.cost) && hasPositiveBillableWork(event);
5355
7357
  }
5356
7358
  function isRootLevel(event) {
5357
7359
  const d = event.depth;
@@ -5366,6 +7368,9 @@ var AXL_EVENT_TYPES = [
5366
7368
  // Workflow lifecycle
5367
7369
  "workflow_start",
5368
7370
  "workflow_end",
7371
+ // Transcription operation lifecycle (separate from chat/agent turns)
7372
+ "transcription_start",
7373
+ "transcription_end",
5369
7374
  // Ask boundary
5370
7375
  "ask_start",
5371
7376
  "ask_end",
@@ -5456,6 +7461,8 @@ var LIFECYCLE_TYPES = /* @__PURE__ */ new Set([
5456
7461
  "schema_diagnostic",
5457
7462
  "workflow_start",
5458
7463
  "workflow_end",
7464
+ "transcription_start",
7465
+ "transcription_end",
5459
7466
  "checkpoint_save",
5460
7467
  "checkpoint_replay",
5461
7468
  "await_human",
@@ -6271,6 +8278,21 @@ function redactGate(event) {
6271
8278
  }
6272
8279
  };
6273
8280
  }
8281
+ function redactInputDescriptor(input) {
8282
+ return {
8283
+ ...input,
8284
+ parts: input.parts.map((part) => {
8285
+ if (!part || typeof part !== "object" || !("type" in part) || part.type === "text")
8286
+ return part;
8287
+ const value = part;
8288
+ return {
8289
+ ...part,
8290
+ ...value.locator !== void 0 ? { locator: REDACTED } : {},
8291
+ ...value.label !== void 0 ? { label: REDACTED } : {}
8292
+ };
8293
+ })
8294
+ };
8295
+ }
6274
8296
  function redactMemory(event) {
6275
8297
  const d = event.data;
6276
8298
  if (!d || typeof d !== "object") return event;
@@ -6294,7 +8316,11 @@ var LEGACY_REDACTION_RULES = {
6294
8316
  }
6295
8317
  };
6296
8318
  },
6297
- ask_start: (e) => ({ ...e, prompt: REDACTED }),
8319
+ ask_start: (e) => ({
8320
+ ...e,
8321
+ prompt: REDACTED,
8322
+ ...e.input ? { input: redactInputDescriptor(e.input) } : {}
8323
+ }),
6298
8324
  ask_end: (e) => ({
6299
8325
  ...e,
6300
8326
  outcome: e.outcome.ok ? { ok: true, result: REDACTED } : { ok: false, error: REDACTED }
@@ -6302,6 +8328,14 @@ var LEGACY_REDACTION_RULES = {
6302
8328
  agent_call_start: (e) => {
6303
8329
  const d = e.data;
6304
8330
  const out = { ...d, prompt: REDACTED };
8331
+ if (d.input) {
8332
+ out.input = redactInputDescriptor(d.input);
8333
+ }
8334
+ if (d.messageInputs)
8335
+ out.messageInputs = d.messageInputs.map((entry) => ({
8336
+ ...entry,
8337
+ input: redactInputDescriptor(entry.input)
8338
+ }));
6305
8339
  if (d.system !== void 0) out.system = REDACTED;
6306
8340
  if (Array.isArray(d.messages)) {
6307
8341
  out.messages = [{ role: "system", content: `[${d.messages.length} messages redacted]` }];
@@ -6457,6 +8491,18 @@ function reuseLegacyRule(type) {
6457
8491
  var COMMON_V2_REDACTION_RULES = {
6458
8492
  workflow_start: reuseLegacyRule("workflow_start"),
6459
8493
  workflow_end: reuseLegacyRule("workflow_end"),
8494
+ transcription_start: (e) => e,
8495
+ transcription_end: (e) => {
8496
+ const d = e.data;
8497
+ return {
8498
+ ...e,
8499
+ data: {
8500
+ ...d,
8501
+ ...d.text !== void 0 ? { text: REDACTED } : {},
8502
+ ...d.error !== void 0 ? { error: REDACTED } : {}
8503
+ }
8504
+ };
8505
+ },
6460
8506
  ask_start: reuseLegacyRule("ask_start"),
6461
8507
  ask_end: reuseLegacyRule("ask_end"),
6462
8508
  agent_call_start: reuseLegacyRule("agent_call_start"),
@@ -6651,7 +8697,7 @@ function resolveConfig(config) {
6651
8697
 
6652
8698
  // src/tool-model-output.ts
6653
8699
  var import_node_util = require("util");
6654
- function invalid(path, reason) {
8700
+ function invalid2(path, reason) {
6655
8701
  return new Error(`Invalid tool model output at ${path}: ${reason}`);
6656
8702
  }
6657
8703
  function propertyPath(parent, key) {
@@ -6669,14 +8715,14 @@ function propertyPath(parent, key) {
6669
8715
  function normalize(value, path, stack) {
6670
8716
  if (value === null || typeof value === "string" || typeof value === "boolean") return value;
6671
8717
  if (typeof value === "number") {
6672
- if (!Number.isFinite(value)) throw invalid(path, "number must be finite");
8718
+ if (!Number.isFinite(value)) throw invalid2(path, "number must be finite");
6673
8719
  return value;
6674
8720
  }
6675
- if (value === void 0) throw invalid(path, "undefined is only allowed on object properties");
8721
+ if (value === void 0) throw invalid2(path, "undefined is only allowed on object properties");
6676
8722
  if (typeof value === "bigint" || typeof value === "function" || typeof value === "symbol") {
6677
- throw invalid(path, `unsupported ${typeof value}`);
8723
+ throw invalid2(path, `unsupported ${typeof value}`);
6678
8724
  }
6679
- if (typeof value !== "object") throw invalid(path, `unsupported ${typeof value}`);
8725
+ if (typeof value !== "object") throw invalid2(path, `unsupported ${typeof value}`);
6680
8726
  let prototype;
6681
8727
  let descriptors;
6682
8728
  try {
@@ -6684,38 +8730,38 @@ function normalize(value, path, stack) {
6684
8730
  descriptors = Object.getOwnPropertyDescriptors(value);
6685
8731
  Object.setPrototypeOf(descriptors, null);
6686
8732
  } catch (cause) {
6687
- throw invalid(
8733
+ throw invalid2(
6688
8734
  path,
6689
8735
  `object inspection failed (${cause instanceof Error ? cause.name : "error"})`
6690
8736
  );
6691
8737
  }
6692
- if (stack.has(value)) throw invalid(path, "cyclic reference");
8738
+ if (stack.has(value)) throw invalid2(path, "cyclic reference");
6693
8739
  for (const key of Reflect.ownKeys(descriptors)) {
6694
8740
  const descriptor = descriptors[key];
6695
8741
  if (descriptor && !("value" in descriptor)) {
6696
- throw invalid(
8742
+ throw invalid2(
6697
8743
  typeof key === "string" ? propertyPath(path, key) : `${path}[symbol]`,
6698
8744
  "accessor properties are not supported"
6699
8745
  );
6700
8746
  }
6701
8747
  if (typeof key === "symbol") {
6702
- throw invalid(`${path}[symbol]`, "symbol keys are not supported");
8748
+ throw invalid2(`${path}[symbol]`, "symbol keys are not supported");
6703
8749
  }
6704
8750
  }
6705
8751
  if (typeof descriptors.toJSON?.value === "function") {
6706
- throw invalid(propertyPath(path, "toJSON"), "custom toJSON is not supported");
8752
+ throw invalid2(propertyPath(path, "toJSON"), "custom toJSON is not supported");
6707
8753
  }
6708
8754
  const array = Array.isArray(value);
6709
8755
  if (array && prototype !== Array.prototype && prototype !== null) {
6710
- throw invalid(path, "array subclasses are not supported");
8756
+ throw invalid2(path, "array subclasses are not supported");
6711
8757
  }
6712
8758
  const inheritedToJSON = prototype === Array.prototype || prototype === Object.prototype ? Object.getOwnPropertyDescriptor(prototype, "toJSON") : void 0;
6713
8759
  if (typeof inheritedToJSON?.value === "function") {
6714
- throw invalid(propertyPath(path, "toJSON"), "inherited custom toJSON is not supported");
8760
+ throw invalid2(propertyPath(path, "toJSON"), "inherited custom toJSON is not supported");
6715
8761
  }
6716
8762
  const inheritedThen = prototype === Array.prototype || prototype === Object.prototype ? Object.getOwnPropertyDescriptor(prototype, "then") : void 0;
6717
8763
  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");
8764
+ throw invalid2(path, "thenable output is not supported");
6719
8765
  }
6720
8766
  stack.add(value);
6721
8767
  try {
@@ -6723,15 +8769,15 @@ function normalize(value, path, stack) {
6723
8769
  const normalized2 = [];
6724
8770
  const length = descriptors.length?.value;
6725
8771
  if (!Number.isSafeInteger(length) || length < 0) {
6726
- throw invalid(propertyPath(path, "length"), "array length is invalid");
8772
+ throw invalid2(propertyPath(path, "length"), "array length is invalid");
6727
8773
  }
6728
8774
  for (let index = 0; index < length; index++) {
6729
8775
  const descriptor = descriptors[String(index)];
6730
8776
  if (!descriptor || !("value" in descriptor)) {
6731
- throw invalid(`${path}[${index}]`, "arrays must be dense data properties");
8777
+ throw invalid2(`${path}[${index}]`, "arrays must be dense data properties");
6732
8778
  }
6733
8779
  if (descriptor.value === void 0) {
6734
- throw invalid(`${path}[${index}]`, "array values cannot be undefined");
8780
+ throw invalid2(`${path}[${index}]`, "array values cannot be undefined");
6735
8781
  }
6736
8782
  normalized2.push(normalize(descriptor.value, `${path}[${index}]`, stack));
6737
8783
  }
@@ -6740,21 +8786,21 @@ function normalize(value, path, stack) {
6740
8786
  if (!descriptor?.enumerable) continue;
6741
8787
  const index = Number(key);
6742
8788
  if (!Number.isInteger(index) || index < 0 || index >= length || String(index) !== key) {
6743
- throw invalid(propertyPath(path, key), "non-index array properties are not supported");
8789
+ throw invalid2(propertyPath(path, key), "non-index array properties are not supported");
6744
8790
  }
6745
8791
  }
6746
8792
  Object.setPrototypeOf(normalized2, null);
6747
8793
  return normalized2;
6748
8794
  }
6749
8795
  if (prototype !== Object.prototype && prototype !== null) {
6750
- throw invalid(path, "only plain objects and null-prototype records are supported");
8796
+ throw invalid2(path, "only plain objects and null-prototype records are supported");
6751
8797
  }
6752
8798
  const normalized = /* @__PURE__ */ Object.create(null);
6753
8799
  for (const key of Object.keys(descriptors)) {
6754
8800
  const descriptor = descriptors[key];
6755
8801
  if (!descriptor?.enumerable) continue;
6756
8802
  if (!("value" in descriptor)) {
6757
- throw invalid(propertyPath(path, key), "accessor properties are not supported");
8803
+ throw invalid2(propertyPath(path, key), "accessor properties are not supported");
6758
8804
  }
6759
8805
  if (descriptor.value === void 0) continue;
6760
8806
  normalized[key] = normalize(descriptor.value, propertyPath(path, key), stack);
@@ -7262,44 +9308,44 @@ function resolveBranchDrainTimeoutMs(value) {
7262
9308
  return resolved;
7263
9309
  }
7264
9310
  function parseHumanDecision(value) {
7265
- const invalid2 = () => {
9311
+ const invalid3 = () => {
7266
9312
  throw new AxlError(
7267
9313
  "INVALID_HUMAN_DECISION",
7268
9314
  "Human decision must be a plain object with a boolean approved field and only branch-appropriate string data"
7269
9315
  );
7270
9316
  };
7271
- if (typeof value !== "object" || value === null || Array.isArray(value)) invalid2();
9317
+ if (typeof value !== "object" || value === null || Array.isArray(value)) invalid3();
7272
9318
  let prototype;
7273
9319
  try {
7274
9320
  prototype = Object.getPrototypeOf(value);
7275
9321
  } catch {
7276
- return invalid2();
9322
+ return invalid3();
7277
9323
  }
7278
- if (prototype !== Object.prototype && prototype !== null) invalid2();
9324
+ if (prototype !== Object.prototype && prototype !== null) invalid3();
7279
9325
  let ownKeys;
7280
9326
  try {
7281
9327
  ownKeys = Reflect.ownKeys(value);
7282
9328
  } catch {
7283
- return invalid2();
9329
+ return invalid3();
7284
9330
  }
7285
9331
  if (ownKeys.some(
7286
9332
  (key) => typeof key !== "string" || key !== "approved" && key !== "data" && key !== "reason"
7287
9333
  )) {
7288
- invalid2();
9334
+ invalid3();
7289
9335
  }
7290
9336
  const readDataProperty = (key) => {
7291
9337
  let descriptor;
7292
9338
  try {
7293
9339
  descriptor = Object.getOwnPropertyDescriptor(value, key);
7294
9340
  } catch {
7295
- return invalid2();
9341
+ return invalid3();
7296
9342
  }
7297
9343
  if (!descriptor) {
7298
- if (ownKeys.includes(key)) invalid2();
9344
+ if (ownKeys.includes(key)) invalid3();
7299
9345
  return { present: false, value: void 0 };
7300
9346
  }
7301
- if (!ownKeys.includes(key)) return invalid2();
7302
- if (!("value" in descriptor)) return invalid2();
9347
+ if (!ownKeys.includes(key)) return invalid3();
9348
+ if (!("value" in descriptor)) return invalid3();
7303
9349
  return { present: true, value: descriptor.value };
7304
9350
  };
7305
9351
  const approved = readDataProperty("approved");
@@ -7318,7 +9364,7 @@ function parseHumanDecision(value) {
7318
9364
  };
7319
9365
  }
7320
9366
  }
7321
- return invalid2();
9367
+ return invalid3();
7322
9368
  }
7323
9369
  var askStorage = new import_node_async_hooks.AsyncLocalStorage();
7324
9370
  var inlineSchemaCache = /* @__PURE__ */ new WeakMap();
@@ -7426,8 +9472,12 @@ function appendRetryMessages(messages, content, feedbackMessage, providerMetadat
7426
9472
  }
7427
9473
  function estimateMessagesTokens(messages) {
7428
9474
  let total = 0;
9475
+ let unmeasured = false;
7429
9476
  for (const msg of messages) {
7430
- total += estimateTokens(msg.content);
9477
+ total += estimateTokens(inputText(msg.content));
9478
+ if (typeof msg.content !== "string") {
9479
+ unmeasured ||= msg.content.some((part) => part.type === "image");
9480
+ }
7431
9481
  if (msg.tool_calls) {
7432
9482
  for (const tc of msg.tool_calls) {
7433
9483
  total += estimateTokens(tc.function.name + tc.function.arguments);
@@ -7435,7 +9485,73 @@ function estimateMessagesTokens(messages) {
7435
9485
  }
7436
9486
  total += 4;
7437
9487
  }
7438
- return total;
9488
+ return { tokens: total, unmeasured };
9489
+ }
9490
+ function cloneMessagesForProvider(messages) {
9491
+ return messages.map((message) => ({
9492
+ ...message,
9493
+ content: cloneModelInput(message.content),
9494
+ ...message.tool_calls ? {
9495
+ tool_calls: message.tool_calls.map((call) => ({
9496
+ ...call,
9497
+ function: { ...call.function }
9498
+ }))
9499
+ } : {}
9500
+ }));
9501
+ }
9502
+ function assertRichMessagesAreUserOnly(messages) {
9503
+ for (const message of messages) {
9504
+ if (typeof message.content !== "string" && message.role !== "user") {
9505
+ throw new InvalidModelInputError(
9506
+ "Non-text model input parts are only supported on user messages"
9507
+ );
9508
+ }
9509
+ }
9510
+ }
9511
+ function normalizeSessionHistory(messages) {
9512
+ return messages.map((message) => {
9513
+ if (typeof message.content === "string") return { ...message };
9514
+ if (message.role !== "user") {
9515
+ throw new InvalidModelInputError(
9516
+ "Non-text model input parts are only supported on user messages"
9517
+ );
9518
+ }
9519
+ return { ...message, content: normalizeModelInput(message.content) };
9520
+ });
9521
+ }
9522
+ function hasRichMessage(messages) {
9523
+ return messages.some((message) => typeof message.content !== "string");
9524
+ }
9525
+ function firstImageSource(input, history) {
9526
+ const find = (value) => typeof value === "string" ? void 0 : value.find((part) => part.type === "image")?.source.type;
9527
+ return find(input) ?? history.map((message) => find(message.content)).find(Boolean);
9528
+ }
9529
+ function appendHandoffInstruction(input, originalText, instruction) {
9530
+ if (typeof input === "string" || instruction.length === 0 || instruction === originalText)
9531
+ return input;
9532
+ return [...input, { type: "text", text: instruction }];
9533
+ }
9534
+ var RICH_MODEL_INPUT_ERROR_MESSAGE = "Provider request failed while processing rich model input";
9535
+ var RICH_MODEL_INPUT_ERROR_NAME = "RichModelInputError";
9536
+ var MAX_RICH_FAILURE_ASSOCIATIONS = 32;
9537
+ var MAX_RICH_FAILURE_CAUSE_DEPTH = 4;
9538
+ function legacyErrorMessage(error) {
9539
+ return error instanceof Error ? error.message : String(error);
9540
+ }
9541
+ function providerErrorEventMetadata(error, richRequest) {
9542
+ if (!(error instanceof ProviderError)) {
9543
+ return error instanceof AxlError && richRequest ? { code: error.code } : {};
9544
+ }
9545
+ return {
9546
+ status: error.status,
9547
+ retryable: error.retryable,
9548
+ ...richRequest ? {
9549
+ code: error.code,
9550
+ provider: error.provider,
9551
+ ...error.requestId ? { requestId: error.requestId } : {},
9552
+ ...error.retryAfterMs !== void 0 ? { retryAfterMs: error.retryAfterMs } : {}
9553
+ } : {}
9554
+ };
7439
9555
  }
7440
9556
  var WorkflowContext = class _WorkflowContext {
7441
9557
  input;
@@ -7451,6 +9567,7 @@ var WorkflowContext = class _WorkflowContext {
7451
9567
  }
7452
9568
  config;
7453
9569
  providerRegistry;
9570
+ transcriptionProviderRegistry;
7454
9571
  sessionHistory;
7455
9572
  onTrace;
7456
9573
  pendingDecisions;
@@ -7476,6 +9593,10 @@ var WorkflowContext = class _WorkflowContext {
7476
9593
  */
7477
9594
  autoCheckpointCounters;
7478
9595
  branchDrainState;
9596
+ /** Bounded exact-identity failure association for this execution tree.
9597
+ * Unlike a process-global marker, this cannot affect another context that
9598
+ * happens to reuse the same Error object. */
9599
+ richFailureState;
7479
9600
  /** Root-shared idempotency guards for workflow lifecycle emission.
7480
9601
  * Both `runtime.execute()` and `runtime.stream()` have paths where a
7481
9602
  * post-emit side-effect (checkpoint deletion, state-store persistence)
@@ -7559,7 +9680,8 @@ var WorkflowContext = class _WorkflowContext {
7559
9680
  get events() {
7560
9681
  if (!this._busRef.current) {
7561
9682
  this._busRef.current = new AxlEventBus(this._eventStreamOptions);
7562
- if (this.signal?.aborted) {
9683
+ const signal = this.currentSignal;
9684
+ if (signal?.aborted) {
7563
9685
  this._busRef.current._finish();
7564
9686
  }
7565
9687
  }
@@ -7597,6 +9719,7 @@ var WorkflowContext = class _WorkflowContext {
7597
9719
  this.metadata = init.metadata ?? {};
7598
9720
  this.config = init.config;
7599
9721
  this.providerRegistry = init.providerRegistry;
9722
+ this.transcriptionProviderRegistry = init.transcriptionProviderRegistry;
7600
9723
  this.sessionHistory = init.sessionHistory ?? [];
7601
9724
  this.onTrace = init.onTrace;
7602
9725
  this.pendingDecisions = init.pendingDecisions;
@@ -7630,6 +9753,11 @@ var WorkflowContext = class _WorkflowContext {
7630
9753
  byAgent: /* @__PURE__ */ new Map(),
7631
9754
  root: 0
7632
9755
  };
9756
+ this.richFailureState = init.richFailureState ?? {
9757
+ associations: [],
9758
+ overflowed: false,
9759
+ indeterminate: false
9760
+ };
7633
9761
  const branchDrainTimeoutMs = resolveBranchDrainTimeoutMs(init.branchDrainTimeoutMs);
7634
9762
  this.branchDrainState = init.branchDrainState ?? {
7635
9763
  pending: /* @__PURE__ */ new Set(),
@@ -7662,6 +9790,7 @@ var WorkflowContext = class _WorkflowContext {
7662
9790
  executionId: this.executionId,
7663
9791
  config: this.config,
7664
9792
  providerRegistry: this.providerRegistry,
9793
+ transcriptionProviderRegistry: this.transcriptionProviderRegistry,
7665
9794
  metadata: { ...this.metadata },
7666
9795
  // Shared infrastructure
7667
9796
  budgetContext: this.budgetContext,
@@ -7684,6 +9813,7 @@ var WorkflowContext = class _WorkflowContext {
7684
9813
  autoCheckpointCounters: this.autoCheckpointCounters,
7685
9814
  branchDrainState: this.branchDrainState,
7686
9815
  workflowLifecycleState: this.workflowLifecycleState,
9816
+ richFailureState: this.richFailureState,
7687
9817
  // Share the parent's bus slot (mutable ref). Late allocation in
7688
9818
  // either parent or child propagates because both read the same
7689
9819
  // slot. Nested-ask events (agent-as-tool pattern) bubble up to
@@ -7695,6 +9825,52 @@ var WorkflowContext = class _WorkflowContext {
7695
9825
  // Isolated: sessionHistory (empty)
7696
9826
  });
7697
9827
  }
9828
+ /** Associate an exact thrown value with rich input in this execution only. */
9829
+ recordRichFailure(error) {
9830
+ const state = this.richFailureState;
9831
+ if (state.associations.some((known) => Object.is(known, error))) return;
9832
+ if (state.associations.length >= MAX_RICH_FAILURE_ASSOCIATIONS) {
9833
+ state.associations.shift();
9834
+ state.overflowed = true;
9835
+ state.indeterminate = true;
9836
+ }
9837
+ state.associations.push(error);
9838
+ }
9839
+ /** @internal Safe observer projection for a failure associated with rich input. */
9840
+ _observerErrorProjection(error) {
9841
+ const state = this.richFailureState;
9842
+ const safe = () => ({
9843
+ message: RICH_MODEL_INPUT_ERROR_MESSAGE,
9844
+ name: RICH_MODEL_INPUT_ERROR_NAME
9845
+ });
9846
+ const indeterminate = () => {
9847
+ if (state.associations.length === 0) return void 0;
9848
+ state.indeterminate = true;
9849
+ return safe();
9850
+ };
9851
+ let current = error;
9852
+ const seen = /* @__PURE__ */ new WeakSet();
9853
+ for (let depth = 0; depth < MAX_RICH_FAILURE_CAUSE_DEPTH; depth++) {
9854
+ if (state.associations.some((known) => Object.is(known, current))) {
9855
+ return safe();
9856
+ }
9857
+ if ((typeof current !== "object" || current === null) && typeof current !== "function") {
9858
+ return state.overflowed || state.indeterminate ? safe() : void 0;
9859
+ }
9860
+ if (seen.has(current)) return indeterminate();
9861
+ seen.add(current);
9862
+ let descriptor;
9863
+ try {
9864
+ descriptor = Object.getOwnPropertyDescriptor(current, "cause");
9865
+ } catch {
9866
+ return indeterminate();
9867
+ }
9868
+ if (!descriptor) return state.overflowed || state.indeterminate ? safe() : void 0;
9869
+ if (!("value" in descriptor)) return indeterminate();
9870
+ current = descriptor.value;
9871
+ }
9872
+ return indeterminate();
9873
+ }
7698
9874
  /**
7699
9875
  * Resolve the current abort signal.
7700
9876
  * Branch-scoped signals (from race/spawn/map/budget) in AsyncLocalStorage
@@ -7896,6 +10072,17 @@ var WorkflowContext = class _WorkflowContext {
7896
10072
  }
7897
10073
  // ── ctx.ask() ─────────────────────────────────────────────────────────
7898
10074
  async ask(agent2, prompt, options) {
10075
+ const normalizedInput = normalizeModelInput(prompt);
10076
+ const normalizedHistory = normalizeSessionHistory(this.sessionHistory);
10077
+ const delegateInputHolder = agent2;
10078
+ if (delegateInputHolder._delegateOriginalInput !== void 0) {
10079
+ delegateInputHolder._delegateOriginalInput = normalizeModelInput(
10080
+ delegateInputHolder._delegateOriginalInput
10081
+ );
10082
+ }
10083
+ const richAsk = typeof normalizedInput !== "string" || hasRichMessage(normalizedHistory) || delegateInputHolder._delegateOriginalInput !== void 0 && typeof delegateInputHolder._delegateOriginalInput !== "string";
10084
+ const promptText = inputText(normalizedInput);
10085
+ const inputDescriptor = describeModelInput(normalizedInput);
7899
10086
  const agentName = agent2._name;
7900
10087
  return this._checkpoint(
7901
10088
  this._autoCheckpointName("ask", agentName),
@@ -7915,7 +10102,11 @@ var WorkflowContext = class _WorkflowContext {
7915
10102
  };
7916
10103
  return askStorage.run(frame, async () => {
7917
10104
  const askStart = Date.now();
7918
- this.emitEvent({ type: "ask_start", prompt });
10105
+ this.emitEvent({
10106
+ type: "ask_start",
10107
+ prompt: promptText,
10108
+ ...inputDescriptor ? { input: inputDescriptor } : {}
10109
+ });
7919
10110
  const costBefore = this.budgetContext?.totalCost ?? 0;
7920
10111
  const unpricedCountBefore = this.budgetContext?.unpricedCount ?? 0;
7921
10112
  const resolveCtx = options?.metadata ? { metadata: { ...this.metadata, ...options.metadata } } : { metadata: this.metadata };
@@ -7923,10 +10114,11 @@ var WorkflowContext = class _WorkflowContext {
7923
10114
  const doCall = async () => {
7924
10115
  const result2 = await this.executeAgentCall(
7925
10116
  agent2,
7926
- prompt,
10117
+ normalizedInput,
7927
10118
  options,
7928
10119
  void 0,
7929
- usageCapture
10120
+ usageCapture,
10121
+ normalizedHistory
7930
10122
  );
7931
10123
  return result2;
7932
10124
  };
@@ -7943,6 +10135,9 @@ var WorkflowContext = class _WorkflowContext {
7943
10135
  },
7944
10136
  async (span) => {
7945
10137
  const r = await doCall();
10138
+ if (usageCapture.modelUri) {
10139
+ span.setAttribute("axl.agent.model", usageCapture.modelUri);
10140
+ }
7946
10141
  const costAfter2 = this.budgetContext?.totalCost ?? 0;
7947
10142
  span.setAttribute("axl.agent.cost", costAfter2 - costBefore);
7948
10143
  span.setAttribute("axl.agent.duration", Date.now() - askStart);
@@ -7976,9 +10171,10 @@ var WorkflowContext = class _WorkflowContext {
7976
10171
  try {
7977
10172
  this.onAgentCallComplete({
7978
10173
  agent: agent2._name,
7979
- prompt,
10174
+ prompt: promptText,
10175
+ ...inputDescriptor ? { input: inputDescriptor } : {},
7980
10176
  response: typeof result === "string" ? result : JSON.stringify(result),
7981
- model: agent2.resolveModel(resolveCtx),
10177
+ model: usageCapture.modelUri ?? agent2.resolveModel(resolveCtx),
7982
10178
  cost: costAfter - costBefore,
7983
10179
  unpriced: frame.askUnpriced || unpricedCountAfter > unpricedCountBefore,
7984
10180
  duration: Date.now() - askStart,
@@ -8002,9 +10198,10 @@ var WorkflowContext = class _WorkflowContext {
8002
10198
  } catch (err) {
8003
10199
  askFailed = true;
8004
10200
  askFailure = err;
10201
+ if (richAsk) this.recordRichFailure(err);
8005
10202
  outcome = {
8006
10203
  ok: false,
8007
- error: err instanceof Error ? err.message : String(err)
10204
+ error: this._observerErrorProjection(err)?.message ?? legacyErrorMessage(err)
8008
10205
  };
8009
10206
  }
8010
10207
  let terminalFailure;
@@ -8032,7 +10229,7 @@ var WorkflowContext = class _WorkflowContext {
8032
10229
  { agent: agentName }
8033
10230
  );
8034
10231
  }
8035
- async executeAgentCall(agent2, prompt, options, handoffMessages, usageCapture) {
10232
+ async executeAgentCall(agent2, input, options, handoffMessages, usageCapture, sessionHistory = normalizeSessionHistory(this.sessionHistory)) {
8036
10233
  if (this.budgetContext?.exceeded) {
8037
10234
  const { limit, totalCost: spent, policy } = this.budgetContext;
8038
10235
  if (policy === "warn") {
@@ -8049,7 +10246,82 @@ var WorkflowContext = class _WorkflowContext {
8049
10246
  const resolveCtx = options?.metadata ? { metadata: { ...this.metadata, ...options.metadata } } : { metadata: this.metadata };
8050
10247
  const modelUri = agent2.resolveModel(resolveCtx);
8051
10248
  const systemPrompt = agent2.resolveSystem(resolveCtx);
8052
- const { provider, model } = this.providerRegistry.resolve(modelUri, this.config);
10249
+ const { provider, model: resolvedModel } = this.providerRegistry.resolve(modelUri, this.config);
10250
+ let model = resolvedModel;
10251
+ const prompt = inputText(input);
10252
+ const providerOptions = options?.providerOptions ?? agent2._config.providerOptions;
10253
+ const richRequest = typeof input !== "string" || hasRichMessage(sessionHistory);
10254
+ if (richRequest) {
10255
+ if (providerOptions && ("messages" in providerOptions || "input" in providerOptions)) {
10256
+ throw new UnsupportedModelInputError({
10257
+ provider: provider.name ?? modelUri.split(":", 1)[0],
10258
+ model,
10259
+ modality: "image",
10260
+ feature: "raw input-container providerOptions"
10261
+ });
10262
+ }
10263
+ const source = firstImageSource(input, sessionHistory);
10264
+ if (!provider.validateInput) {
10265
+ throw new UnsupportedModelInputError({
10266
+ provider: provider.name ?? modelUri.split(":", 1)[0],
10267
+ model,
10268
+ modality: "image",
10269
+ ...source ? { source } : {}
10270
+ });
10271
+ }
10272
+ const validation = provider.validateInput({
10273
+ model,
10274
+ input: cloneModelInput(input),
10275
+ history: cloneMessagesForProvider(sessionHistory),
10276
+ stream: this._streamingEnabled,
10277
+ hasTools: Boolean(
10278
+ agent2._config.tools?.length || agent2._config.mcp?.length || agent2._config.mcpTools?.length || agent2._config.handoffs
10279
+ ),
10280
+ responseMode: options?.schema ? "structured" : "text",
10281
+ providerOptions
10282
+ });
10283
+ if (!validation || typeof validation.effectiveModel !== "string" || validation.effectiveModel.length === 0) {
10284
+ throw new UnsupportedModelInputError({
10285
+ provider: provider.name ?? modelUri.split(":", 1)[0],
10286
+ model,
10287
+ modality: "image",
10288
+ ...source ? { source } : {}
10289
+ });
10290
+ }
10291
+ model = validation.effectiveModel;
10292
+ }
10293
+ const effectiveModelUri = !richRequest ? modelUri : `${modelUri.slice(0, modelUri.indexOf(":") + 1)}${model}`;
10294
+ if (usageCapture) usageCapture.modelUri ??= effectiveModelUri;
10295
+ if (richRequest) {
10296
+ const descriptors = [
10297
+ describeModelInput(input),
10298
+ ...sessionHistory.map((message) => describeModelInput(message.content))
10299
+ ].filter(
10300
+ (descriptor) => descriptor !== void 0
10301
+ );
10302
+ const parts = descriptors.flatMap((descriptor) => descriptor.parts);
10303
+ this.spanManager?.addEventToActiveSpan("axl.model_input", {
10304
+ "axl.input.parts": parts.length,
10305
+ "axl.input.images": parts.filter((part) => part.type === "image").length,
10306
+ "axl.input.source.bytes": parts.filter(
10307
+ (part) => part.type === "image" && part.source === "bytes"
10308
+ ).length,
10309
+ "axl.input.source.base64": parts.filter(
10310
+ (part) => part.type === "image" && part.source === "base64"
10311
+ ).length,
10312
+ "axl.input.source.url": parts.filter(
10313
+ (part) => part.type === "image" && part.source === "url"
10314
+ ).length,
10315
+ "axl.input.source.provider_file": parts.filter(
10316
+ (part) => part.type === "image" && part.source === "provider-file"
10317
+ ).length,
10318
+ "axl.input.inline_bytes": parts.reduce(
10319
+ (sum, part) => sum + (part.type === "image" ? part.bytes ?? 0 : 0),
10320
+ 0
10321
+ )
10322
+ });
10323
+ }
10324
+ assertRichMessagesAreUserOnly(sessionHistory);
8053
10325
  let resolvedHandoffs;
8054
10326
  if (typeof agent2._config.handoffs === "function") {
8055
10327
  try {
@@ -8070,50 +10342,60 @@ var WorkflowContext = class _WorkflowContext {
8070
10342
  messages.push({ role: "system", content: systemPrompt });
8071
10343
  }
8072
10344
  const maxContext = agent2._config.maxContext;
8073
- if (maxContext && this.sessionHistory.length > 0) {
10345
+ if (maxContext && sessionHistory.length > 0) {
8074
10346
  const reserveTokens = this.config.contextManagement?.reserveTokens ?? 2e3;
8075
10347
  const systemTokens = systemPrompt ? estimateTokens(systemPrompt) : 0;
8076
10348
  const toolTokens = toolDefs.length > 0 ? estimateTokens(JSON.stringify(toolDefs)) : 0;
8077
10349
  const overhead = systemTokens + toolTokens + reserveTokens;
8078
10350
  const availableForHistory = maxContext - overhead;
8079
- const historyTokens = estimateMessagesTokens(this.sessionHistory);
8080
- if (historyTokens > availableForHistory) {
10351
+ const historyEstimate = estimateMessagesTokens(sessionHistory);
10352
+ if (historyEstimate.unmeasured) {
10353
+ this.emitEvent({
10354
+ type: "log",
10355
+ data: {
10356
+ warning: "Context media contribution is unmeasured; provider will enforce its context limit"
10357
+ }
10358
+ });
10359
+ }
10360
+ if (historyEstimate.tokens > availableForHistory) {
8081
10361
  const summarizedMessages = await this.summarizeHistory(
8082
10362
  provider,
8083
10363
  model,
8084
- this.sessionHistory,
10364
+ sessionHistory,
8085
10365
  availableForHistory
8086
10366
  );
8087
10367
  for (const msg of summarizedMessages) {
8088
10368
  messages.push(msg);
8089
10369
  }
8090
10370
  } else {
8091
- for (const msg of this.sessionHistory) {
10371
+ for (const msg of sessionHistory) {
8092
10372
  messages.push(msg);
8093
10373
  }
8094
10374
  }
8095
10375
  } else {
8096
- for (const msg of this.sessionHistory) {
10376
+ for (const msg of sessionHistory) {
8097
10377
  messages.push(msg);
8098
10378
  }
8099
10379
  }
8100
10380
  const schemaPromptMode = options?.schemaPrompt ?? agent2._config.schemaPrompt ?? "json-schema";
8101
- let userContent = prompt;
10381
+ let userContent = input;
8102
10382
  let appendedSchemaText;
8103
10383
  if (options?.schema) {
8104
10384
  const schema = options.schema;
8105
10385
  if (schemaPromptMode === "none") {
8106
10386
  } else if (schemaPromptMode === "json-schema") {
8107
10387
  appendedSchemaText = renderSchemaForPrompt(schema);
8108
- userContent += `
10388
+ const guidance = `
8109
10389
 
8110
10390
  Respond with valid JSON matching this schema:
8111
10391
  ${appendedSchemaText}`;
10392
+ userContent = typeof input === "string" ? input + guidance : [...input, { type: "text", text: guidance }];
8112
10393
  } else {
8113
10394
  appendedSchemaText = typeof schemaPromptMode.render === "function" ? schemaPromptMode.render(schema) : schemaPromptMode.render;
8114
- userContent += `
10395
+ const guidance = `
8115
10396
 
8116
10397
  ${appendedSchemaText}`;
10398
+ userContent = typeof input === "string" ? input + guidance : [...input, { type: "text", text: guidance }];
8117
10399
  }
8118
10400
  }
8119
10401
  messages.push({ role: "user", content: userContent });
@@ -8141,7 +10423,10 @@ ${appendedSchemaText}`;
8141
10423
  }
8142
10424
  const guardrails = agent2._config.guardrails;
8143
10425
  if (guardrails?.input) {
8144
- const inputResult = await guardrails.input(prompt, { metadata: this.metadata });
10426
+ const inputResult = await guardrails.input(prompt, {
10427
+ metadata: this.metadata,
10428
+ input: cloneModelInput(input)
10429
+ });
8145
10430
  this.emitEvent({
8146
10431
  type: "guardrail",
8147
10432
  agent: agent2._name,
@@ -8232,7 +10517,7 @@ ${appendedSchemaText}`;
8232
10517
  includeThoughts: options?.includeThoughts ?? agent2._config.includeThoughts,
8233
10518
  toolChoice: options?.toolChoice ?? agent2._config.toolChoice,
8234
10519
  stop: options?.stop ?? agent2._config.stop,
8235
- providerOptions: options?.providerOptions ?? agent2._config.providerOptions,
10520
+ providerOptions,
8236
10521
  signal: this.currentSignal
8237
10522
  };
8238
10523
  if (options?.schema && toolDefs.length === 0) {
@@ -8258,15 +10543,26 @@ ${appendedSchemaText}`;
8258
10543
  ...chatOptions.stop !== void 0 ? { stop: chatOptions.stop } : {}
8259
10544
  };
8260
10545
  let messagesSnapshot;
10546
+ let messageInputs;
8261
10547
  if (verboseTrace) {
8262
10548
  try {
8263
- messagesSnapshot = structuredClone(currentMessages);
10549
+ messageInputs = currentMessages.flatMap((message, index) => {
10550
+ const descriptor = describeModelInput(message.content);
10551
+ return descriptor ? [{ index, input: descriptor }] : [];
10552
+ });
10553
+ messagesSnapshot = currentMessages.map((message) => ({
10554
+ ...message,
10555
+ content: sanitizeModelInputForTrace(message.content)
10556
+ }));
8264
10557
  } catch (err) {
8265
10558
  console.warn(
8266
10559
  "[axl] verbose trace messages snapshot failed to clone; emitting shallow copy:",
8267
10560
  err instanceof Error ? err.message : String(err)
8268
10561
  );
8269
- messagesSnapshot = [...currentMessages];
10562
+ messagesSnapshot = currentMessages.map((message) => ({
10563
+ ...message,
10564
+ content: sanitizeModelInputForTrace(message.content)
10565
+ }));
8270
10566
  }
8271
10567
  }
8272
10568
  const retryReason = pendingRetryReason;
@@ -8274,16 +10570,18 @@ ${appendedSchemaText}`;
8274
10570
  this.emitEvent({
8275
10571
  type: "agent_call_start",
8276
10572
  agent: agent2._name,
8277
- model: modelUri,
10573
+ model: effectiveModelUri,
8278
10574
  turn: turns,
8279
10575
  data: {
8280
10576
  prompt,
10577
+ ...typeof input === "string" ? {} : { input: describeModelInput(input) },
8281
10578
  ...systemPrompt ? { system: systemPrompt } : {},
8282
10579
  params: callParams,
8283
10580
  turn: turns,
8284
10581
  ...retryReason ? { retryReason } : {},
8285
10582
  ...toolDefs.length > 0 ? { toolNames: toolDefs.map((t) => t.function.name) } : {},
8286
- ...messagesSnapshot ? { messages: messagesSnapshot } : {}
10583
+ ...messagesSnapshot ? { messages: messagesSnapshot } : {},
10584
+ ...messageInputs && messageInputs.length > 0 ? { messageInputs } : {}
8287
10585
  }
8288
10586
  });
8289
10587
  let response;
@@ -8307,7 +10605,10 @@ ${appendedSchemaText}`;
8307
10605
  });
8308
10606
  }
8309
10607
  }) : void 0;
8310
- for await (const chunk of provider.stream(currentMessages, chatOptions)) {
10608
+ for await (const chunk of provider.stream(
10609
+ cloneMessagesForProvider(currentMessages),
10610
+ chatOptions
10611
+ )) {
8311
10612
  if (chunk.type === "text_delta") {
8312
10613
  content2 += chunk.content;
8313
10614
  this.emitEvent({ type: "token", data: chunk.content });
@@ -8373,24 +10674,25 @@ ${appendedSchemaText}`;
8373
10674
  response.thinking_content = thinkingContent;
8374
10675
  }
8375
10676
  } else {
8376
- response = await provider.chat(currentMessages, chatOptions);
10677
+ response = await provider.chat(cloneMessagesForProvider(currentMessages), chatOptions);
8377
10678
  }
8378
10679
  } catch (err) {
10680
+ if (richRequest) this.recordRichFailure(err);
8379
10681
  this.emitEvent({
8380
10682
  type: "agent_call_end",
8381
10683
  agent: agent2._name,
8382
- model: modelUri,
10684
+ model: effectiveModelUri,
8383
10685
  promptVersion: agent2._config.version,
8384
10686
  duration: Date.now() - turnStart,
8385
10687
  data: {
8386
10688
  response: "",
8387
10689
  turn: turns,
8388
10690
  ...retryReason ? { retryReason } : {},
8389
- error: err instanceof Error ? err.message : String(err),
10691
+ error: this._observerErrorProjection(err)?.message ?? legacyErrorMessage(err),
8390
10692
  // Enrich with typed-error metadata when available. `body` is
8391
10693
  // deliberately NOT emitted — it's redaction-eligible (see
8392
10694
  // docs/security.md). `data.error` already carries the message.
8393
- ...err instanceof ProviderError ? { status: err.status, retryable: err.retryable } : {}
10695
+ ...providerErrorEventMetadata(err, richRequest)
8394
10696
  }
8395
10697
  });
8396
10698
  throw err;
@@ -8404,7 +10706,7 @@ ${appendedSchemaText}`;
8404
10706
  this.emitEvent({
8405
10707
  type: "agent_call_end",
8406
10708
  agent: agent2._name,
8407
- model: modelUri,
10709
+ model: effectiveModelUri,
8408
10710
  promptVersion: agent2._config.version,
8409
10711
  cost: response.cost,
8410
10712
  tokens: response.usage ? {
@@ -8438,12 +10740,12 @@ ${appendedSchemaText}`;
8438
10740
  if (descriptor) {
8439
10741
  const mode = descriptor.mode ?? "oneway";
8440
10742
  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 {
10743
+ try {
10744
+ const args = JSON.parse(toolCall.function.arguments);
10745
+ if (typeof args.message === "string" && args.message.length > 0) {
10746
+ handoffPrompt = args.message;
8446
10747
  }
10748
+ } catch {
8447
10749
  }
8448
10750
  const handoffStart = Date.now();
8449
10751
  const sourceFrame = askStorage.getStore();
@@ -8470,15 +10772,27 @@ ${appendedSchemaText}`;
8470
10772
  validateRetries: options.validateRetries
8471
10773
  } : void 0;
8472
10774
  const handoffFn = () => askStorage.run(targetFrame, async () => {
8473
- this.emitEvent({ type: "ask_start", prompt: handoffPrompt });
10775
+ const delegateOriginal = agent2._delegateOriginalInput;
10776
+ const handoffInput = delegateOriginal ?? (typeof input === "string" ? handoffPrompt : input);
10777
+ const enrichedHandoffInput = appendHandoffInstruction(
10778
+ handoffInput,
10779
+ inputText(input),
10780
+ handoffPrompt
10781
+ );
10782
+ this.emitEvent({
10783
+ type: "ask_start",
10784
+ prompt: inputText(enrichedHandoffInput),
10785
+ ...typeof enrichedHandoffInput === "string" ? {} : { input: describeModelInput(enrichedHandoffInput) }
10786
+ });
8474
10787
  const targetAskStart = Date.now();
8475
10788
  try {
8476
10789
  const result = await this.executeAgentCall(
8477
10790
  descriptor.agent,
8478
- handoffPrompt,
10791
+ enrichedHandoffInput,
8479
10792
  handoffOptions,
8480
10793
  currentMessages,
8481
- usageCapture
10794
+ usageCapture,
10795
+ sessionHistory
8482
10796
  );
8483
10797
  this.emitEvent({
8484
10798
  type: "ask_end",
@@ -8489,11 +10803,13 @@ ${appendedSchemaText}`;
8489
10803
  });
8490
10804
  return result;
8491
10805
  } catch (err) {
10806
+ const richHandoff = typeof enrichedHandoffInput !== "string" || hasRichMessage(sessionHistory);
10807
+ if (richHandoff) this.recordRichFailure(err);
8492
10808
  this.emitEvent({
8493
10809
  type: "ask_end",
8494
10810
  outcome: {
8495
10811
  ok: false,
8496
- error: err instanceof Error ? err.message : String(err)
10812
+ error: this._observerErrorProjection(err)?.message ?? legacyErrorMessage(err)
8497
10813
  },
8498
10814
  cost: targetFrame.askCost.value,
8499
10815
  ...targetFrame.askUnpriced ? { unpriced: true } : {},
@@ -9002,12 +11318,12 @@ ${appendedSchemaText}`;
9002
11318
  content: `Summary of earlier conversation:
9003
11319
  ${this.summaryCache}`
9004
11320
  };
9005
- const summaryTokens = estimateTokens(summaryMsg2.content) + 4;
11321
+ const summaryTokens = estimateTokens(summarizeModelInput(summaryMsg2.content)) + 4;
9006
11322
  const remaining = availableTokens - summaryTokens;
9007
11323
  let recentTokens2 = 0;
9008
11324
  let splitIdx2 = history.length;
9009
11325
  for (let i = history.length - 1; i >= 0; i--) {
9010
- const msgTokens = estimateTokens(history[i].content) + 4;
11326
+ const msgTokens = estimateTokens(summarizeModelInput(history[i].content)) + 4;
9011
11327
  if (recentTokens2 + msgTokens > remaining) break;
9012
11328
  recentTokens2 += msgTokens;
9013
11329
  splitIdx2 = i;
@@ -9020,7 +11336,7 @@ ${this.summaryCache}`
9020
11336
  let splitIdx = history.length;
9021
11337
  const targetRecent = Math.floor(availableTokens * 0.6);
9022
11338
  for (let i = history.length - 1; i >= 0; i--) {
9023
- const msgTokens = estimateTokens(history[i].content) + 4;
11339
+ const msgTokens = estimateTokens(summarizeModelInput(history[i].content)) + 4;
9024
11340
  if (recentTokens + msgTokens > targetRecent) break;
9025
11341
  recentTokens += msgTokens;
9026
11342
  splitIdx = i;
@@ -9038,7 +11354,7 @@ ${this.summaryCache}`
9038
11354
  summaryProvider = provider;
9039
11355
  summaryModel = model;
9040
11356
  }
9041
- const oldContent = oldMessages.map((m) => `${m.role}: ${m.content}`).join("\n");
11357
+ const oldContent = oldMessages.map((m) => `${m.role}: ${summarizeModelInput(m.content)}`).join("\n");
9042
11358
  const summaryResponse = await summaryProvider.chat(
9043
11359
  [
9044
11360
  {
@@ -10272,7 +12588,9 @@ ${summaryResponse.content}`
10272
12588
  reason: "routed"
10273
12589
  }
10274
12590
  });
10275
- return this.ask(routerAgent, prompt, {
12591
+ const routerInput = options?.routerInput === "text" ? inputText(prompt) : prompt;
12592
+ routerAgent._delegateOriginalInput = prompt;
12593
+ return this.ask(routerAgent, routerInput, {
10276
12594
  schema: options?.schema,
10277
12595
  schemaPrompt: options?.schemaPrompt,
10278
12596
  nativeStructuredOutput: options?.nativeStructuredOutput,
@@ -10282,6 +12600,278 @@ ${summaryResponse.content}`
10282
12600
  validateRetries: options?.validateRetries
10283
12601
  });
10284
12602
  }
12603
+ /** Transcribe recorded audio through the dedicated transcription registry.
12604
+ * This operation never enters the chat history or chat-provider path. */
12605
+ async transcribe(request) {
12606
+ const startedAt = Date.now();
12607
+ const transcriptionId = (0, import_node_crypto.randomUUID)();
12608
+ const signal = this.currentSignal;
12609
+ const rawAudio = request && typeof request === "object" && "audio" in request ? request.audio : void 0;
12610
+ const source = rawAudio?.type;
12611
+ const requestedUri = typeof request?.model === "string" ? request.model : void 0;
12612
+ const colon = requestedUri?.indexOf(":") ?? -1;
12613
+ const requestedProvider = colon > 0 ? requestedUri.slice(0, colon) : void 0;
12614
+ const requestedModel = colon > 0 && colon < requestedUri.length - 1 ? requestedUri.slice(colon + 1) : void 0;
12615
+ const startAudio = {
12616
+ source: typeof source === "string" ? source : "unknown",
12617
+ ...rawAudio?.data instanceof Uint8Array ? { bytes: rawAudio.data.byteLength } : {},
12618
+ ...typeof rawAudio?.mediaType === "string" ? { mediaType: rawAudio.mediaType } : {}
12619
+ };
12620
+ this.emitEvent({
12621
+ type: "transcription_start",
12622
+ transcriptionId,
12623
+ model: requestedModel,
12624
+ data: {
12625
+ ...requestedProvider ? { provider: requestedProvider } : {},
12626
+ ...requestedModel ? { model: requestedModel } : {},
12627
+ audio: startAudio
12628
+ }
12629
+ });
12630
+ let terminalEmitted = false;
12631
+ const emitTerminal = (partial) => {
12632
+ if (terminalEmitted) return;
12633
+ terminalEmitted = true;
12634
+ this.emitEvent({
12635
+ type: "transcription_end",
12636
+ transcriptionId,
12637
+ model: partial.model,
12638
+ duration: Date.now() - startedAt,
12639
+ ...partial.cost !== void 0 ? { cost: partial.cost } : {},
12640
+ ...partial.tokens ? { tokens: partial.tokens } : {},
12641
+ data: partial.data
12642
+ });
12643
+ };
12644
+ const abortError3 = () => new DOMException("The transcription operation was aborted", "AbortError");
12645
+ const cleanupStatus = (value) => value === "not_required" || value === "deleted" || value === "failed" || value === "timed_out" ? value : void 0;
12646
+ const safeProviderFailure = (error, provider, model) => {
12647
+ const details = error && typeof error === "object" ? error : void 0;
12648
+ const accounting = normalizeTranscriptionAccounting(details?.usage, details?.pricingStatus);
12649
+ const cleanup = cleanupStatus(details?.cleanupStatus);
12650
+ const providerError = error instanceof ProviderError ? error : void 0;
12651
+ const diagnostics = providerError ? {
12652
+ status: providerError.status,
12653
+ retryable: providerError.retryable,
12654
+ ...providerError.retryAfterMs !== void 0 ? { retryAfterMs: providerError.retryAfterMs } : {},
12655
+ ...providerError.requestId ? { requestId: providerError.requestId } : {}
12656
+ } : void 0;
12657
+ return {
12658
+ accounting,
12659
+ cleanup,
12660
+ diagnostics,
12661
+ error: new TranscriptionOperationError({
12662
+ provider,
12663
+ model,
12664
+ ...accounting.usage ? { usage: accounting.usage } : {},
12665
+ ...accounting.pricingStatus ? { pricingStatus: accounting.pricingStatus } : {},
12666
+ ...cleanup ? { cleanupStatus: cleanup } : {},
12667
+ ...diagnostics ?? {},
12668
+ cause: error
12669
+ })
12670
+ };
12671
+ };
12672
+ const terminalData = (status, provider, model, audio, accounting, extras = {}) => ({
12673
+ status,
12674
+ ...provider ? { provider } : {},
12675
+ ...model ? { model } : {},
12676
+ ...audio ? { audio } : {},
12677
+ ...accounting.usage ? { usage: accounting.usage } : {},
12678
+ ...accounting.pricingStatus ? { pricingStatus: accounting.pricingStatus } : {},
12679
+ ...extras
12680
+ });
12681
+ try {
12682
+ const normalized = normalizeTranscriptionRequest(request);
12683
+ const descriptor = {
12684
+ source: normalized.audio.type,
12685
+ ...normalized.audio.type === "bytes" ? { bytes: normalized.audio.data.byteLength } : {},
12686
+ ...normalized.audio.mediaType !== void 0 ? { mediaType: normalized.audio.mediaType } : {}
12687
+ };
12688
+ if (this.currentSignal?.aborted || signal?.aborted) {
12689
+ emitTerminal({
12690
+ model: requestedModel,
12691
+ data: terminalData("aborted", requestedProvider, requestedModel, descriptor, {
12692
+ hasWork: false
12693
+ })
12694
+ });
12695
+ throw abortError3();
12696
+ }
12697
+ if (this.budgetContext?.exceeded) {
12698
+ const { limit, totalCost: spent, policy } = this.budgetContext;
12699
+ if (policy === "warn") {
12700
+ this.emitEvent({
12701
+ type: "log",
12702
+ data: { warning: "Budget exceeded", limit, spent, policy }
12703
+ });
12704
+ } else {
12705
+ throw new BudgetExceededError(limit, spent, policy);
12706
+ }
12707
+ }
12708
+ if (!this.transcriptionProviderRegistry) {
12709
+ throw new InvalidTranscriptionInputError(
12710
+ "No transcription provider registry is configured"
12711
+ );
12712
+ }
12713
+ const resolved = this.transcriptionProviderRegistry.resolve(normalized.model, this.config);
12714
+ if (normalized.audio.type === "provider-file" && normalized.audio.provider !== resolved.providerName) {
12715
+ throw new InvalidTranscriptionInputError(
12716
+ "Transcription provider-file source does not match the requested provider"
12717
+ );
12718
+ }
12719
+ const capabilities = resolved.provider.capabilities(resolved.model);
12720
+ if (!capabilities || !capabilities.sources.includes(normalized.audio.type)) {
12721
+ throw new InvalidTranscriptionInputError(
12722
+ "The requested transcription model does not support this audio source"
12723
+ );
12724
+ }
12725
+ if (normalized.timestamps && !capabilities.timestamps?.includes(normalized.timestamps)) {
12726
+ throw new InvalidTranscriptionInputError(
12727
+ "The requested transcription model does not support requested timestamps"
12728
+ );
12729
+ }
12730
+ if (normalized.diarization && !capabilities.diarization) {
12731
+ throw new InvalidTranscriptionInputError(
12732
+ "The requested transcription model does not support diarization"
12733
+ );
12734
+ }
12735
+ const providerRequest = {
12736
+ ...normalized,
12737
+ model: resolved.model,
12738
+ ...signal ? { signal } : {}
12739
+ };
12740
+ let result;
12741
+ try {
12742
+ result = await resolved.provider.transcribe(providerRequest);
12743
+ } catch (error) {
12744
+ const failure = safeProviderFailure(error, resolved.providerName, resolved.model);
12745
+ if (this.currentSignal?.aborted || signal?.aborted) {
12746
+ if (failure.accounting.cost !== void 0)
12747
+ this._accumulateBudgetCost(failure.accounting.cost);
12748
+ emitTerminal({
12749
+ model: resolved.model,
12750
+ ...failure.accounting.cost !== void 0 ? { cost: failure.accounting.cost } : {},
12751
+ ...failure.accounting.tokens ? { tokens: failure.accounting.tokens } : {},
12752
+ data: terminalData(
12753
+ "aborted",
12754
+ resolved.providerName,
12755
+ resolved.model,
12756
+ descriptor,
12757
+ failure.accounting,
12758
+ ...failure.cleanup ? [{ cleanupStatus: failure.cleanup }] : []
12759
+ )
12760
+ });
12761
+ throw abortError3();
12762
+ }
12763
+ if (failure.accounting.cost !== void 0)
12764
+ this._accumulateBudgetCost(failure.accounting.cost);
12765
+ emitTerminal({
12766
+ model: resolved.model,
12767
+ ...failure.accounting.cost !== void 0 ? { cost: failure.accounting.cost } : {},
12768
+ ...failure.accounting.tokens ? { tokens: failure.accounting.tokens } : {},
12769
+ data: terminalData(
12770
+ "failed",
12771
+ resolved.providerName,
12772
+ resolved.model,
12773
+ descriptor,
12774
+ failure.accounting,
12775
+ {
12776
+ error: "Transcription operation failed",
12777
+ errorCode: failure.error.code,
12778
+ ...failure.diagnostics ? { providerError: failure.diagnostics } : {},
12779
+ ...failure.cleanup ? { cleanupStatus: failure.cleanup } : {}
12780
+ }
12781
+ )
12782
+ });
12783
+ throw failure.error;
12784
+ }
12785
+ const accounting = normalizeTranscriptionAccounting(
12786
+ result.transcript?.usage,
12787
+ result.transcript?.pricingStatus
12788
+ );
12789
+ if (accounting.cost !== void 0) this._accumulateBudgetCost(accounting.cost);
12790
+ if (this.currentSignal?.aborted || signal?.aborted) {
12791
+ emitTerminal({
12792
+ model: resolved.model,
12793
+ ...accounting.cost !== void 0 ? { cost: accounting.cost } : {},
12794
+ ...accounting.tokens ? { tokens: accounting.tokens } : {},
12795
+ data: terminalData(
12796
+ "aborted",
12797
+ resolved.providerName,
12798
+ resolved.model,
12799
+ descriptor,
12800
+ accounting,
12801
+ ...cleanupStatus(result.cleanupStatus) ? [{ cleanupStatus: cleanupStatus(result.cleanupStatus) }] : []
12802
+ )
12803
+ });
12804
+ throw abortError3();
12805
+ }
12806
+ let transcript;
12807
+ try {
12808
+ transcript = cloneTranscript(result.transcript);
12809
+ } catch (error) {
12810
+ const cleanup = cleanupStatus(result.cleanupStatus);
12811
+ const failure = new TranscriptionOperationError({
12812
+ provider: resolved.providerName,
12813
+ model: resolved.model,
12814
+ ...accounting.usage ? { usage: accounting.usage } : {},
12815
+ ...accounting.pricingStatus ? { pricingStatus: accounting.pricingStatus } : {},
12816
+ ...cleanup ? { cleanupStatus: cleanup } : {},
12817
+ cause: error
12818
+ });
12819
+ emitTerminal({
12820
+ model: resolved.model,
12821
+ ...accounting.cost !== void 0 ? { cost: accounting.cost } : {},
12822
+ ...accounting.tokens ? { tokens: accounting.tokens } : {},
12823
+ data: terminalData(
12824
+ "failed",
12825
+ resolved.providerName,
12826
+ resolved.model,
12827
+ descriptor,
12828
+ accounting,
12829
+ {
12830
+ error: "Transcription operation failed",
12831
+ errorCode: "TRANSCRIPTION_PROVIDER_ERROR",
12832
+ ...cleanup ? { cleanupStatus: cleanup } : {}
12833
+ }
12834
+ )
12835
+ });
12836
+ throw failure;
12837
+ }
12838
+ emitTerminal({
12839
+ model: resolved.model,
12840
+ ...accounting.cost !== void 0 ? { cost: accounting.cost } : {},
12841
+ ...accounting.tokens ? { tokens: accounting.tokens } : {},
12842
+ data: terminalData(
12843
+ "completed",
12844
+ resolved.providerName,
12845
+ resolved.model,
12846
+ descriptor,
12847
+ accounting,
12848
+ {
12849
+ text: transcript.text,
12850
+ ...cleanupStatus(result.cleanupStatus) ? { cleanupStatus: cleanupStatus(result.cleanupStatus) } : {}
12851
+ }
12852
+ )
12853
+ });
12854
+ return transcript;
12855
+ } catch (error) {
12856
+ if (!terminalEmitted) {
12857
+ const aborted = signal?.aborted || error instanceof Error && error.name === "AbortError";
12858
+ emitTerminal({
12859
+ model: requestedModel,
12860
+ data: terminalData(
12861
+ aborted ? "aborted" : "failed",
12862
+ requestedProvider,
12863
+ requestedModel,
12864
+ void 0,
12865
+ { hasWork: false },
12866
+ {
12867
+ error: aborted ? "Transcription operation aborted" : "Transcription operation failed"
12868
+ }
12869
+ )
12870
+ });
12871
+ }
12872
+ throw error;
12873
+ }
12874
+ }
10285
12875
  // ── Private ───────────────────────────────────────────────────────────
10286
12876
  /**
10287
12877
  * Internal emitter input — intentionally loose so call sites don't need to
@@ -10297,7 +12887,7 @@ ${summaryResponse.content}`
10297
12887
  const cost = partial.cost;
10298
12888
  if (COST_BEARING_LEAF_TYPES.includes(partial.type)) {
10299
12889
  const usable = isUsableCost(cost);
10300
- const unpriced = !usable && hasPositiveTokens(
12890
+ const unpriced = !usable && hasPositiveBillableWork(
10301
12891
  partial
10302
12892
  );
10303
12893
  if (frame) {
@@ -10402,11 +12992,13 @@ function agent(config) {
10402
12992
  _name: defaultName,
10403
12993
  async ask(prompt, options) {
10404
12994
  const registry = new ProviderRegistry();
12995
+ const transcriptionRegistry = new TranscriptionProviderRegistry();
10405
12996
  const ctx = new WorkflowContext({
10406
12997
  input: prompt,
10407
12998
  executionId: (0, import_node_crypto2.randomUUID)(),
10408
12999
  config: {},
10409
- providerRegistry: registry
13000
+ providerRegistry: registry,
13001
+ transcriptionProviderRegistry: transcriptionRegistry
10410
13002
  });
10411
13003
  return ctx.ask(agentInstance, prompt, options);
10412
13004
  },
@@ -10466,7 +13058,7 @@ var MemoryStore = class {
10466
13058
  return data !== void 0 ? structuredClone(data) : null;
10467
13059
  }
10468
13060
  async saveSession(sessionId, history) {
10469
- this.sessions.set(sessionId, structuredClone(history));
13061
+ this.sessions.set(sessionId, structuredClone(normalizePersistedSessionHistory(history)));
10470
13062
  }
10471
13063
  async getSession(sessionId) {
10472
13064
  const history = this.sessions.get(sessionId);
@@ -10890,7 +13482,7 @@ var SQLiteStore = class {
10890
13482
  const stmt = this.db.prepare(
10891
13483
  "INSERT OR REPLACE INTO sessions (session_id, history) VALUES (?, ?)"
10892
13484
  );
10893
- stmt.run(sessionId, JSON.stringify(history));
13485
+ stmt.run(sessionId, JSON.stringify(normalizePersistedSessionHistory(history)));
10894
13486
  }
10895
13487
  async getSession(sessionId) {
10896
13488
  const stmt = this.db.prepare("SELECT history FROM sessions WHERE session_id = ?");
@@ -11458,7 +14050,7 @@ var AxlStream = class extends import_node_stream.Readable {
11458
14050
  /** Signal an error. `executionId` is required for the same reason as
11459
14051
  * `_done`: terminal events must carry a real id even when the
11460
14052
  * failure happens before any real trace event fires (review S4). */
11461
- _error(error, executionId) {
14053
+ _error(error, executionId, observerMessage = error.message, observerName = error.name) {
11462
14054
  if (this.events.isFinished) return;
11463
14055
  const errorEvent = {
11464
14056
  schemaVersion: 2,
@@ -11466,7 +14058,7 @@ var AxlStream = class extends import_node_stream.Readable {
11466
14058
  executionId,
11467
14059
  step: Number.MAX_SAFE_INTEGER,
11468
14060
  timestamp: Date.now(),
11469
- data: { message: error.message, name: error.name }
14061
+ data: { message: observerMessage, name: observerName }
11470
14062
  };
11471
14063
  this.events._push(errorEvent);
11472
14064
  this.push(errorEvent);
@@ -12193,6 +14785,10 @@ var StreamingFlusher = class {
12193
14785
  function isAbortError4(err) {
12194
14786
  return typeof err === "object" && err !== null && err.name === "AbortError";
12195
14787
  }
14788
+ function executionErrorProjection(ctx, error) {
14789
+ const safe = ctx?._observerErrorProjection(error);
14790
+ return safe ?? { message: error instanceof Error ? error.message : String(error) };
14791
+ }
12196
14792
  function pushEventBounded(execInfo, event, cap) {
12197
14793
  if (STREAMING_EXCLUDED_TYPES.has(event.type)) return;
12198
14794
  if (cap === Infinity || execInfo.events.length < cap) {
@@ -12287,6 +14883,7 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
12287
14883
  tools = /* @__PURE__ */ new Map();
12288
14884
  agents = /* @__PURE__ */ new Map();
12289
14885
  providerRegistry;
14886
+ transcriptionProviderRegistry;
12290
14887
  stateStore;
12291
14888
  executions = /* @__PURE__ */ new Map();
12292
14889
  pendingDecisionResolvers = /* @__PURE__ */ new Map();
@@ -12342,6 +14939,7 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
12342
14939
  super();
12343
14940
  this.config = resolveConfig(config ?? {});
12344
14941
  this.providerRegistry = new ProviderRegistry();
14942
+ this.transcriptionProviderRegistry = new TranscriptionProviderRegistry();
12345
14943
  this.stateStore = this.createStateStore();
12346
14944
  const requested = this.config.state?.maxEventsPerExecution;
12347
14945
  if (requested === void 0) {
@@ -12490,7 +15088,7 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
12490
15088
  execInfo.status = "failed";
12491
15089
  execInfo.completedAt = Date.now();
12492
15090
  execInfo.duration = execInfo.completedAt - execInfo.startedAt;
12493
- execInfo.error = terminalError instanceof Error ? terminalError.message : String(terminalError);
15091
+ execInfo.error = executionErrorProjection(ctx, terminalError).message;
12494
15092
  execInfo.observation = observation;
12495
15093
  ctx._emitWorkflowEnd({
12496
15094
  status: "failed",
@@ -12745,6 +15343,7 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
12745
15343
  metadata: options?.metadata,
12746
15344
  config: this.config,
12747
15345
  providerRegistry: this.providerRegistry,
15346
+ transcriptionProviderRegistry: this.transcriptionProviderRegistry,
12748
15347
  stateStore: this.stateStore,
12749
15348
  mcpManager: this.mcpManager,
12750
15349
  spanManager: this.spanManager,
@@ -12773,6 +15372,10 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
12773
15372
  registerProvider(name, provider) {
12774
15373
  this.providerRegistry.registerInstance(name, provider);
12775
15374
  }
15375
+ /** Register a dedicated transcription adapter. It is never used for chat. */
15376
+ registerTranscriptionProvider(name, provider) {
15377
+ this.transcriptionProviderRegistry.registerInstance(name, provider);
15378
+ }
12776
15379
  /** Resolve a provider:model URI to a Provider instance and model name. */
12777
15380
  resolveProvider(uri) {
12778
15381
  return this.providerRegistry.resolve(uri, this.config);
@@ -12816,6 +15419,7 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
12816
15419
  metadata: options?.metadata,
12817
15420
  config: this.config,
12818
15421
  providerRegistry: this.providerRegistry,
15422
+ transcriptionProviderRegistry: this.transcriptionProviderRegistry,
12819
15423
  sessionHistory,
12820
15424
  signal: controller.signal,
12821
15425
  eventStreamOptions: options?.events,
@@ -12903,6 +15507,7 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
12903
15507
  const executionId = (0, import_node_crypto3.randomUUID)();
12904
15508
  this.abortControllers.set(executionId, controller);
12905
15509
  let execInfo;
15510
+ let wfCtx;
12906
15511
  const run = async () => {
12907
15512
  const workflow2 = this.workflows.get(name);
12908
15513
  if (!workflow2) throw new Error(`Workflow "${name}" not registered`);
@@ -12926,12 +15531,13 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
12926
15531
  metadata: liftPersistedMetadata(options?.metadata)
12927
15532
  };
12928
15533
  this.executions.set(executionId, execInfo);
12929
- const wfCtx = new WorkflowContext({
15534
+ const context = new WorkflowContext({
12930
15535
  input: validated,
12931
15536
  executionId,
12932
15537
  metadata: options?.metadata,
12933
15538
  config: this.config,
12934
15539
  providerRegistry: this.providerRegistry,
15540
+ transcriptionProviderRegistry: this.transcriptionProviderRegistry,
12935
15541
  sessionHistory,
12936
15542
  signal: controller.signal,
12937
15543
  eventStreamOptions: options?.events,
@@ -12996,6 +15602,7 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
12996
15602
  unpricedWarned: false
12997
15603
  }
12998
15604
  });
15605
+ wfCtx = context;
12999
15606
  return this.spanManager.withSpanAsync(
13000
15607
  "axl.workflow.execute",
13001
15608
  {
@@ -13005,7 +15612,7 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
13005
15612
  },
13006
15613
  (span) => this.runWorkflowBody({
13007
15614
  workflow: workflow2,
13008
- ctx: wfCtx,
15615
+ ctx: context,
13009
15616
  execInfo,
13010
15617
  validated,
13011
15618
  span,
@@ -13018,12 +15625,14 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
13018
15625
  execInfo.status = "failed";
13019
15626
  execInfo.completedAt = Date.now();
13020
15627
  execInfo.duration = execInfo.completedAt - execInfo.startedAt;
13021
- execInfo.error = err instanceof Error ? err.message : String(err);
15628
+ execInfo.error = executionErrorProjection(wfCtx, err).message;
13022
15629
  this.persistExecution(execInfo);
13023
15630
  }
13024
15631
  this.abortControllers.delete(executionId);
13025
15632
  cleanupAbortForwarder();
13026
- axlStream._error(err instanceof Error ? err : new Error(String(err)), executionId);
15633
+ const originalError = err instanceof Error ? err : new Error(String(err));
15634
+ const projection = executionErrorProjection(wfCtx, err);
15635
+ axlStream._error(originalError, executionId, projection.message, projection.name);
13027
15636
  });
13028
15637
  return axlStream;
13029
15638
  }
@@ -13434,7 +16043,7 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
13434
16043
  },
13435
16044
  {
13436
16045
  role: "user",
13437
- content: messages.map((m) => `${m.role}: ${m.content}`).join("\n")
16046
+ content: messages.map((m) => `${m.role}: ${summarizeModelInput(m.content)}`).join("\n")
13438
16047
  }
13439
16048
  ],
13440
16049
  { model, maxTokens: 1024 }
@@ -13881,7 +16490,7 @@ async function registerConditions(conditions) {
13881
16490
  ].join("\n");
13882
16491
  nodeModule.register(`data:text/javascript,${encodeURIComponent(hookCode)}`);
13883
16492
  } catch {
13884
- console.warn("Warning: --conditions requires Node.js 20.6+");
16493
+ console.warn("Warning: this Node.js runtime does not support --conditions");
13885
16494
  }
13886
16495
  }
13887
16496
 
@@ -14190,7 +16799,11 @@ var RedisStore = class _RedisStore {
14190
16799
  async saveSession(sessionId, history) {
14191
16800
  const ttl = this.ttlFor("session");
14192
16801
  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();
16802
+ await this.client.multi().set(
16803
+ this.sessionKey(sessionId),
16804
+ JSON.stringify(normalizePersistedSessionHistory(history)),
16805
+ setOptions
16806
+ ).sAdd(this.sessionIdsKey(), sessionId).exec();
14194
16807
  }
14195
16808
  async getSession(sessionId) {
14196
16809
  const raw = await this.client.get(this.sessionKey(sessionId));
@@ -14679,10 +17292,15 @@ function cosineSimilarity2(a, b) {
14679
17292
  EventStreamOverflowError,
14680
17293
  GROQ_PROFILE,
14681
17294
  GeminiProvider,
17295
+ GeminiTranscriptionProvider,
14682
17296
  GuardrailError,
14683
17297
  InMemoryVectorStore,
17298
+ InvalidModelInputError,
17299
+ InvalidTranscriptionInputError,
14684
17300
  LLAMACPP_PROFILE,
14685
17301
  LMSTUDIO_PROFILE,
17302
+ MAX_INLINE_MODEL_INPUT_BYTES,
17303
+ MAX_INLINE_TRANSCRIPTION_BYTES,
14686
17304
  MISTRAL_PROFILE,
14687
17305
  MaxTurnsError,
14688
17306
  McpManager,
@@ -14698,6 +17316,8 @@ function cosineSimilarity2(a, b) {
14698
17316
  OpenAIEmbedder,
14699
17317
  OpenAIProvider,
14700
17318
  OpenAIResponsesProvider,
17319
+ OpenAITranscriptionProvider,
17320
+ OpenRouterTranscriptionProvider,
14701
17321
  ProviderError,
14702
17322
  ProviderRegistry,
14703
17323
  QuorumNotMet,
@@ -14713,7 +17333,11 @@ function cosineSimilarity2(a, b) {
14713
17333
  TimeoutError,
14714
17334
  ToolFailure,
14715
17335
  ToolModelOutputError,
17336
+ TranscriptionOperationError,
17337
+ TranscriptionProviderRegistry,
14716
17338
  UnsupportedEventSchemaVersionError,
17339
+ UnsupportedModelInputError,
17340
+ UnsupportedTranscriptionInputError,
14717
17341
  VLLM_PROFILE,
14718
17342
  ValidationError,
14719
17343
  VerifyError,
@@ -14730,8 +17354,10 @@ function cosineSimilarity2(a, b) {
14730
17354
  findConfig,
14731
17355
  getEventSchemaVersion,
14732
17356
  getExecutionEventSchemaVersion,
17357
+ hasPositiveBillableWork,
14733
17358
  hasPositiveTokens,
14734
17359
  importModule,
17360
+ inputText,
14735
17361
  isCostBearingLeaf,
14736
17362
  isRetryableStatus,
14737
17363
  isRootLevel,