@axlsdk/axl 0.18.2 → 0.19.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
@@ -111,57 +111,88 @@ var init_span_manager = __esm({
111
111
  var index_exports = {};
112
112
  __export(index_exports, {
113
113
  AXL_EVENT_TYPES: () => AXL_EVENT_TYPES,
114
+ AZURE_PROFILE: () => AZURE_PROFILE,
114
115
  AnthropicProvider: () => AnthropicProvider,
115
116
  AxlError: () => AxlError,
116
117
  AxlEventBus: () => AxlEventBus,
117
118
  AxlRuntime: () => AxlRuntime,
118
119
  AxlStream: () => AxlStream,
120
+ BEDROCK_PROFILE: () => BEDROCK_PROFILE,
121
+ BUILTIN_PROFILES: () => BUILTIN_PROFILES,
119
122
  BudgetExceededError: () => BudgetExceededError,
120
123
  CONFIG_CANDIDATES: () => CONFIG_CANDIDATES,
121
124
  COST_BEARING_LEAF_TYPES: () => COST_BEARING_LEAF_TYPES,
125
+ DEEPSEEK_PROFILE: () => DEEPSEEK_PROFILE,
122
126
  EventStreamOverflowError: () => EventStreamOverflowError,
127
+ GROQ_PROFILE: () => GROQ_PROFILE,
123
128
  GeminiProvider: () => GeminiProvider,
124
129
  GuardrailError: () => GuardrailError,
125
130
  InMemoryVectorStore: () => InMemoryVectorStore,
131
+ LLAMACPP_PROFILE: () => LLAMACPP_PROFILE,
132
+ LMSTUDIO_PROFILE: () => LMSTUDIO_PROFILE,
133
+ MISTRAL_PROFILE: () => MISTRAL_PROFILE,
126
134
  MaxTurnsError: () => MaxTurnsError,
127
135
  McpManager: () => McpManager,
128
136
  MemoryManager: () => MemoryManager,
129
137
  MemoryStore: () => MemoryStore,
130
138
  NoConsensus: () => NoConsensus,
131
139
  NoopSpanManager: () => NoopSpanManager,
140
+ OLLAMA_PROFILE: () => OLLAMA_PROFILE,
141
+ OPENAI_PRICING: () => OPENAI_PRICING,
142
+ OPENAI_PROFILE: () => OPENAI_PROFILE,
143
+ OPENROUTER_PROFILE: () => OPENROUTER_PROFILE,
144
+ OpenAICompatibleProvider: () => OpenAICompatibleProvider,
132
145
  OpenAIEmbedder: () => OpenAIEmbedder,
133
146
  OpenAIProvider: () => OpenAIProvider,
134
147
  OpenAIResponsesProvider: () => OpenAIResponsesProvider,
148
+ ProviderError: () => ProviderError,
135
149
  ProviderRegistry: () => ProviderRegistry,
136
150
  QuorumNotMet: () => QuorumNotMet,
137
151
  REDACTED: () => REDACTED,
138
152
  REDACTION_RULES: () => REDACTION_RULES,
139
153
  RateLimiter: () => RateLimiter,
140
154
  RedisStore: () => RedisStore,
155
+ SGLANG_PROFILE: () => SGLANG_PROFILE,
141
156
  SQLiteStore: () => SQLiteStore,
142
157
  Session: () => Session,
143
158
  SqliteVectorStore: () => SqliteVectorStore,
159
+ ThinkTagScanner: () => ThinkTagScanner,
144
160
  TimeoutError: () => TimeoutError,
145
161
  ToolDenied: () => ToolDenied,
162
+ ToolModelOutputError: () => ToolModelOutputError,
163
+ VLLM_PROFILE: () => VLLM_PROFILE,
146
164
  ValidationError: () => ValidationError,
147
165
  VerifyError: () => VerifyError,
148
166
  WorkflowContext: () => WorkflowContext,
167
+ XAI_PROFILE: () => XAI_PROFILE,
149
168
  agent: () => agent,
150
169
  createSpanManager: () => createSpanManager,
151
170
  defineConfig: () => defineConfig,
171
+ estimateOpenAICost: () => estimateOpenAICost,
152
172
  eventCostContribution: () => eventCostContribution,
153
173
  expandGlob: () => expandGlob,
154
174
  extractJson: () => extractJson,
175
+ extractThinkTags: () => extractThinkTags,
155
176
  findConfig: () => findConfig,
177
+ hasPositiveTokens: () => hasPositiveTokens,
156
178
  importModule: () => importModule,
157
179
  isCostBearingLeaf: () => isCostBearingLeaf,
180
+ isRetryableStatus: () => isRetryableStatus,
158
181
  isRootLevel: () => isRootLevel,
182
+ isUnpricedLeaf: () => isUnpricedLeaf,
183
+ isUsableCost: () => isUsableCost,
159
184
  needsTsxLoader: () => needsTsxLoader,
185
+ openaiReasoningEmit: () => openaiReasoningEmit,
160
186
  parsePartialJson: () => parsePartialJson,
161
187
  pickDefault: () => pickDefault,
162
188
  pickExport: () => pickExport,
189
+ priceFromTable: () => priceFromTable,
190
+ reasoningEffortEmit: () => reasoningEffortEmit,
191
+ reasoningObjectEmit: () => reasoningObjectEmit,
163
192
  redactEvent: () => redactEvent,
164
193
  registerConditions: () => registerConditions,
194
+ resolveApiKey: () => resolveApiKey,
195
+ resolvePerModel: () => resolvePerModel,
165
196
  resolveRuntime: () => resolveRuntime,
166
197
  resolveThinkingOptions: () => resolveThinkingOptions,
167
198
  stringStreamFromEvents: () => stringStreamFromEvents,
@@ -242,6 +273,7 @@ function tool(config) {
242
273
  retry: retryPolicy,
243
274
  requireApproval: config.requireApproval ?? false,
244
275
  hooks: config.hooks,
276
+ ...config.toModelOutput !== void 0 ? { toModelOutput: config.toModelOutput } : {},
245
277
  async run(ctx, input) {
246
278
  const startTime = Date.now();
247
279
  try {
@@ -289,11 +321,229 @@ function resolveThinkingOptions(options) {
289
321
  hasBudgetOverride
290
322
  };
291
323
  }
324
+ async function resolveApiKey(source) {
325
+ if (typeof source === "function") return await source() ?? "";
326
+ return source ?? "";
327
+ }
328
+
329
+ // src/errors.ts
330
+ var AxlError = class extends Error {
331
+ code;
332
+ constructor(code, message) {
333
+ super(message);
334
+ this.name = "AxlError";
335
+ this.code = code;
336
+ }
337
+ };
338
+ var ToolModelOutputError = class extends AxlError {
339
+ toolName;
340
+ cause;
341
+ constructor(toolName, cause) {
342
+ super("TOOL_MODEL_OUTPUT_ERROR", `Failed to prepare model output for tool "${toolName}"`);
343
+ this.name = "ToolModelOutputError";
344
+ this.toolName = toolName;
345
+ this.cause = cause;
346
+ Object.defineProperty(this, "cause", { enumerable: false });
347
+ }
348
+ };
349
+ var VerifyError = class extends AxlError {
350
+ lastOutput;
351
+ zodError;
352
+ retries;
353
+ constructor(lastOutput, zodError, retries) {
354
+ super("VERIFY_ERROR", `Schema validation failed after ${retries} retries: ${zodError.message}`);
355
+ this.name = "VerifyError";
356
+ this.lastOutput = lastOutput;
357
+ this.zodError = zodError;
358
+ this.retries = retries;
359
+ }
360
+ };
361
+ var QuorumNotMet = class extends AxlError {
362
+ results;
363
+ constructor(required, actual, results) {
364
+ super("QUORUM_NOT_MET", `Quorum not met: needed ${required} successes, got ${actual}`);
365
+ this.name = "QuorumNotMet";
366
+ this.results = results;
367
+ }
368
+ };
369
+ var NoConsensus = class extends AxlError {
370
+ constructor(reason) {
371
+ super("NO_CONSENSUS", `No consensus: ${reason}`);
372
+ this.name = "NoConsensus";
373
+ }
374
+ };
375
+ var TimeoutError = class extends AxlError {
376
+ constructor(operation, timeoutMs) {
377
+ super("TIMEOUT", `${operation} exceeded timeout of ${timeoutMs}ms`);
378
+ this.name = "TimeoutError";
379
+ }
380
+ };
381
+ function formatBudgetCost(cost) {
382
+ if (Number.isNaN(cost)) return "$NaN";
383
+ if (cost === Infinity) return "$Infinity";
384
+ if (cost === -Infinity) return "-$Infinity";
385
+ if (cost === 0) return "$0.00";
386
+ const sign = cost < 0 ? "-" : "";
387
+ const abs = Math.abs(cost);
388
+ if (abs < 1e-6) return `${sign}< $0.000001`;
389
+ if (abs < 1e-4) {
390
+ const [mantissa, exponent] = abs.toExponential(2).split("e");
391
+ return `${sign}$${mantissa}e${parseInt(exponent, 10)}`;
392
+ }
393
+ if (abs < 0.01) return `${sign}$${abs.toFixed(6)}`;
394
+ return `${sign}$${abs.toFixed(2)}`;
395
+ }
396
+ var BudgetExceededError = class extends AxlError {
397
+ limit;
398
+ spent;
399
+ policy;
400
+ constructor(limit, spent, policy) {
401
+ super(
402
+ "BUDGET_EXCEEDED",
403
+ `Budget exceeded: spent ${formatBudgetCost(spent)} of ${formatBudgetCost(
404
+ limit
405
+ )} limit (policy: ${policy})`
406
+ );
407
+ this.name = "BudgetExceededError";
408
+ this.limit = limit;
409
+ this.spent = spent;
410
+ this.policy = policy;
411
+ }
412
+ };
413
+ var MaxTurnsError = class extends AxlError {
414
+ maxTurns;
415
+ constructor(operation, maxTurns) {
416
+ super("MAX_TURNS", `${operation} exceeded maximum of ${maxTurns} turns`);
417
+ this.name = "MaxTurnsError";
418
+ this.maxTurns = maxTurns;
419
+ }
420
+ };
421
+ var GuardrailError = class extends AxlError {
422
+ guardrailType;
423
+ reason;
424
+ constructor(guardrailType, reason) {
425
+ super("GUARDRAIL_BLOCKED", `${guardrailType} guardrail blocked: ${reason}`);
426
+ this.name = "GuardrailError";
427
+ this.guardrailType = guardrailType;
428
+ this.reason = reason;
429
+ }
430
+ };
431
+ var ValidationError = class extends AxlError {
432
+ lastOutput;
433
+ reason;
434
+ retries;
435
+ constructor(lastOutput, reason, retries) {
436
+ super("VALIDATION_ERROR", `Validation failed after ${retries} retries: ${reason}`);
437
+ this.name = "ValidationError";
438
+ this.lastOutput = lastOutput;
439
+ this.reason = reason;
440
+ this.retries = retries;
441
+ }
442
+ };
443
+ var ToolDenied = class extends AxlError {
444
+ toolName;
445
+ agentName;
446
+ constructor(toolName, agentName) {
447
+ super(
448
+ "TOOL_DENIED",
449
+ `Agent "${agentName}" attempted to call tool "${toolName}" which is not in its ACL`
450
+ );
451
+ this.name = "ToolDenied";
452
+ this.toolName = toolName;
453
+ this.agentName = agentName;
454
+ }
455
+ };
456
+
457
+ // src/providers/errors.ts
458
+ var ProviderError = class extends AxlError {
459
+ /** Adapter/profile name, e.g. `'openai'`, `'anthropic'`, `'google'`. */
460
+ provider;
461
+ /** HTTP status code; `0` for non-HTTP (network) failures. */
462
+ status;
463
+ /** Semantic failover hint (see module doc) — NOT the transport-retry set. */
464
+ retryable;
465
+ /** Parsed `Retry-After` delay in ms (RAW/unclamped), when the header was present. */
466
+ retryAfterMs;
467
+ /** Provider request id from response headers, when present. */
468
+ requestId;
469
+ /**
470
+ * Raw provider error body. Lives ONLY on the error object — it is
471
+ * intentionally never placed on the event stream (it can echo prompt text
472
+ * and is redaction-eligible). See `docs/security.md`.
473
+ */
474
+ body;
475
+ constructor(args) {
476
+ super("PROVIDER_ERROR", args.message);
477
+ this.name = "ProviderError";
478
+ this.provider = args.provider;
479
+ this.status = args.status;
480
+ this.retryable = args.retryable;
481
+ this.retryAfterMs = args.retryAfterMs;
482
+ this.requestId = args.requestId;
483
+ this.body = args.body;
484
+ }
485
+ };
486
+ function isRetryableStatus(status) {
487
+ switch (status) {
488
+ case 0:
489
+ return true;
490
+ case 408:
491
+ // request timeout
492
+ case 429:
493
+ // rate limit
494
+ case 500:
495
+ case 502:
496
+ case 503:
497
+ case 504:
498
+ case 529:
499
+ return true;
500
+ // Explicitly non-retryable (bad request / auth / not found / payload):
501
+ // 400, 401, 403, 404, 413, 422 fall through to the default.
502
+ default:
503
+ return false;
504
+ }
505
+ }
506
+ function parseRetryAfter(headers) {
507
+ const raw = headers.get("retry-after");
508
+ if (!raw) return void 0;
509
+ const trimmed = raw.trim();
510
+ if (/^\d+(\.\d+)?$/.test(trimmed)) {
511
+ const ms = Number(trimmed) * 1e3;
512
+ return ms > 0 ? ms : void 0;
513
+ }
514
+ const dateMs = Date.parse(trimmed);
515
+ if (Number.isNaN(dateMs)) return void 0;
516
+ const delta = dateMs - Date.now();
517
+ return delta > 0 ? delta : void 0;
518
+ }
519
+ var REQUEST_ID_HEADERS = ["x-request-id", "request-id"];
520
+ function extractRequestId(headers) {
521
+ for (const name of REQUEST_ID_HEADERS) {
522
+ const value = headers.get(name);
523
+ if (value) return value;
524
+ }
525
+ return void 0;
526
+ }
527
+ function buildProviderError(args) {
528
+ return new ProviderError({
529
+ provider: args.provider,
530
+ status: args.status,
531
+ retryable: isRetryableStatus(args.status),
532
+ message: args.message,
533
+ retryAfterMs: args.headers ? parseRetryAfter(args.headers) : void 0,
534
+ requestId: args.headers ? extractRequestId(args.headers) : void 0,
535
+ body: args.body
536
+ });
537
+ }
292
538
 
293
539
  // src/providers/retry.ts
294
540
  var RETRYABLE_STATUS_CODES = /* @__PURE__ */ new Set([429, 503, 529]);
295
541
  var MAX_RETRIES = 2;
296
542
  var BASE_DELAY_MS = 1e3;
543
+ var MAX_BACKOFF_MS = 6e4;
544
+ function jitter(ms) {
545
+ return ms * (0.75 + Math.random() * 0.5);
546
+ }
297
547
  function sleep2(ms, signal) {
298
548
  return new Promise((resolve2) => {
299
549
  if (signal?.aborted) {
@@ -311,9 +561,14 @@ function sleep2(ms, signal) {
311
561
  );
312
562
  });
313
563
  }
564
+ function isAbortError(err, signal) {
565
+ if (signal?.aborted) return true;
566
+ return typeof err === "object" && err !== null && "name" in err && err.name === "AbortError";
567
+ }
314
568
  async function fetchWithRetry(input, init, opts) {
315
569
  const maxRetries = opts?.maxRetries ?? MAX_RETRIES;
316
570
  const governor = opts?.governor;
571
+ const provider = opts?.provider ?? "unknown";
317
572
  let acquired = false;
318
573
  if (governor) {
319
574
  await governor.acquire(init?.signal ?? void 0);
@@ -321,7 +576,21 @@ async function fetchWithRetry(input, init, opts) {
321
576
  }
322
577
  try {
323
578
  for (let attempt = 0; ; attempt++) {
324
- const res = await fetch(input, init);
579
+ let res;
580
+ try {
581
+ res = await fetch(input, init);
582
+ } catch (err) {
583
+ if (isAbortError(err, init?.signal ?? void 0)) throw err;
584
+ if (attempt >= maxRetries) {
585
+ throw buildProviderError({
586
+ provider,
587
+ status: 0,
588
+ message: err instanceof Error ? err.message : String(err)
589
+ });
590
+ }
591
+ await sleep2(jitter(BASE_DELAY_MS * 2 ** attempt), init?.signal ?? void 0);
592
+ continue;
593
+ }
325
594
  governor?.observe(res);
326
595
  if (res.ok || !RETRYABLE_STATUS_CODES.has(res.status) || attempt >= maxRetries) {
327
596
  return res;
@@ -329,15 +598,9 @@ async function fetchWithRetry(input, init, opts) {
329
598
  if (init?.signal?.aborted) {
330
599
  return res;
331
600
  }
332
- const retryAfter = res.headers.get("retry-after");
333
- let delay;
334
- if (retryAfter && !isNaN(Number(retryAfter))) {
335
- delay = Number(retryAfter) * 1e3;
336
- } else {
337
- delay = BASE_DELAY_MS * 2 ** attempt;
338
- }
339
- delay *= 0.75 + Math.random() * 0.5;
340
- await sleep2(delay, init?.signal ?? void 0);
601
+ const retryAfterMs = parseRetryAfter(res.headers);
602
+ const baseDelay = retryAfterMs !== void 0 ? Math.min(retryAfterMs, MAX_BACKOFF_MS) : BASE_DELAY_MS * 2 ** attempt;
603
+ await sleep2(jitter(baseDelay), init?.signal ?? void 0);
341
604
  }
342
605
  } finally {
343
606
  if (acquired) governor.release();
@@ -485,254 +748,467 @@ var RateLimiter = class {
485
748
  }
486
749
  };
487
750
 
488
- // src/providers/openai.ts
489
- var OPENAI_PRICING = {
490
- // gpt-4o era — cache reads at 50% of input rate
491
- "gpt-4o": [25e-7, 1e-5, 0.5],
492
- "gpt-4o-mini": [15e-8, 6e-7, 0.5],
493
- "gpt-4-turbo": [1e-5, 3e-5, 0.5],
494
- "gpt-4": [3e-5, 6e-5, 0.5],
495
- "gpt-3.5-turbo": [5e-7, 15e-7, 0.5],
496
- o1: [15e-6, 6e-5, 0.5],
497
- "o1-mini": [3e-6, 12e-6, 0.5],
498
- "o1-pro": [15e-5, 6e-4, 0.5],
499
- // gpt-4.1 / o3 / o4 era — cache reads at 25% of input rate
500
- "gpt-4.1": [2e-6, 8e-6, 0.25],
501
- "gpt-4.1-mini": [4e-7, 16e-7, 0.25],
502
- "gpt-4.1-nano": [1e-7, 4e-7, 0.25],
503
- o3: [1e-5, 4e-5, 0.25],
504
- "o3-mini": [11e-7, 44e-7, 0.25],
505
- "o3-pro": [2e-5, 8e-5, 0.25],
506
- "o4-mini": [11e-7, 44e-7, 0.25],
507
- // gpt-5 era — cache reads at 10% of input rate
508
- "gpt-5": [125e-8, 1e-5, 0.1],
509
- "gpt-5-mini": [25e-8, 2e-6, 0.1],
510
- "gpt-5-nano": [5e-8, 4e-7, 0.1],
511
- "gpt-5.1": [125e-8, 1e-5, 0.1],
512
- "gpt-5.2": [175e-8, 14e-6, 0.1],
513
- "gpt-5.3": [175e-8, 14e-6, 0.1],
514
- "gpt-5.4": [25e-7, 15e-6, 0.1],
515
- "gpt-5.4-pro": [3e-5, 18e-5, 0.1],
516
- // gpt-5.5 era (flagship released 2026-04) — cache reads at 10% of input rate
517
- "gpt-5.5": [5e-6, 3e-5, 0.1],
518
- "gpt-5.5-pro": [3e-5, 18e-5, 0.1]
519
- };
520
- var PRICING_KEYS_BY_LENGTH = Object.keys(OPENAI_PRICING).sort((a, b) => b.length - a.length);
521
- function estimateOpenAICost(model, promptTokens, completionTokens, cachedTokens) {
522
- let pricing = OPENAI_PRICING[model];
751
+ // src/providers/openai-compatible.ts
752
+ function resolvePerModel(value, model, fallback) {
753
+ if (value === void 0) return fallback;
754
+ return typeof value === "function" ? value(model) : value;
755
+ }
756
+ var SORTED_KEYS_CACHE = /* @__PURE__ */ new WeakMap();
757
+ function sortedKeys(table) {
758
+ let keys = SORTED_KEYS_CACHE.get(table);
759
+ if (!keys) {
760
+ keys = Object.keys(table).sort((a, b) => b.length - a.length);
761
+ SORTED_KEYS_CACHE.set(table, keys);
762
+ }
763
+ return keys;
764
+ }
765
+ function priceFromTable(table, model, promptTokens, completionTokens, cachedTokens) {
766
+ let pricing = table[model];
523
767
  if (!pricing) {
524
- for (const key of PRICING_KEYS_BY_LENGTH) {
768
+ for (const key of sortedKeys(table)) {
525
769
  if (model.startsWith(key)) {
526
- pricing = OPENAI_PRICING[key];
770
+ pricing = table[key];
527
771
  break;
528
772
  }
529
773
  }
530
774
  }
531
- if (!pricing) return 0;
775
+ if (!pricing) return void 0;
532
776
  const [inputRate, outputRate, cacheMultiplier] = pricing;
533
777
  const cached = cachedTokens ?? 0;
534
778
  const inputCost = (promptTokens - cached) * inputRate + cached * inputRate * cacheMultiplier;
535
779
  return inputCost + completionTokens * outputRate;
536
780
  }
537
- function isOSeriesModel(model) {
538
- return /^(o1|o3|o4-mini)/.test(model);
781
+ var THINK_OPEN = "<think>";
782
+ var THINK_CLOSE = "</think>";
783
+ function partialTagSuffixLen(s, tag) {
784
+ const max = Math.min(s.length, tag.length - 1);
785
+ for (let n = max; n > 0; n--) {
786
+ if (tag.startsWith(s.slice(s.length - n))) return n;
787
+ }
788
+ return 0;
539
789
  }
540
- function supportsReasoningEffort(model) {
541
- return isOSeriesModel(model) || /^gpt-5/.test(model);
542
- }
543
- function supportsReasoningNone(model) {
544
- return /^gpt-5\.[1-9]/.test(model);
790
+ var ThinkTagScanner = class {
791
+ buffer = "";
792
+ inside = false;
793
+ push(chunk) {
794
+ this.buffer += chunk;
795
+ let text = "";
796
+ let thinking = "";
797
+ for (; ; ) {
798
+ const tag = this.inside ? THINK_CLOSE : THINK_OPEN;
799
+ const idx = this.buffer.indexOf(tag);
800
+ if (idx === -1) {
801
+ const hold = partialTagSuffixLen(this.buffer, tag);
802
+ const emit = this.buffer.slice(0, this.buffer.length - hold);
803
+ if (this.inside) thinking += emit;
804
+ else text += emit;
805
+ this.buffer = this.buffer.slice(this.buffer.length - hold);
806
+ break;
807
+ }
808
+ const before = this.buffer.slice(0, idx);
809
+ if (this.inside) thinking += before;
810
+ else text += before;
811
+ this.buffer = this.buffer.slice(idx + tag.length);
812
+ this.inside = !this.inside;
813
+ }
814
+ return { text, thinking };
815
+ }
816
+ /** Flush any residual buffer at stream end (treat as the current channel). */
817
+ flush() {
818
+ const rest = this.buffer;
819
+ this.buffer = "";
820
+ return this.inside ? { text: "", thinking: rest } : { text: rest, thinking: "" };
821
+ }
822
+ };
823
+ function extractThinkTags(content) {
824
+ const scanner = new ThinkTagScanner();
825
+ const a = scanner.push(content);
826
+ const b = scanner.flush();
827
+ return { content: a.text + b.text, thinking: a.thinking + b.thinking };
545
828
  }
546
- function supportsXhigh(model) {
547
- return /^gpt-5\.([2-9]|\d{2,})/.test(model);
829
+ function reasoningEffortEmit(map) {
830
+ return (body, resolved, model) => {
831
+ const value = map(resolved, model);
832
+ if (value) body.reasoning_effort = value;
833
+ };
548
834
  }
549
- function clampReasoningEffort(model, effort) {
550
- if (model.startsWith("gpt-5-pro")) return "high";
551
- if (effort === "none" && !supportsReasoningNone(model)) return "minimal";
552
- if (effort === "xhigh" && !supportsXhigh(model)) return "high";
553
- return effort;
835
+ function reasoningObjectEmit(mapEffort) {
836
+ return (body, resolved, model) => {
837
+ if (resolved.hasBudgetOverride) {
838
+ body.reasoning = { max_tokens: resolved.thinkingBudget };
839
+ } else if (resolved.activeEffort) {
840
+ body.reasoning = { effort: mapEffort(resolved.activeEffort, model) };
841
+ } else if (resolved.thinkingDisabled) {
842
+ body.reasoning = { enabled: false };
843
+ }
844
+ };
554
845
  }
555
- function effortToReasoningEffort(effort) {
556
- return effort === "max" ? "xhigh" : effort;
846
+ var ROUND_TRIP_FIELDS = ["reasoning_content", "reasoning", "reasoning_details"];
847
+ function isRoundTripField(field) {
848
+ return typeof field === "string" && ROUND_TRIP_FIELDS.includes(field);
557
849
  }
558
- function budgetToReasoningEffort(budget) {
559
- if (budget <= 1024) return "low";
560
- if (budget <= 8192) return "medium";
561
- return "high";
850
+ function normalizeMessageContent(content) {
851
+ if (typeof content === "string") return content;
852
+ if (content == null) return "";
853
+ if (!Array.isArray(content)) return "";
854
+ return content.map((part) => {
855
+ if (typeof part === "string") return part;
856
+ if (!part || typeof part !== "object") return "";
857
+ if (typeof part.text === "string") return part.text;
858
+ if (typeof part.content === "string") return part.content;
859
+ return "";
860
+ }).join("");
562
861
  }
563
- var OpenAIProvider = class {
564
- name = "openai";
862
+ var OpenAICompatibleProvider = class {
863
+ name;
864
+ /** `json_schema` is honored natively when the profile's `supportsJsonSchema`
865
+ * capability is true for this model (the default); otherwise the engine
866
+ * downgrades the request to plain `json_object` (see `buildRequestBody`),
867
+ * which does NOT enforce the schema shape. */
868
+ nativeStructuredOutputSupport(model) {
869
+ const supportsSchema = resolvePerModel(
870
+ this.profile.capabilities?.supportsJsonSchema,
871
+ model,
872
+ true
873
+ );
874
+ return supportsSchema ? "schema" : "downgraded";
875
+ }
876
+ profile;
565
877
  baseUrl;
566
- apiKey;
878
+ /** A key string, or a resolver invoked per request (expiring tokens). */
879
+ apiKeySource;
880
+ authHeader;
567
881
  governor;
568
- constructor(options = {}) {
569
- this.apiKey = options.apiKey ?? process.env.OPENAI_API_KEY ?? "";
570
- this.baseUrl = (options.baseUrl ?? process.env.OPENAI_BASE_URL ?? "https://api.openai.com/v1").replace(/\/$/, "");
882
+ constructor(options) {
883
+ const p = options.profile;
884
+ this.profile = p;
885
+ this.name = p.name;
886
+ this.apiKeySource = options.apiKey ?? (p.envApiKey ? process.env[p.envApiKey] : void 0) ?? "";
887
+ this.authHeader = options.authHeader;
888
+ const envBase = p.envBaseUrl ? process.env[p.envBaseUrl] : void 0;
889
+ const explicitBase = options.baseUrl ?? envBase;
890
+ this.baseUrl = (explicitBase ?? p.defaultBaseUrl).replace(/\/$/, "");
571
891
  this.governor = options.rateLimit ? new RateLimiter(options.rateLimit) : void 0;
572
- if (!this.apiKey) {
573
- throw new Error("OpenAI API key is required. Set OPENAI_API_KEY or pass apiKey in options.");
892
+ const label = p.label ?? p.name;
893
+ if (p.requireExplicitBaseUrl && explicitBase === void 0) {
894
+ const env = p.envBaseUrl ? ` or ${p.envBaseUrl}` : "";
895
+ throw new Error(
896
+ `${label} requires a base URL. Set providers.${p.name}.baseUrl${env} (it is resource-specific and has no default).`
897
+ );
898
+ }
899
+ if (typeof this.apiKeySource === "string" && !this.apiKeySource && !p.allowMissingApiKey) {
900
+ const env = p.envApiKey ?? "the API key env var";
901
+ throw new Error(`${label} API key is required. Set ${env} or pass apiKey in options.`);
574
902
  }
575
903
  }
576
- // ---------------------------------------------------------------------------
577
- // chat - non-streaming completion
578
- // ---------------------------------------------------------------------------
904
+ /** Resolve the API key for one request, validating against allowMissingApiKey. */
905
+ async resolveKey() {
906
+ const key = await resolveApiKey(this.apiKeySource);
907
+ if (!key && !this.profile.allowMissingApiKey) {
908
+ const label = this.profile.label ?? this.profile.name;
909
+ const env = this.profile.envApiKey ?? "the API key env var";
910
+ throw new Error(`${label} API key is required. Set ${env} or pass apiKey in options.`);
911
+ }
912
+ return key;
913
+ }
579
914
  async chat(messages, options) {
915
+ const headers = this.buildHeaders(await this.resolveKey());
580
916
  const body = this.buildRequestBody(messages, options, false);
581
917
  const res = await fetchWithRetry(
582
918
  `${this.baseUrl}/chat/completions`,
583
919
  {
584
920
  method: "POST",
585
- headers: {
586
- "Content-Type": "application/json",
587
- Authorization: `Bearer ${this.apiKey}`
588
- },
921
+ headers,
589
922
  body: JSON.stringify(body),
590
923
  signal: options.signal
591
924
  },
592
- { governor: this.governor }
925
+ { governor: this.governor, provider: this.name }
593
926
  );
594
927
  if (!res.ok) {
595
928
  const errorBody = await res.text();
596
- const message = this.extractErrorMessage(errorBody, res.status);
597
- throw new Error(message);
929
+ throw buildProviderError({
930
+ provider: this.name,
931
+ status: res.status,
932
+ headers: res.headers,
933
+ message: this.extractErrorMessage(errorBody, res.status),
934
+ body: errorBody
935
+ });
598
936
  }
599
937
  const json = await res.json();
600
- const choice = json.choices[0];
601
- const usage = json.usage ? {
602
- prompt_tokens: json.usage.prompt_tokens,
603
- completion_tokens: json.usage.completion_tokens,
604
- total_tokens: json.usage.total_tokens,
605
- reasoning_tokens: json.usage.completion_tokens_details?.reasoning_tokens,
606
- cached_tokens: json.usage.prompt_tokens_details?.cached_tokens
607
- } : void 0;
608
- const cost = usage ? estimateOpenAICost(
609
- options.model,
610
- usage.prompt_tokens,
611
- usage.completion_tokens,
612
- usage.cached_tokens
613
- ) : void 0;
614
- return {
615
- content: choice.message.content ?? "",
616
- tool_calls: choice.message.tool_calls?.map((tc) => ({
617
- id: tc.id,
618
- type: "function",
619
- function: {
620
- name: tc.function.name,
621
- arguments: tc.function.arguments
622
- }
623
- })),
624
- usage,
625
- cost
626
- };
938
+ return this.parseResponse(json, options.model);
627
939
  }
628
- // ---------------------------------------------------------------------------
629
- // stream - SSE streaming completion
630
- // ---------------------------------------------------------------------------
631
940
  async *stream(messages, options) {
941
+ const headers = this.buildHeaders(await this.resolveKey());
632
942
  const body = this.buildRequestBody(messages, options, true);
633
943
  const res = await fetchWithRetry(
634
944
  `${this.baseUrl}/chat/completions`,
635
945
  {
636
946
  method: "POST",
637
- headers: {
638
- "Content-Type": "application/json",
639
- Authorization: `Bearer ${this.apiKey}`
640
- },
947
+ headers,
641
948
  body: JSON.stringify(body),
642
949
  signal: options.signal
643
950
  },
644
- { governor: this.governor }
951
+ { governor: this.governor, provider: this.name }
645
952
  );
646
953
  if (!res.ok) {
647
954
  const errorBody = await res.text();
648
- const message = this.extractErrorMessage(errorBody, res.status);
649
- throw new Error(message);
955
+ throw buildProviderError({
956
+ provider: this.name,
957
+ status: res.status,
958
+ headers: res.headers,
959
+ message: this.extractErrorMessage(errorBody, res.status),
960
+ body: errorBody
961
+ });
650
962
  }
651
963
  if (!res.body) {
652
- throw new Error("OpenAI stream response has no body");
964
+ throw new Error(`${this.profile.label ?? this.profile.name} stream response has no body`);
653
965
  }
654
966
  yield* this.parseSSEStream(res.body, options.model);
655
967
  }
656
968
  // ---------------------------------------------------------------------------
657
- // Internal helpers
969
+ // Request building
658
970
  // ---------------------------------------------------------------------------
659
- buildRequestBody(messages, options, stream) {
660
- const oSeries = isOSeriesModel(options.model);
661
- const reasoningCapable = supportsReasoningEffort(options.model);
662
- const { thinkingBudget, thinkingDisabled, activeEffort, hasBudgetOverride } = resolveThinkingOptions(options);
663
- let wireEffort;
664
- if (reasoningCapable) {
665
- if (hasBudgetOverride) {
666
- wireEffort = clampReasoningEffort(options.model, budgetToReasoningEffort(thinkingBudget));
667
- } else if (!thinkingDisabled && activeEffort) {
668
- wireEffort = clampReasoningEffort(options.model, effortToReasoningEffort(activeEffort));
669
- } else if (thinkingDisabled) {
670
- wireEffort = clampReasoningEffort(options.model, "none");
671
- }
971
+ buildHeaders(apiKey) {
972
+ const headers = {
973
+ "Content-Type": "application/json",
974
+ ...this.profile.headers ?? {}
975
+ };
976
+ if (apiKey) {
977
+ const auth = this.authHeader ?? this.profile.authHeader ?? "bearer";
978
+ if (auth === "bearer") headers.Authorization = `Bearer ${apiKey}`;
979
+ else if (auth === "api-key") headers["api-key"] = apiKey;
980
+ else headers[auth.header] = auth.scheme ? `${auth.scheme} ${apiKey}` : apiKey;
672
981
  }
673
- const stripTemp = oSeries || reasoningCapable && wireEffort !== void 0;
982
+ return headers;
983
+ }
984
+ buildRequestBody(messages, options, stream) {
985
+ const profile = this.profile;
986
+ const resolved = resolveThinkingOptions(options);
674
987
  const body = {
675
988
  model: options.model,
676
- messages: messages.map((m) => this.formatMessage(m, oSeries)),
989
+ messages: messages.map((m) => this.formatMessage(m, options.model)),
677
990
  stream
678
991
  };
679
- if (options.temperature !== void 0 && !stripTemp) {
992
+ const emitResult = profile.reasoning.emit(body, resolved, options.model) ?? {};
993
+ const stripTemperature = emitResult.stripTemperature ?? false;
994
+ if (options.temperature !== void 0 && !stripTemperature) {
680
995
  body.temperature = options.temperature;
681
996
  }
682
997
  if (options.maxTokens !== void 0) {
683
- body.max_completion_tokens = options.maxTokens;
998
+ body[profile.maxTokensField ?? "max_completion_tokens"] = options.maxTokens;
684
999
  }
685
1000
  if (options.stop) body.stop = options.stop;
686
1001
  if (options.tools && options.tools.length > 0) {
687
1002
  body.tools = options.tools;
688
- if (!oSeries) {
1003
+ if (resolvePerModel(profile.parallelToolCalls, options.model, false)) {
689
1004
  body.parallel_tool_calls = true;
690
1005
  }
691
1006
  }
692
- if (options.toolChoice !== void 0) {
693
- body.tool_choice = options.toolChoice;
694
- }
1007
+ if (options.toolChoice !== void 0) body.tool_choice = options.toolChoice;
695
1008
  if (options.responseFormat) {
696
- body.response_format = options.responseFormat;
1009
+ const supportsSchema = resolvePerModel(
1010
+ profile.capabilities?.supportsJsonSchema,
1011
+ options.model,
1012
+ true
1013
+ );
1014
+ body.response_format = options.responseFormat.type === "json_schema" && !supportsSchema ? { type: "json_object" } : options.responseFormat;
697
1015
  }
698
- if (wireEffort) body.reasoning_effort = wireEffort;
699
- if (stream) {
1016
+ if (stream && (profile.capabilities?.supportsStreamUsage ?? true)) {
700
1017
  body.stream_options = { include_usage: true };
701
1018
  }
702
- if (options.providerOptions) {
703
- Object.assign(body, options.providerOptions);
1019
+ if (profile.requestDefaults) Object.assign(body, profile.requestDefaults);
1020
+ const userKeys = options.providerOptions ? Object.keys(options.providerOptions) : [];
1021
+ if (options.providerOptions) Object.assign(body, options.providerOptions);
1022
+ const forbidden = resolvePerModel(profile.capabilities?.forbiddenParams, options.model, []);
1023
+ for (const key of forbidden) {
1024
+ if (!userKeys.includes(key)) delete body[key];
704
1025
  }
705
1026
  return body;
706
1027
  }
707
- /** Extract a human-readable message from an API error response body. */
1028
+ formatMessage(msg, model) {
1029
+ const out = {
1030
+ role: this.profile.roleFor ? this.profile.roleFor(msg.role, model) : msg.role,
1031
+ content: msg.content
1032
+ };
1033
+ if (msg.name && (this.profile.capabilities?.emitsMessageName ?? true)) out.name = msg.name;
1034
+ if (msg.tool_calls) out.tool_calls = msg.tool_calls;
1035
+ if (msg.tool_call_id) out.tool_call_id = msg.tool_call_id;
1036
+ if (this.profile.reasoning.roundTrip === "on-tool-call-turns" && msg.role === "assistant" && msg.tool_calls && msg.tool_calls.length > 0) {
1037
+ const rt = msg.providerMetadata?.openaiCompatReasoning;
1038
+ if (rt && rt.provider === this.name && isRoundTripField(rt.field) && rt.value !== void 0 && rt.value !== null) {
1039
+ out[rt.field] = rt.value;
1040
+ }
1041
+ }
1042
+ return out;
1043
+ }
1044
+ // ---------------------------------------------------------------------------
1045
+ // Response parsing
1046
+ // ---------------------------------------------------------------------------
708
1047
  extractErrorMessage(body, status) {
1048
+ const label = this.profile.label ?? this.profile.name;
709
1049
  try {
710
1050
  const json = JSON.parse(body);
711
- if (json.error?.message) {
712
- return `OpenAI API error (${status}): ${json.error.message}`;
713
- }
1051
+ const message = json.error?.message ?? json.message ?? json.detail;
1052
+ if (message) return `${label} API error (${status}): ${message}`;
714
1053
  } catch {
715
1054
  }
716
- return `OpenAI API error (${status}): ${body}`;
1055
+ return `${label} API error (${status}): ${body}`;
717
1056
  }
718
- formatMessage(msg, oSeries) {
719
- const out = {
720
- role: msg.role === "system" && oSeries ? "developer" : msg.role,
721
- content: msg.content
1057
+ toUsage(raw) {
1058
+ if (!raw) return void 0;
1059
+ return {
1060
+ prompt_tokens: raw.prompt_tokens,
1061
+ completion_tokens: raw.completion_tokens,
1062
+ total_tokens: raw.total_tokens,
1063
+ reasoning_tokens: raw.completion_tokens_details?.reasoning_tokens,
1064
+ cached_tokens: raw.prompt_tokens_details?.cached_tokens
722
1065
  };
723
- if (msg.name) out.name = msg.name;
724
- if (msg.tool_calls) out.tool_calls = msg.tool_calls;
725
- if (msg.tool_call_id) out.tool_call_id = msg.tool_call_id;
726
- return out;
727
1066
  }
1067
+ computeCost(model, usage, reportedCost) {
1068
+ const p = this.profile.pricing;
1069
+ if (p.kind === "zero") return 0;
1070
+ if (!usage) return void 0;
1071
+ switch (p.kind) {
1072
+ case "unknown":
1073
+ return void 0;
1074
+ case "from-response":
1075
+ return typeof reportedCost === "number" && Number.isFinite(reportedCost) ? reportedCost : void 0;
1076
+ case "table": {
1077
+ const c = priceFromTable(
1078
+ p.table,
1079
+ model,
1080
+ usage.prompt_tokens,
1081
+ usage.completion_tokens,
1082
+ usage.cached_tokens
1083
+ );
1084
+ return c !== void 0 && Number.isFinite(c) ? c : void 0;
1085
+ }
1086
+ default: {
1087
+ const _exhaustive = p;
1088
+ throw new Error(`Unhandled pricing source: ${JSON.stringify(_exhaustive)}`);
1089
+ }
1090
+ }
1091
+ }
1092
+ parseResponse(json, model) {
1093
+ const message = json.choices?.[0]?.message ?? {};
1094
+ let content = normalizeMessageContent(message.content);
1095
+ let thinking;
1096
+ let roundTrip;
1097
+ switch (this.profile.reasoning.capture) {
1098
+ case "reasoning_content":
1099
+ if (typeof message.reasoning_content === "string") {
1100
+ thinking = message.reasoning_content;
1101
+ roundTrip = {
1102
+ provider: this.name,
1103
+ field: "reasoning_content",
1104
+ value: message.reasoning_content
1105
+ };
1106
+ }
1107
+ break;
1108
+ case "reasoning":
1109
+ if (typeof message.reasoning === "string") {
1110
+ thinking = message.reasoning;
1111
+ roundTrip = { provider: this.name, field: "reasoning", value: message.reasoning };
1112
+ }
1113
+ break;
1114
+ case "reasoning_details":
1115
+ if (typeof message.reasoning === "string") thinking = message.reasoning;
1116
+ if (message.reasoning_details !== void 0) {
1117
+ roundTrip = {
1118
+ provider: this.name,
1119
+ field: "reasoning_details",
1120
+ value: message.reasoning_details
1121
+ };
1122
+ }
1123
+ break;
1124
+ case "think_tags": {
1125
+ const ex = extractThinkTags(content);
1126
+ content = ex.content;
1127
+ if (ex.thinking) thinking = ex.thinking;
1128
+ break;
1129
+ }
1130
+ case "none":
1131
+ break;
1132
+ default: {
1133
+ const _exhaustive = this.profile.reasoning.capture;
1134
+ throw new Error(`Unhandled reasoning capture: ${String(_exhaustive)}`);
1135
+ }
1136
+ }
1137
+ const usage = this.toUsage(json.usage);
1138
+ const cost = this.computeCost(model, usage, json.usage?.cost);
1139
+ return {
1140
+ content,
1141
+ thinking_content: thinking || void 0,
1142
+ tool_calls: message.tool_calls?.map((tc) => ({
1143
+ id: tc.id,
1144
+ type: "function",
1145
+ function: { name: tc.function.name, arguments: tc.function.arguments }
1146
+ })),
1147
+ usage,
1148
+ cost,
1149
+ providerMetadata: this.roundTripMetadata(roundTrip)
1150
+ };
1151
+ }
1152
+ roundTripMetadata(rt) {
1153
+ if (!rt || this.profile.reasoning.roundTrip === void 0 || this.profile.reasoning.roundTrip === "none") {
1154
+ return void 0;
1155
+ }
1156
+ return rt.value === void 0 || rt.value === null ? void 0 : { openaiCompatReasoning: rt };
1157
+ }
1158
+ // ---------------------------------------------------------------------------
1159
+ // SSE streaming
1160
+ // ---------------------------------------------------------------------------
728
1161
  async *parseSSEStream(body, model) {
729
1162
  const reader = body.getReader();
730
1163
  const decoder = new TextDecoder();
731
1164
  let buffer = "";
732
1165
  let usageData;
1166
+ const capture = this.profile.reasoning.capture;
1167
+ const thinkScanner = capture === "think_tags" ? new ThinkTagScanner() : void 0;
1168
+ let reasoningText = "";
1169
+ const reasoningDetails = [];
733
1170
  const indexToId = /* @__PURE__ */ new Map();
1171
+ const pendingToolCalls = /* @__PURE__ */ new Map();
1172
+ const flushPendingToolCalls = () => {
1173
+ const out = [];
1174
+ for (const [index, buf] of pendingToolCalls) {
1175
+ out.push({
1176
+ type: "tool_call_delta",
1177
+ id: `__toolcall_${index}`,
1178
+ name: buf.name,
1179
+ arguments: buf.arguments
1180
+ });
1181
+ }
1182
+ pendingToolCalls.clear();
1183
+ return out;
1184
+ };
1185
+ const makeDone = () => {
1186
+ const usage = this.toUsage(usageData);
1187
+ let providerMetadata;
1188
+ if (this.profile.reasoning.roundTrip === "on-tool-call-turns") {
1189
+ if (capture === "reasoning_details" && reasoningDetails.length > 0) {
1190
+ providerMetadata = {
1191
+ openaiCompatReasoning: {
1192
+ provider: this.name,
1193
+ field: "reasoning_details",
1194
+ value: reasoningDetails
1195
+ }
1196
+ };
1197
+ } else if ((capture === "reasoning_content" || capture === "reasoning") && reasoningText) {
1198
+ providerMetadata = {
1199
+ openaiCompatReasoning: { provider: this.name, field: capture, value: reasoningText }
1200
+ };
1201
+ }
1202
+ }
1203
+ return {
1204
+ type: "done",
1205
+ usage,
1206
+ cost: this.computeCost(model, usage, usageData?.cost),
1207
+ providerMetadata
1208
+ };
1209
+ };
734
1210
  try {
735
- while (true) {
1211
+ for (; ; ) {
736
1212
  const { done, value } = await reader.read();
737
1213
  if (done) break;
738
1214
  buffer += decoder.decode(value, { stream: true });
@@ -742,47 +1218,76 @@ var OpenAIProvider = class {
742
1218
  const trimmed = line.trim();
743
1219
  if (!trimmed || trimmed.startsWith(":")) continue;
744
1220
  if (trimmed === "data: [DONE]") {
745
- yield {
746
- type: "done",
747
- usage: usageData,
748
- cost: usageData ? estimateOpenAICost(
749
- model,
750
- usageData.prompt_tokens,
751
- usageData.completion_tokens,
752
- usageData.cached_tokens
753
- ) : void 0
754
- };
1221
+ if (thinkScanner) {
1222
+ const flushed = thinkScanner.flush();
1223
+ if (flushed.text) yield { type: "text_delta", content: flushed.text };
1224
+ if (flushed.thinking) yield { type: "thinking_delta", content: flushed.thinking };
1225
+ }
1226
+ yield* flushPendingToolCalls();
1227
+ yield makeDone();
755
1228
  return;
756
1229
  }
757
- if (trimmed.startsWith("data: ")) {
758
- const jsonStr = trimmed.slice(6);
759
- let parsed;
760
- try {
761
- parsed = JSON.parse(jsonStr);
762
- } catch {
763
- continue;
764
- }
765
- if (parsed.usage) {
766
- usageData = {
767
- prompt_tokens: parsed.usage.prompt_tokens,
768
- completion_tokens: parsed.usage.completion_tokens,
769
- total_tokens: parsed.usage.total_tokens,
770
- reasoning_tokens: parsed.usage.completion_tokens_details?.reasoning_tokens,
771
- cached_tokens: parsed.usage.prompt_tokens_details?.cached_tokens
772
- };
773
- continue;
774
- }
775
- const delta = parsed.choices?.[0]?.delta;
776
- if (!delta) continue;
777
- if (delta.content) {
1230
+ if (!trimmed.startsWith("data: ")) continue;
1231
+ let parsed;
1232
+ try {
1233
+ parsed = JSON.parse(trimmed.slice(6));
1234
+ } catch {
1235
+ continue;
1236
+ }
1237
+ if (parsed.error) {
1238
+ const message = typeof parsed.error.message === "string" ? parsed.error.message : `${this.profile.label ?? this.profile.name} stream error`;
1239
+ throw new ProviderError({
1240
+ provider: this.name,
1241
+ status: 0,
1242
+ retryable: false,
1243
+ message,
1244
+ body: JSON.stringify(parsed.error)
1245
+ });
1246
+ }
1247
+ if (parsed.usage) {
1248
+ usageData = parsed.usage;
1249
+ if (!parsed.choices || parsed.choices.length === 0) continue;
1250
+ }
1251
+ const delta = parsed.choices?.[0]?.delta;
1252
+ if (!delta) continue;
1253
+ const deltaReasoning = capture === "reasoning_content" ? delta.reasoning_content : capture === "reasoning" || capture === "reasoning_details" ? delta.reasoning : void 0;
1254
+ if (typeof deltaReasoning === "string" && deltaReasoning.length > 0) {
1255
+ reasoningText += deltaReasoning;
1256
+ yield { type: "thinking_delta", content: deltaReasoning };
1257
+ }
1258
+ if (capture === "reasoning_details" && Array.isArray(delta.reasoning_details)) {
1259
+ reasoningDetails.push(...delta.reasoning_details);
1260
+ }
1261
+ if (typeof delta.content === "string" && delta.content.length > 0) {
1262
+ if (thinkScanner) {
1263
+ const { text, thinking } = thinkScanner.push(delta.content);
1264
+ if (text) yield { type: "text_delta", content: text };
1265
+ if (thinking) yield { type: "thinking_delta", content: thinking };
1266
+ } else {
778
1267
  yield { type: "text_delta", content: delta.content };
779
1268
  }
780
- if (delta.tool_calls) {
781
- for (const tc of delta.tool_calls) {
782
- if (tc.id) {
783
- indexToId.set(tc.index, tc.id);
784
- }
785
- const id = indexToId.get(tc.index) ?? `__pending_${tc.index}`;
1269
+ }
1270
+ if (delta.tool_calls) {
1271
+ for (const tc of delta.tool_calls) {
1272
+ if (tc.id) indexToId.set(tc.index, tc.id);
1273
+ const id = indexToId.get(tc.index);
1274
+ if (id === void 0) {
1275
+ const buf2 = pendingToolCalls.get(tc.index) ?? { arguments: "" };
1276
+ if (tc.function?.name) buf2.name = tc.function.name;
1277
+ if (tc.function?.arguments) buf2.arguments += tc.function.arguments;
1278
+ pendingToolCalls.set(tc.index, buf2);
1279
+ continue;
1280
+ }
1281
+ const buf = pendingToolCalls.get(tc.index);
1282
+ if (buf) {
1283
+ pendingToolCalls.delete(tc.index);
1284
+ yield {
1285
+ type: "tool_call_delta",
1286
+ id,
1287
+ name: buf.name ?? tc.function?.name,
1288
+ arguments: buf.arguments + (tc.function?.arguments ?? "")
1289
+ };
1290
+ } else {
786
1291
  yield {
787
1292
  type: "tool_call_delta",
788
1293
  id,
@@ -794,58 +1299,176 @@ var OpenAIProvider = class {
794
1299
  }
795
1300
  }
796
1301
  }
797
- yield {
798
- type: "done",
799
- usage: usageData,
800
- cost: usageData ? estimateOpenAICost(
801
- model,
802
- usageData.prompt_tokens,
803
- usageData.completion_tokens,
804
- usageData.cached_tokens
805
- ) : void 0
806
- };
1302
+ if (thinkScanner) {
1303
+ const flushed = thinkScanner.flush();
1304
+ if (flushed.text) yield { type: "text_delta", content: flushed.text };
1305
+ if (flushed.thinking) yield { type: "thinking_delta", content: flushed.thinking };
1306
+ }
1307
+ yield* flushPendingToolCalls();
1308
+ throw new ProviderError({
1309
+ provider: this.name,
1310
+ status: 0,
1311
+ retryable: true,
1312
+ message: `${this.profile.label ?? this.profile.name} stream ended before [DONE]`
1313
+ });
807
1314
  } finally {
808
1315
  reader.releaseLock();
809
1316
  }
810
1317
  }
811
1318
  };
812
1319
 
1320
+ // src/providers/openai.ts
1321
+ var OPENAI_PRICING = {
1322
+ // gpt-4o era — cache reads at 50% of input rate
1323
+ "gpt-4o": [25e-7, 1e-5, 0.5],
1324
+ "gpt-4o-mini": [15e-8, 6e-7, 0.5],
1325
+ "gpt-4-turbo": [1e-5, 3e-5, 0.5],
1326
+ "gpt-4": [3e-5, 6e-5, 0.5],
1327
+ "gpt-3.5-turbo": [5e-7, 15e-7, 0.5],
1328
+ o1: [15e-6, 6e-5, 0.5],
1329
+ "o1-mini": [3e-6, 12e-6, 0.5],
1330
+ "o1-pro": [15e-5, 6e-4, 0.5],
1331
+ // gpt-4.1 / o3 / o4 era — cache reads at 25% of input rate
1332
+ "gpt-4.1": [2e-6, 8e-6, 0.25],
1333
+ "gpt-4.1-mini": [4e-7, 16e-7, 0.25],
1334
+ "gpt-4.1-nano": [1e-7, 4e-7, 0.25],
1335
+ o3: [1e-5, 4e-5, 0.25],
1336
+ "o3-mini": [11e-7, 44e-7, 0.25],
1337
+ "o3-pro": [2e-5, 8e-5, 0.25],
1338
+ "o4-mini": [11e-7, 44e-7, 0.25],
1339
+ // gpt-5 era — cache reads at 10% of input rate
1340
+ "gpt-5": [125e-8, 1e-5, 0.1],
1341
+ "gpt-5-mini": [25e-8, 2e-6, 0.1],
1342
+ "gpt-5-nano": [5e-8, 4e-7, 0.1],
1343
+ "gpt-5.1": [125e-8, 1e-5, 0.1],
1344
+ "gpt-5.2": [175e-8, 14e-6, 0.1],
1345
+ "gpt-5.3": [175e-8, 14e-6, 0.1],
1346
+ "gpt-5.4": [25e-7, 15e-6, 0.1],
1347
+ "gpt-5.4-pro": [3e-5, 18e-5, 0.1],
1348
+ // gpt-5.5 era (flagship released 2026-04) — cache reads at 10% of input rate
1349
+ "gpt-5.5": [5e-6, 3e-5, 0.1],
1350
+ "gpt-5.5-pro": [3e-5, 18e-5, 0.1]
1351
+ };
1352
+ function estimateOpenAICost(model, promptTokens, completionTokens, cachedTokens) {
1353
+ return priceFromTable(OPENAI_PRICING, model, promptTokens, completionTokens, cachedTokens);
1354
+ }
1355
+ function isOSeriesModel(model) {
1356
+ return /^(o1|o3|o4-mini)/.test(model);
1357
+ }
1358
+ function supportsReasoningEffort(model) {
1359
+ return isOSeriesModel(model) || /^gpt-5/.test(model);
1360
+ }
1361
+ function supportsReasoningNone(model) {
1362
+ return /^gpt-5\.[1-9]/.test(model);
1363
+ }
1364
+ function supportsXhigh(model) {
1365
+ return /^gpt-5\.([2-9]|\d{2,})/.test(model);
1366
+ }
1367
+ function clampReasoningEffort(model, effort) {
1368
+ if (model.startsWith("gpt-5-pro")) return "high";
1369
+ if (effort === "none" && !supportsReasoningNone(model)) return "minimal";
1370
+ if (effort === "xhigh" && !supportsXhigh(model)) return "high";
1371
+ return effort;
1372
+ }
1373
+ function effortToReasoningEffort(effort) {
1374
+ return effort === "max" ? "xhigh" : effort;
1375
+ }
1376
+ function budgetToReasoningEffort(budget) {
1377
+ if (budget <= 1024) return "low";
1378
+ if (budget <= 8192) return "medium";
1379
+ return "high";
1380
+ }
1381
+ var openaiReasoningEmit = (body, resolved, model) => {
1382
+ const oSeries = isOSeriesModel(model);
1383
+ const reasoningCapable = supportsReasoningEffort(model);
1384
+ const { thinkingBudget, thinkingDisabled, activeEffort, hasBudgetOverride } = resolved;
1385
+ let wireEffort;
1386
+ if (reasoningCapable) {
1387
+ if (hasBudgetOverride) {
1388
+ wireEffort = clampReasoningEffort(model, budgetToReasoningEffort(thinkingBudget));
1389
+ } else if (!thinkingDisabled && activeEffort) {
1390
+ wireEffort = clampReasoningEffort(model, effortToReasoningEffort(activeEffort));
1391
+ } else if (thinkingDisabled) {
1392
+ wireEffort = clampReasoningEffort(model, "none");
1393
+ }
1394
+ }
1395
+ if (wireEffort) body.reasoning_effort = wireEffort;
1396
+ return { stripTemperature: oSeries || reasoningCapable && wireEffort !== void 0 };
1397
+ };
1398
+ var OPENAI_PROFILE = {
1399
+ name: "openai",
1400
+ label: "OpenAI",
1401
+ defaultBaseUrl: "https://api.openai.com/v1",
1402
+ envApiKey: "OPENAI_API_KEY",
1403
+ envBaseUrl: "OPENAI_BASE_URL",
1404
+ pricing: { kind: "table", table: OPENAI_PRICING },
1405
+ reasoning: { emit: openaiReasoningEmit, capture: "none" },
1406
+ roleFor: (role, model) => role === "system" && isOSeriesModel(model) ? "developer" : role,
1407
+ maxTokensField: "max_completion_tokens",
1408
+ parallelToolCalls: (model) => !isOSeriesModel(model)
1409
+ };
1410
+ var OpenAIProvider = class extends OpenAICompatibleProvider {
1411
+ constructor(options = {}) {
1412
+ super({ profile: OPENAI_PROFILE, ...options });
1413
+ }
1414
+ };
1415
+
813
1416
  // src/providers/openai-responses.ts
814
1417
  var OpenAIResponsesProvider = class {
815
1418
  name = "openai-responses";
1419
+ /** The Responses API honors `json_schema` (structured outputs) natively. */
1420
+ nativeStructuredOutputSupport() {
1421
+ return "schema";
1422
+ }
816
1423
  baseUrl;
817
- apiKey;
1424
+ apiKeySource;
818
1425
  governor;
819
1426
  constructor(options = {}) {
820
- this.apiKey = options.apiKey ?? process.env.OPENAI_API_KEY ?? "";
1427
+ this.apiKeySource = options.apiKey ?? process.env.OPENAI_API_KEY ?? "";
821
1428
  this.baseUrl = (options.baseUrl ?? process.env.OPENAI_BASE_URL ?? "https://api.openai.com/v1").replace(/\/$/, "");
822
1429
  this.governor = options.rateLimit ? new RateLimiter(options.rateLimit) : void 0;
823
- if (!this.apiKey) {
1430
+ if (typeof this.apiKeySource === "string" && !this.apiKeySource) {
1431
+ throw new Error("OpenAI API key is required. Set OPENAI_API_KEY or pass apiKey in options.");
1432
+ }
1433
+ }
1434
+ /** Resolve the API key for one request (supports an expiring-token callback). */
1435
+ async resolveKey() {
1436
+ const key = await resolveApiKey(this.apiKeySource);
1437
+ if (!key) {
824
1438
  throw new Error("OpenAI API key is required. Set OPENAI_API_KEY or pass apiKey in options.");
825
1439
  }
1440
+ return key;
1441
+ }
1442
+ /** Build request headers from a resolved key (used by both chat and stream). */
1443
+ buildHeaders(apiKey) {
1444
+ return { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` };
826
1445
  }
827
1446
  // ---------------------------------------------------------------------------
828
1447
  // chat - non-streaming
829
1448
  // ---------------------------------------------------------------------------
830
1449
  async chat(messages, options) {
1450
+ const headers = this.buildHeaders(await this.resolveKey());
831
1451
  const body = this.buildRequestBody(messages, options, false);
832
1452
  const res = await fetchWithRetry(
833
1453
  `${this.baseUrl}/responses`,
834
1454
  {
835
1455
  method: "POST",
836
- headers: {
837
- "Content-Type": "application/json",
838
- Authorization: `Bearer ${this.apiKey}`
839
- },
1456
+ headers,
840
1457
  body: JSON.stringify(body),
841
1458
  signal: options.signal
842
1459
  },
843
- { governor: this.governor }
1460
+ { governor: this.governor, provider: this.name }
844
1461
  );
845
1462
  if (!res.ok) {
846
1463
  const errorBody = await res.text();
847
1464
  const message = this.extractErrorMessage(errorBody, res.status);
848
- throw new Error(message);
1465
+ throw buildProviderError({
1466
+ provider: this.name,
1467
+ status: res.status,
1468
+ headers: res.headers,
1469
+ message,
1470
+ body: errorBody
1471
+ });
849
1472
  }
850
1473
  const json = await res.json();
851
1474
  return this.parseResponse(json, options.model);
@@ -854,24 +1477,28 @@ var OpenAIResponsesProvider = class {
854
1477
  // stream - SSE streaming
855
1478
  // ---------------------------------------------------------------------------
856
1479
  async *stream(messages, options) {
1480
+ const headers = this.buildHeaders(await this.resolveKey());
857
1481
  const body = this.buildRequestBody(messages, options, true);
858
1482
  const res = await fetchWithRetry(
859
1483
  `${this.baseUrl}/responses`,
860
1484
  {
861
1485
  method: "POST",
862
- headers: {
863
- "Content-Type": "application/json",
864
- Authorization: `Bearer ${this.apiKey}`
865
- },
1486
+ headers,
866
1487
  body: JSON.stringify(body),
867
1488
  signal: options.signal
868
1489
  },
869
- { governor: this.governor }
1490
+ { governor: this.governor, provider: this.name }
870
1491
  );
871
1492
  if (!res.ok) {
872
1493
  const errorBody = await res.text();
873
1494
  const message = this.extractErrorMessage(errorBody, res.status);
874
- throw new Error(message);
1495
+ throw buildProviderError({
1496
+ provider: this.name,
1497
+ status: res.status,
1498
+ headers: res.headers,
1499
+ message,
1500
+ body: errorBody
1501
+ });
875
1502
  }
876
1503
  if (!res.body) {
877
1504
  throw new Error("OpenAI Responses stream has no body");
@@ -1160,7 +1787,12 @@ var OpenAIResponsesProvider = class {
1160
1787
  }
1161
1788
  case "response.failed": {
1162
1789
  const errorMsg = data.response?.error?.message ?? data.response?.status_details?.error?.message ?? "Unknown error";
1163
- throw new Error(`OpenAI Responses API error: ${errorMsg}`);
1790
+ throw new ProviderError({
1791
+ provider: this.name,
1792
+ status: 0,
1793
+ retryable: false,
1794
+ message: `OpenAI Responses API error: ${errorMsg}`
1795
+ });
1164
1796
  }
1165
1797
  default:
1166
1798
  return null;
@@ -1214,7 +1846,7 @@ function estimateAnthropicCost(model, inputTokens, outputTokens, cacheReadTokens
1214
1846
  }
1215
1847
  }
1216
1848
  }
1217
- if (!pricing) return 0;
1849
+ if (!pricing) return void 0;
1218
1850
  const [inputRate, outputRate] = pricing;
1219
1851
  const cacheRead = cacheReadTokens ?? 0;
1220
1852
  const cacheWrite = cacheWriteTokens ?? 0;
@@ -1248,63 +1880,90 @@ function clampAnthropicEffort(model, effort) {
1248
1880
  }
1249
1881
  var AnthropicProvider = class {
1250
1882
  name = "anthropic";
1883
+ /** Anthropic ignores native `json_schema` structurally — Axl uses a system
1884
+ * prompt JSON instruction + client-side Zod validation (see buildRequest). */
1885
+ nativeStructuredOutputSupport() {
1886
+ return "unsupported";
1887
+ }
1251
1888
  baseUrl;
1252
- apiKey;
1253
- currentModel;
1889
+ apiKeySource;
1254
1890
  governor;
1255
1891
  constructor(options = {}) {
1256
- this.apiKey = options.apiKey ?? process.env.ANTHROPIC_API_KEY ?? "";
1892
+ this.apiKeySource = options.apiKey ?? process.env.ANTHROPIC_API_KEY ?? "";
1257
1893
  this.baseUrl = (options.baseUrl ?? "https://api.anthropic.com/v1").replace(/\/$/, "");
1258
1894
  this.governor = options.rateLimit ? new RateLimiter(options.rateLimit) : void 0;
1259
- if (!this.apiKey) {
1895
+ if (typeof this.apiKeySource === "string" && !this.apiKeySource) {
1896
+ throw new Error(
1897
+ "Anthropic API key is required. Set ANTHROPIC_API_KEY or pass apiKey in options."
1898
+ );
1899
+ }
1900
+ }
1901
+ /** Resolve the API key for one request (supports an expiring-token callback). */
1902
+ async resolveKey() {
1903
+ const key = await resolveApiKey(this.apiKeySource);
1904
+ if (!key) {
1260
1905
  throw new Error(
1261
1906
  "Anthropic API key is required. Set ANTHROPIC_API_KEY or pass apiKey in options."
1262
1907
  );
1263
1908
  }
1909
+ return key;
1264
1910
  }
1265
1911
  // ---------------------------------------------------------------------------
1266
1912
  // chat - non-streaming completion
1267
1913
  // ---------------------------------------------------------------------------
1268
1914
  async chat(messages, options) {
1269
- this.currentModel = options.model;
1915
+ const headers = this.buildHeaders(await this.resolveKey());
1270
1916
  const body = this.buildRequestBody(messages, options, false);
1271
1917
  const res = await fetchWithRetry(
1272
1918
  `${this.baseUrl}/messages`,
1273
1919
  {
1274
1920
  method: "POST",
1275
- headers: this.buildHeaders(),
1921
+ headers,
1276
1922
  body: JSON.stringify(body),
1277
1923
  signal: options.signal
1278
1924
  },
1279
- { governor: this.governor }
1925
+ { governor: this.governor, provider: this.name }
1280
1926
  );
1281
1927
  if (!res.ok) {
1282
1928
  const errorBody = await res.text();
1283
1929
  const message = this.extractErrorMessage(errorBody, res.status);
1284
- throw new Error(message);
1930
+ throw buildProviderError({
1931
+ provider: this.name,
1932
+ status: res.status,
1933
+ headers: res.headers,
1934
+ message,
1935
+ body: errorBody
1936
+ });
1285
1937
  }
1286
1938
  const json = await res.json();
1287
- return this.parseResponse(json);
1939
+ return this.parseResponse(json, options.model);
1288
1940
  }
1289
1941
  // ---------------------------------------------------------------------------
1290
1942
  // stream - SSE streaming completion
1291
1943
  // ---------------------------------------------------------------------------
1292
1944
  async *stream(messages, options) {
1945
+ const headers = this.buildHeaders(await this.resolveKey());
1293
1946
  const body = this.buildRequestBody(messages, options, true);
1294
1947
  const res = await fetchWithRetry(
1295
1948
  `${this.baseUrl}/messages`,
1296
1949
  {
1297
1950
  method: "POST",
1298
- headers: this.buildHeaders(),
1951
+ headers,
1299
1952
  body: JSON.stringify(body),
1300
1953
  signal: options.signal
1301
1954
  },
1302
- { governor: this.governor }
1955
+ { governor: this.governor, provider: this.name }
1303
1956
  );
1304
1957
  if (!res.ok) {
1305
1958
  const errorBody = await res.text();
1306
1959
  const message = this.extractErrorMessage(errorBody, res.status);
1307
- throw new Error(message);
1960
+ throw buildProviderError({
1961
+ provider: this.name,
1962
+ status: res.status,
1963
+ headers: res.headers,
1964
+ message,
1965
+ body: errorBody
1966
+ });
1308
1967
  }
1309
1968
  if (!res.body) {
1310
1969
  throw new Error("Anthropic stream response has no body");
@@ -1314,10 +1973,10 @@ var AnthropicProvider = class {
1314
1973
  // ---------------------------------------------------------------------------
1315
1974
  // Internal: request building
1316
1975
  // ---------------------------------------------------------------------------
1317
- buildHeaders() {
1976
+ buildHeaders(apiKey) {
1318
1977
  return {
1319
1978
  "Content-Type": "application/json",
1320
- "x-api-key": this.apiKey,
1979
+ "x-api-key": apiKey,
1321
1980
  "anthropic-version": ANTHROPIC_API_VERSION
1322
1981
  };
1323
1982
  }
@@ -1502,7 +2161,7 @@ ${jsonInstruction}` : jsonInstruction;
1502
2161
  // ---------------------------------------------------------------------------
1503
2162
  // Internal: response parsing
1504
2163
  // ---------------------------------------------------------------------------
1505
- parseResponse(json) {
2164
+ parseResponse(json, model) {
1506
2165
  let content = "";
1507
2166
  let thinkingContent = "";
1508
2167
  const toolCalls = [];
@@ -1531,13 +2190,7 @@ ${jsonInstruction}` : jsonInstruction;
1531
2190
  total_tokens: totalInput + json.usage.output_tokens,
1532
2191
  cached_tokens: cacheRead > 0 ? cacheRead : void 0
1533
2192
  } : void 0;
1534
- const cost = json.usage ? estimateAnthropicCost(
1535
- this.currentModel ?? "",
1536
- totalInput,
1537
- json.usage.output_tokens,
1538
- cacheRead,
1539
- cacheWrite
1540
- ) : void 0;
2193
+ const cost = json.usage ? estimateAnthropicCost(model, totalInput, json.usage.output_tokens, cacheRead, cacheWrite) : void 0;
1541
2194
  return {
1542
2195
  content,
1543
2196
  thinking_content: thinkingContent || void 0,
@@ -1713,6 +2366,15 @@ function sanitizeSchemaForGemini(schema) {
1713
2366
  }
1714
2367
  return out;
1715
2368
  }
2369
+ function parseGeminiFunctionResponse(content) {
2370
+ let parsed;
2371
+ try {
2372
+ parsed = JSON.parse(content);
2373
+ } catch {
2374
+ return { result: content };
2375
+ }
2376
+ return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : { result: parsed };
2377
+ }
1716
2378
  var GEMINI_PRICING = {
1717
2379
  "gemini-2.5-pro": [125e-8, 1e-5],
1718
2380
  "gemini-2.5-flash": [3e-7, 25e-7],
@@ -1739,7 +2401,7 @@ function estimateGeminiCost(model, inputTokens, outputTokens, cachedTokens) {
1739
2401
  }
1740
2402
  }
1741
2403
  }
1742
- if (!pricing) return 0;
2404
+ if (!pricing) return void 0;
1743
2405
  const [inputRate, outputRate] = pricing;
1744
2406
  const cached = cachedTokens ?? 0;
1745
2407
  const inputCost = (inputTokens - cached) * inputRate + cached * inputRate * 0.1;
@@ -1784,40 +2446,61 @@ function warnGemini3xEffortNone(model) {
1784
2446
  }
1785
2447
  var GeminiProvider = class {
1786
2448
  name = "google";
2449
+ /** Gemini accepts a `responseSchema` but `sanitizeSchemaForGemini` strips
2450
+ * keywords it doesn't support (`$ref`/`$defs`/`additionalProperties`/…), so a
2451
+ * derived schema can lose constraints — lossy, not faithful. */
2452
+ nativeStructuredOutputSupport() {
2453
+ return "lossy";
2454
+ }
1787
2455
  baseUrl;
1788
- apiKey;
2456
+ apiKeySource;
1789
2457
  callCounter = 0;
1790
2458
  governor;
1791
2459
  constructor(options = {}) {
1792
- this.apiKey = options.apiKey ?? process.env.GOOGLE_API_KEY ?? process.env.GEMINI_API_KEY ?? "";
2460
+ this.apiKeySource = options.apiKey ?? process.env.GOOGLE_API_KEY ?? process.env.GEMINI_API_KEY ?? "";
1793
2461
  this.baseUrl = (options.baseUrl ?? "https://generativelanguage.googleapis.com/v1beta").replace(
1794
2462
  /\/$/,
1795
2463
  ""
1796
2464
  );
1797
2465
  this.governor = options.rateLimit ? new RateLimiter(options.rateLimit) : void 0;
1798
- if (!this.apiKey) {
2466
+ if (typeof this.apiKeySource === "string" && !this.apiKeySource) {
1799
2467
  throw new Error("Google API key is required. Set GOOGLE_API_KEY or pass apiKey in options.");
1800
2468
  }
1801
2469
  }
2470
+ /** Resolve the API key for one request (supports an expiring-token callback). */
2471
+ async resolveKey() {
2472
+ const key = await resolveApiKey(this.apiKeySource);
2473
+ if (!key) {
2474
+ throw new Error("Google API key is required. Set GOOGLE_API_KEY or pass apiKey in options.");
2475
+ }
2476
+ return key;
2477
+ }
1802
2478
  // ---------------------------------------------------------------------------
1803
2479
  // chat - non-streaming completion
1804
2480
  // ---------------------------------------------------------------------------
1805
2481
  async chat(messages, options) {
2482
+ const headers = this.buildHeaders(await this.resolveKey());
1806
2483
  const body = this.buildRequestBody(messages, options);
1807
2484
  const res = await fetchWithRetry(
1808
2485
  `${this.baseUrl}/models/${options.model}:generateContent`,
1809
2486
  {
1810
2487
  method: "POST",
1811
- headers: this.buildHeaders(),
2488
+ headers,
1812
2489
  body: JSON.stringify(body),
1813
2490
  signal: options.signal
1814
2491
  },
1815
- { governor: this.governor }
2492
+ { governor: this.governor, provider: this.name }
1816
2493
  );
1817
2494
  if (!res.ok) {
1818
2495
  const errorBody = await res.text();
1819
2496
  const message = this.extractErrorMessage(errorBody, res.status);
1820
- throw new Error(message);
2497
+ throw buildProviderError({
2498
+ provider: this.name,
2499
+ status: res.status,
2500
+ headers: res.headers,
2501
+ message,
2502
+ body: errorBody
2503
+ });
1821
2504
  }
1822
2505
  const json = await res.json();
1823
2506
  return this.parseResponse(json, options.model);
@@ -1826,21 +2509,28 @@ var GeminiProvider = class {
1826
2509
  // stream - SSE streaming completion
1827
2510
  // ---------------------------------------------------------------------------
1828
2511
  async *stream(messages, options) {
2512
+ const headers = this.buildHeaders(await this.resolveKey());
1829
2513
  const body = this.buildRequestBody(messages, options);
1830
2514
  const res = await fetchWithRetry(
1831
2515
  `${this.baseUrl}/models/${options.model}:streamGenerateContent?alt=sse`,
1832
2516
  {
1833
2517
  method: "POST",
1834
- headers: this.buildHeaders(),
2518
+ headers,
1835
2519
  body: JSON.stringify(body),
1836
2520
  signal: options.signal
1837
2521
  },
1838
- { governor: this.governor }
2522
+ { governor: this.governor, provider: this.name }
1839
2523
  );
1840
2524
  if (!res.ok) {
1841
2525
  const errorBody = await res.text();
1842
2526
  const message = this.extractErrorMessage(errorBody, res.status);
1843
- throw new Error(message);
2527
+ throw buildProviderError({
2528
+ provider: this.name,
2529
+ status: res.status,
2530
+ headers: res.headers,
2531
+ message,
2532
+ body: errorBody
2533
+ });
1844
2534
  }
1845
2535
  if (!res.body) {
1846
2536
  throw new Error("Gemini stream response has no body");
@@ -1850,10 +2540,10 @@ var GeminiProvider = class {
1850
2540
  // ---------------------------------------------------------------------------
1851
2541
  // Internal: request building
1852
2542
  // ---------------------------------------------------------------------------
1853
- buildHeaders() {
2543
+ buildHeaders(apiKey) {
1854
2544
  return {
1855
2545
  "Content-Type": "application/json",
1856
- "x-goog-api-key": this.apiKey
2546
+ "x-goog-api-key": apiKey
1857
2547
  };
1858
2548
  }
1859
2549
  extractErrorMessage(body, status) {
@@ -2020,15 +2710,9 @@ var GeminiProvider = class {
2020
2710
  }
2021
2711
  } else if (msg.role === "tool") {
2022
2712
  const functionName = toolCallIdToName.get(msg.tool_call_id) ?? "unknown";
2023
- let responseData;
2024
- try {
2025
- responseData = JSON.parse(msg.content);
2026
- } catch {
2027
- responseData = { result: msg.content };
2028
- }
2029
2713
  const functionResponse = {
2030
2714
  name: functionName,
2031
- response: responseData
2715
+ response: parseGeminiFunctionResponse(msg.content)
2032
2716
  };
2033
2717
  if (msg.tool_call_id && geminiNativeIds.has(msg.tool_call_id)) {
2034
2718
  functionResponse.id = msg.tool_call_id;
@@ -2210,6 +2894,203 @@ var GeminiProvider = class {
2210
2894
  }
2211
2895
  };
2212
2896
 
2897
+ // src/providers/profiles/openrouter.ts
2898
+ var OPENROUTER_PROFILE = {
2899
+ name: "openrouter",
2900
+ label: "OpenRouter",
2901
+ defaultBaseUrl: "https://openrouter.ai/api/v1",
2902
+ envApiKey: "OPENROUTER_API_KEY",
2903
+ envBaseUrl: "OPENROUTER_BASE_URL",
2904
+ pricing: { kind: "from-response" },
2905
+ // Ask OpenRouter to report per-call cost in usage.cost.
2906
+ requestDefaults: { usage: { include: true } },
2907
+ reasoning: {
2908
+ // OpenRouter's effort vocabulary is low|medium|high; xhigh/max clamp to high.
2909
+ emit: reasoningObjectEmit(
2910
+ (effort) => effort === "xhigh" || effort === "max" ? "high" : effort
2911
+ ),
2912
+ capture: "reasoning_details",
2913
+ roundTrip: "on-tool-call-turns"
2914
+ },
2915
+ // OpenRouter normalizes to max_tokens across its backends.
2916
+ maxTokensField: "max_tokens"
2917
+ };
2918
+
2919
+ // src/providers/profiles/azure.ts
2920
+ var AZURE_PROFILE = {
2921
+ name: "azure",
2922
+ label: "Azure OpenAI",
2923
+ // Never actually used — requireExplicitBaseUrl forces a real value first.
2924
+ defaultBaseUrl: "https://YOUR-RESOURCE.openai.azure.com/openai/v1",
2925
+ requireExplicitBaseUrl: true,
2926
+ envApiKey: "AZURE_OPENAI_API_KEY",
2927
+ envBaseUrl: "AZURE_OPENAI_BASE_URL",
2928
+ authHeader: "api-key",
2929
+ pricing: { kind: "table", table: OPENAI_PRICING },
2930
+ reasoning: { emit: openaiReasoningEmit, capture: "none" },
2931
+ roleFor: (role, model) => role === "system" && isOSeriesModel(model) ? "developer" : role,
2932
+ maxTokensField: "max_completion_tokens",
2933
+ parallelToolCalls: (model) => !isOSeriesModel(model)
2934
+ };
2935
+
2936
+ // src/providers/profiles/xai.ts
2937
+ var acceptsReasoningEffort = (model) => model.startsWith("grok-3-mini");
2938
+ var isReasoningModel = (model) => /^grok-(3-mini|4)/.test(model);
2939
+ var XAI_PROFILE = {
2940
+ name: "xai",
2941
+ label: "xAI",
2942
+ defaultBaseUrl: "https://api.x.ai/v1",
2943
+ envApiKey: "XAI_API_KEY",
2944
+ envBaseUrl: "XAI_BASE_URL",
2945
+ pricing: { kind: "unknown" },
2946
+ reasoning: {
2947
+ emit: reasoningEffortEmit((resolved, model) => {
2948
+ if (!acceptsReasoningEffort(model)) return void 0;
2949
+ if (resolved.thinkingDisabled || !resolved.activeEffort) return void 0;
2950
+ return resolved.activeEffort === "low" ? "low" : "high";
2951
+ }),
2952
+ capture: "reasoning"
2953
+ },
2954
+ capabilities: {
2955
+ forbiddenParams: (model) => isReasoningModel(model) ? ["stop", "presence_penalty", "frequency_penalty"] : []
2956
+ }
2957
+ };
2958
+
2959
+ // src/providers/profiles/deepseek.ts
2960
+ var isReasoner = (model) => /reasoner|v4/i.test(model);
2961
+ var deepseekEmit = (_body, _resolved, model) => ({
2962
+ stripTemperature: isReasoner(model)
2963
+ });
2964
+ var DEEPSEEK_PROFILE = {
2965
+ name: "deepseek",
2966
+ label: "DeepSeek",
2967
+ defaultBaseUrl: "https://api.deepseek.com/v1",
2968
+ envApiKey: "DEEPSEEK_API_KEY",
2969
+ envBaseUrl: "DEEPSEEK_BASE_URL",
2970
+ pricing: { kind: "unknown" },
2971
+ reasoning: {
2972
+ emit: deepseekEmit,
2973
+ capture: "reasoning_content",
2974
+ roundTrip: "on-tool-call-turns"
2975
+ },
2976
+ capabilities: {
2977
+ supportsJsonSchema: false,
2978
+ forbiddenParams: (model) => isReasoner(model) ? ["top_p", "presence_penalty", "frequency_penalty"] : []
2979
+ }
2980
+ };
2981
+
2982
+ // src/providers/profiles/mistral.ts
2983
+ var acceptsReasoningEffort2 = (model) => /^mistral-(small|medium)/i.test(model);
2984
+ var MISTRAL_PROFILE = {
2985
+ name: "mistral",
2986
+ label: "Mistral",
2987
+ defaultBaseUrl: "https://api.mistral.ai/v1",
2988
+ envApiKey: "MISTRAL_API_KEY",
2989
+ envBaseUrl: "MISTRAL_BASE_URL",
2990
+ pricing: { kind: "unknown" },
2991
+ reasoning: {
2992
+ emit: reasoningEffortEmit((resolved, model) => {
2993
+ if (!acceptsReasoningEffort2(model)) return void 0;
2994
+ if (resolved.thinkingDisabled || !resolved.activeEffort) return void 0;
2995
+ return "high";
2996
+ }),
2997
+ capture: "reasoning_content"
2998
+ }
2999
+ };
3000
+
3001
+ // src/providers/profiles/groq.ts
3002
+ var isGptOss = (model) => /gpt-oss/i.test(model);
3003
+ var GROQ_PROFILE = {
3004
+ name: "groq",
3005
+ label: "Groq",
3006
+ defaultBaseUrl: "https://api.groq.com/openai/v1",
3007
+ envApiKey: "GROQ_API_KEY",
3008
+ envBaseUrl: "GROQ_BASE_URL",
3009
+ pricing: { kind: "unknown" },
3010
+ reasoning: {
3011
+ emit: reasoningEffortEmit((resolved, model) => {
3012
+ if (!isGptOss(model) || resolved.thinkingDisabled || !resolved.activeEffort) {
3013
+ return void 0;
3014
+ }
3015
+ const e = resolved.activeEffort;
3016
+ return e === "xhigh" || e === "max" ? "high" : e;
3017
+ }),
3018
+ capture: "reasoning"
3019
+ },
3020
+ capabilities: {
3021
+ emitsMessageName: false,
3022
+ // Groq's `response_format: json_schema` is supported only on a subset of
3023
+ // models — the `openai/gpt-oss-*` family accepts it; llama/gemma/etc 400
3024
+ // with "This model does not support response format `json_schema`".
3025
+ // Per-model so `nativeStructuredOutput` engages the native path only where
3026
+ // it works and otherwise downgrades cleanly to `json_object` (verified live).
3027
+ supportsJsonSchema: isGptOss
3028
+ }
3029
+ };
3030
+
3031
+ // src/providers/profiles/bedrock.ts
3032
+ var isGptOss2 = (model) => /gpt-oss/i.test(model);
3033
+ var BEDROCK_PROFILE = {
3034
+ name: "bedrock",
3035
+ label: "AWS Bedrock",
3036
+ // Never used — requireExplicitBaseUrl forces a real (region-specific) value first.
3037
+ defaultBaseUrl: "https://bedrock-mantle.us-east-1.api.aws/v1",
3038
+ requireExplicitBaseUrl: true,
3039
+ envApiKey: "AWS_BEARER_TOKEN_BEDROCK",
3040
+ envBaseUrl: "BEDROCK_BASE_URL",
3041
+ pricing: { kind: "unknown" },
3042
+ reasoning: {
3043
+ emit: reasoningEffortEmit((resolved, model) => {
3044
+ if (!isGptOss2(model) || resolved.thinkingDisabled || !resolved.activeEffort) {
3045
+ return void 0;
3046
+ }
3047
+ const e = resolved.activeEffort;
3048
+ return e === "xhigh" || e === "max" ? "high" : e;
3049
+ }),
3050
+ capture: "reasoning"
3051
+ }
3052
+ };
3053
+
3054
+ // src/providers/profiles/local.ts
3055
+ function localProfile(name, label, port, envApiKey) {
3056
+ return {
3057
+ name,
3058
+ label,
3059
+ defaultBaseUrl: `http://localhost:${port}/v1`,
3060
+ envApiKey,
3061
+ envBaseUrl: `${envApiKey.replace(/_API_KEY$/, "")}_BASE_URL`,
3062
+ allowMissingApiKey: true,
3063
+ pricing: { kind: "zero" },
3064
+ reasoning: { emit: () => {
3065
+ }, capture: "think_tags" },
3066
+ maxTokensField: "max_tokens"
3067
+ };
3068
+ }
3069
+ var OLLAMA_PROFILE = localProfile("ollama", "Ollama", 11434, "OLLAMA_API_KEY");
3070
+ var VLLM_PROFILE = localProfile("vllm", "vLLM", 8e3, "VLLM_API_KEY");
3071
+ var LMSTUDIO_PROFILE = localProfile("lmstudio", "LM Studio", 1234, "LMSTUDIO_API_KEY");
3072
+ var LLAMACPP_PROFILE = localProfile("llamacpp", "llama.cpp", 8080, "LLAMACPP_API_KEY");
3073
+ var SGLANG_PROFILE = localProfile("sglang", "SGLang", 3e4, "SGLANG_API_KEY");
3074
+ var LOCAL_PROFILES = [
3075
+ OLLAMA_PROFILE,
3076
+ VLLM_PROFILE,
3077
+ LMSTUDIO_PROFILE,
3078
+ LLAMACPP_PROFILE,
3079
+ SGLANG_PROFILE
3080
+ ];
3081
+
3082
+ // src/providers/profiles/index.ts
3083
+ var BUILTIN_PROFILES = [
3084
+ OPENROUTER_PROFILE,
3085
+ AZURE_PROFILE,
3086
+ XAI_PROFILE,
3087
+ DEEPSEEK_PROFILE,
3088
+ MISTRAL_PROFILE,
3089
+ GROQ_PROFILE,
3090
+ BEDROCK_PROFILE,
3091
+ ...LOCAL_PROFILES
3092
+ ];
3093
+
2213
3094
  // src/providers/registry.ts
2214
3095
  var builtinFactories = {
2215
3096
  openai: (config) => {
@@ -2249,6 +3130,23 @@ var builtinFactories = {
2249
3130
  });
2250
3131
  }
2251
3132
  };
3133
+ function presetFactory(profile) {
3134
+ return (config) => {
3135
+ const opts = config.providers?.[profile.name] ?? {};
3136
+ return new OpenAICompatibleProvider({
3137
+ profile,
3138
+ apiKey: opts.apiKey,
3139
+ baseUrl: opts.baseUrl,
3140
+ authHeader: opts.authHeader,
3141
+ rateLimit: opts.rateLimit
3142
+ });
3143
+ };
3144
+ }
3145
+ for (const profile of BUILTIN_PROFILES) {
3146
+ if (!(profile.name in builtinFactories)) {
3147
+ builtinFactories[profile.name] = presetFactory(profile);
3148
+ }
3149
+ }
2252
3150
  var ProviderRegistry = class {
2253
3151
  /** Cached provider instances, keyed by provider name */
2254
3152
  instances = /* @__PURE__ */ new Map();
@@ -2313,175 +3211,58 @@ var ProviderRegistry = class {
2313
3211
  }
2314
3212
  throw new Error(`Unknown provider "${name}". Registered providers: ${this.list().join(", ")}`);
2315
3213
  }
2316
- /**
2317
- * Resolve a "provider:model" URI string into a Provider instance and model name.
2318
- *
2319
- * Supported formats:
2320
- * - "openai:gpt-4o" -> provider=openai, model=gpt-4o
2321
- * - "anthropic:claude-3" -> provider=anthropic, model=claude-3
2322
- * - "gpt-4o" -> uses defaultProvider from config, model=gpt-4o
2323
- * - undefined / empty -> uses defaultProvider and defaultModel from config
2324
- *
2325
- * @param uri Provider:model string, or just a model name
2326
- * @param config Axl configuration for provider options and defaults
2327
- */
2328
- resolve(uri, config = {}) {
2329
- if (!uri) {
2330
- const providerName2 = config.defaultProvider ?? "openai";
2331
- const model2 = config.defaultModel ?? "gpt-4o";
2332
- return { provider: this.get(providerName2, config), model: model2 };
2333
- }
2334
- const colonIndex = uri.indexOf(":");
2335
- if (colonIndex === -1) {
2336
- const providerName2 = config.defaultProvider ?? "openai";
2337
- return { provider: this.get(providerName2, config), model: uri };
2338
- }
2339
- const providerName = uri.slice(0, colonIndex);
2340
- const model = uri.slice(colonIndex + 1);
2341
- if (!providerName || !model) {
2342
- throw new Error(
2343
- `Invalid provider URI "${uri}". Expected format: "provider:model" (e.g. "openai:gpt-4o")`
2344
- );
2345
- }
2346
- return { provider: this.get(providerName, config), model };
2347
- }
2348
- /**
2349
- * Clear all cached provider instances. Useful for testing or reconfiguration.
2350
- */
2351
- clearCache() {
2352
- this.instances.clear();
2353
- }
2354
- /**
2355
- * Clear all registered factories (including built-ins).
2356
- * Useful for test runtimes where only explicitly registered instances should be used.
2357
- */
2358
- clearFactories() {
2359
- this.factories.clear();
2360
- }
2361
- };
2362
- var defaultRegistry = new ProviderRegistry();
2363
-
2364
- // src/context.ts
2365
- var import_node_async_hooks = require("async_hooks");
2366
- var import_node_crypto = require("crypto");
2367
- var import_zod = require("zod");
2368
-
2369
- // src/errors.ts
2370
- var AxlError = class extends Error {
2371
- code;
2372
- constructor(code, message) {
2373
- super(message);
2374
- this.name = "AxlError";
2375
- this.code = code;
2376
- }
2377
- };
2378
- var VerifyError = class extends AxlError {
2379
- lastOutput;
2380
- zodError;
2381
- retries;
2382
- constructor(lastOutput, zodError, retries) {
2383
- super("VERIFY_ERROR", `Schema validation failed after ${retries} retries: ${zodError.message}`);
2384
- this.name = "VerifyError";
2385
- this.lastOutput = lastOutput;
2386
- this.zodError = zodError;
2387
- this.retries = retries;
2388
- }
2389
- };
2390
- var QuorumNotMet = class extends AxlError {
2391
- results;
2392
- constructor(required, actual, results) {
2393
- super("QUORUM_NOT_MET", `Quorum not met: needed ${required} successes, got ${actual}`);
2394
- this.name = "QuorumNotMet";
2395
- this.results = results;
2396
- }
2397
- };
2398
- var NoConsensus = class extends AxlError {
2399
- constructor(reason) {
2400
- super("NO_CONSENSUS", `No consensus: ${reason}`);
2401
- this.name = "NoConsensus";
2402
- }
2403
- };
2404
- var TimeoutError = class extends AxlError {
2405
- constructor(operation, timeoutMs) {
2406
- super("TIMEOUT", `${operation} exceeded timeout of ${timeoutMs}ms`);
2407
- this.name = "TimeoutError";
2408
- }
2409
- };
2410
- function formatBudgetCost(cost) {
2411
- if (Number.isNaN(cost)) return "$NaN";
2412
- if (cost === Infinity) return "$Infinity";
2413
- if (cost === -Infinity) return "-$Infinity";
2414
- if (cost === 0) return "$0.00";
2415
- const sign = cost < 0 ? "-" : "";
2416
- const abs = Math.abs(cost);
2417
- if (abs < 1e-6) return `${sign}< $0.000001`;
2418
- if (abs < 1e-4) {
2419
- const [mantissa, exponent] = abs.toExponential(2).split("e");
2420
- return `${sign}$${mantissa}e${parseInt(exponent, 10)}`;
2421
- }
2422
- if (abs < 0.01) return `${sign}$${abs.toFixed(6)}`;
2423
- return `${sign}$${abs.toFixed(2)}`;
2424
- }
2425
- var BudgetExceededError = class extends AxlError {
2426
- limit;
2427
- spent;
2428
- policy;
2429
- constructor(limit, spent, policy) {
2430
- super(
2431
- "BUDGET_EXCEEDED",
2432
- `Budget exceeded: spent ${formatBudgetCost(spent)} of ${formatBudgetCost(
2433
- limit
2434
- )} limit (policy: ${policy})`
2435
- );
2436
- this.name = "BudgetExceededError";
2437
- this.limit = limit;
2438
- this.spent = spent;
2439
- this.policy = policy;
2440
- }
2441
- };
2442
- var MaxTurnsError = class extends AxlError {
2443
- maxTurns;
2444
- constructor(operation, maxTurns) {
2445
- super("MAX_TURNS", `${operation} exceeded maximum of ${maxTurns} turns`);
2446
- this.name = "MaxTurnsError";
2447
- this.maxTurns = maxTurns;
2448
- }
2449
- };
2450
- var GuardrailError = class extends AxlError {
2451
- guardrailType;
2452
- reason;
2453
- constructor(guardrailType, reason) {
2454
- super("GUARDRAIL_BLOCKED", `${guardrailType} guardrail blocked: ${reason}`);
2455
- this.name = "GuardrailError";
2456
- this.guardrailType = guardrailType;
2457
- this.reason = reason;
3214
+ /**
3215
+ * Resolve a "provider:model" URI string into a Provider instance and model name.
3216
+ *
3217
+ * Supported formats:
3218
+ * - "openai:gpt-4o" -> provider=openai, model=gpt-4o
3219
+ * - "anthropic:claude-3" -> provider=anthropic, model=claude-3
3220
+ * - "gpt-4o" -> uses defaultProvider from config, model=gpt-4o
3221
+ * - undefined / empty -> uses defaultProvider and defaultModel from config
3222
+ *
3223
+ * @param uri Provider:model string, or just a model name
3224
+ * @param config Axl configuration for provider options and defaults
3225
+ */
3226
+ resolve(uri, config = {}) {
3227
+ if (!uri) {
3228
+ const providerName2 = config.defaultProvider ?? "openai";
3229
+ const model2 = config.defaultModel ?? "gpt-4o";
3230
+ return { provider: this.get(providerName2, config), model: model2 };
3231
+ }
3232
+ const colonIndex = uri.indexOf(":");
3233
+ if (colonIndex === -1) {
3234
+ const providerName2 = config.defaultProvider ?? "openai";
3235
+ return { provider: this.get(providerName2, config), model: uri };
3236
+ }
3237
+ const providerName = uri.slice(0, colonIndex);
3238
+ const model = uri.slice(colonIndex + 1);
3239
+ if (!providerName || !model) {
3240
+ throw new Error(
3241
+ `Invalid provider URI "${uri}". Expected format: "provider:model" (e.g. "openai:gpt-4o")`
3242
+ );
3243
+ }
3244
+ return { provider: this.get(providerName, config), model };
2458
3245
  }
2459
- };
2460
- var ValidationError = class extends AxlError {
2461
- lastOutput;
2462
- reason;
2463
- retries;
2464
- constructor(lastOutput, reason, retries) {
2465
- super("VALIDATION_ERROR", `Validation failed after ${retries} retries: ${reason}`);
2466
- this.name = "ValidationError";
2467
- this.lastOutput = lastOutput;
2468
- this.reason = reason;
2469
- this.retries = retries;
3246
+ /**
3247
+ * Clear all cached provider instances. Useful for testing or reconfiguration.
3248
+ */
3249
+ clearCache() {
3250
+ this.instances.clear();
2470
3251
  }
2471
- };
2472
- var ToolDenied = class extends AxlError {
2473
- toolName;
2474
- agentName;
2475
- constructor(toolName, agentName) {
2476
- super(
2477
- "TOOL_DENIED",
2478
- `Agent "${agentName}" attempted to call tool "${toolName}" which is not in its ACL`
2479
- );
2480
- this.name = "ToolDenied";
2481
- this.toolName = toolName;
2482
- this.agentName = agentName;
3252
+ /**
3253
+ * Clear all registered factories (including built-ins).
3254
+ * Useful for test runtimes where only explicitly registered instances should be used.
3255
+ */
3256
+ clearFactories() {
3257
+ this.factories.clear();
2483
3258
  }
2484
3259
  };
3260
+ var defaultRegistry = new ProviderRegistry();
3261
+
3262
+ // src/context.ts
3263
+ var import_node_async_hooks = require("async_hooks");
3264
+ var import_node_crypto = require("crypto");
3265
+ var import_zod = require("zod");
2485
3266
 
2486
3267
  // src/partial-json.ts
2487
3268
  var MAX_DEPTH = 256;
@@ -3057,6 +3838,14 @@ var COST_BEARING_LEAF_TYPES = [
3057
3838
  "memory_recall"
3058
3839
  ];
3059
3840
  var COST_LEAF_SET = new Set(COST_BEARING_LEAF_TYPES);
3841
+ function hasPositiveTokens(event) {
3842
+ const t = event.tokens;
3843
+ if (!t) return false;
3844
+ return (t.input ?? 0) + (t.output ?? 0) + (t.reasoning ?? 0) > 0;
3845
+ }
3846
+ function isUsableCost(cost) {
3847
+ return typeof cost === "number" && Number.isFinite(cost) && cost >= 0;
3848
+ }
3060
3849
  function eventCostContribution(event) {
3061
3850
  if (event.type === "ask_end") return 0;
3062
3851
  const c = event.cost;
@@ -3065,6 +3854,9 @@ function eventCostContribution(event) {
3065
3854
  function isCostBearingLeaf(event) {
3066
3855
  return COST_LEAF_SET.has(event.type);
3067
3856
  }
3857
+ function isUnpricedLeaf(event) {
3858
+ return event.type !== void 0 && COST_LEAF_SET.has(event.type) && !isUsableCost(event.cost) && hasPositiveTokens(event);
3859
+ }
3068
3860
  function isRootLevel(event) {
3069
3861
  const d = event.depth;
3070
3862
  return (d ?? 0) === 0;
@@ -3113,6 +3905,10 @@ var AXL_EVENT_TYPES = [
3113
3905
  "string_delta",
3114
3906
  // Verification
3115
3907
  "verify",
3908
+ // Schema capability diagnostics (spec 22, Problem E) — surface the silent
3909
+ // structured-output cliffs (oversized appended schema, dropped refinements,
3910
+ // streaming disabled, zero-guidance `schemaPrompt:'none'`).
3911
+ "schema_diagnostic",
3116
3912
  // Observability
3117
3913
  "log",
3118
3914
  "memory_remember",
@@ -3155,6 +3951,7 @@ var LIFECYCLE_TYPES = /* @__PURE__ */ new Set([
3155
3951
  "delegate",
3156
3952
  "pipeline",
3157
3953
  "verify",
3954
+ "schema_diagnostic",
3158
3955
  "workflow_start",
3159
3956
  "workflow_end",
3160
3957
  "checkpoint_save",
@@ -3959,6 +4756,11 @@ var REDACTION_RULES = {
3959
4756
  // Scrub only the delta — preserving path keeps "which fields stream" telemetry.
3960
4757
  string_delta: (e) => ({ ...e, data: { ...e.data, delta: REDACTED } }),
3961
4758
  verify: (e) => e.data.lastError !== void 0 ? { ...e, data: { ...e.data, lastError: REDACTED } } : e,
4759
+ // `schema_diagnostic` carries only structural schema metadata — token counts,
4760
+ // thresholds, JSON-Schema field paths, a root type name, a tool name — none of
4761
+ // which is user content or PII (field paths are schema-shape, preserved like
4762
+ // `string_delta.path`). Passthrough, as an explicit reviewed decision.
4763
+ schema_diagnostic: passthrough,
3962
4764
  log: (e) => {
3963
4765
  if (!e.data || typeof e.data !== "object" || Array.isArray(e.data)) return e;
3964
4766
  return { ...e, data: walkObjectOneLevel(e.data, LOG_PRESERVE_KEYS) };
@@ -3991,6 +4793,80 @@ function redactEvent(event) {
3991
4793
  return rule(event);
3992
4794
  }
3993
4795
 
4796
+ // src/schema-diagnostics.ts
4797
+ var DEFAULT_SCHEMA_OVERSIZED_TOKENS = 4e3;
4798
+ var refinementCache = /* @__PURE__ */ new WeakMap();
4799
+ function detectDroppedRefinements(schema) {
4800
+ const cached = refinementCache.get(schema);
4801
+ if (cached) return cached;
4802
+ const paths = /* @__PURE__ */ new Set();
4803
+ let count = 0;
4804
+ const visited = /* @__PURE__ */ new WeakSet();
4805
+ const walk = (node, path) => {
4806
+ if (node === null || typeof node !== "object") return;
4807
+ if (visited.has(node)) return;
4808
+ visited.add(node);
4809
+ const def = node._zod?.def;
4810
+ if (!def) return;
4811
+ const here = () => paths.add(path === "" ? "<root>" : path);
4812
+ const checks = def.checks;
4813
+ if (Array.isArray(checks)) {
4814
+ for (const check of checks) {
4815
+ const kind = check?._zod?.def?.check;
4816
+ if (kind === "custom") {
4817
+ count += 1;
4818
+ here();
4819
+ }
4820
+ }
4821
+ }
4822
+ if (def.check === "custom") {
4823
+ count += 1;
4824
+ here();
4825
+ }
4826
+ const join2 = (child) => path === "" ? child : `${path}.${child}`;
4827
+ const shape = def.shape;
4828
+ if (shape && typeof shape === "object") {
4829
+ for (const [key, child] of Object.entries(shape)) walk(child, join2(key));
4830
+ }
4831
+ if ("element" in def) walk(def.element, `${path}[]`);
4832
+ if (Array.isArray(def.options)) {
4833
+ for (const opt of def.options) walk(opt, path);
4834
+ }
4835
+ if ("innerType" in def) walk(def.innerType, path);
4836
+ if (Array.isArray(def.items)) {
4837
+ def.items.forEach((item, i) => walk(item, `${path}[${i}]`));
4838
+ }
4839
+ if ("rest" in def && def.rest) walk(def.rest, `${path}[]`);
4840
+ if ("catchall" in def && def.catchall) walk(def.catchall, `${path}[*]`);
4841
+ if ("keyType" in def) walk(def.keyType, `${path}[key]`);
4842
+ if ("valueType" in def) walk(def.valueType, `${path}[*]`);
4843
+ if ("in" in def) walk(def.in, path);
4844
+ if ("out" in def) walk(def.out, path);
4845
+ if ("left" in def) walk(def.left, path);
4846
+ if ("right" in def) walk(def.right, path);
4847
+ if (typeof def.getter === "function") {
4848
+ try {
4849
+ walk(def.getter(), path);
4850
+ } catch {
4851
+ }
4852
+ }
4853
+ };
4854
+ walk(schema, "");
4855
+ const result = { count, paths: [...paths] };
4856
+ refinementCache.set(schema, result);
4857
+ return result;
4858
+ }
4859
+ var DOCS_URL = "docs/observability.md#schema-diagnostics";
4860
+ var MAX_WARNED_KEYS = 2048;
4861
+ var warnedDiagnosticKeys = /* @__PURE__ */ new Set();
4862
+ function warnSchemaDiagnosticOnce(key, message, silent) {
4863
+ if (silent || process.env.AXL_DIAGNOSTICS_SILENT === "true") return;
4864
+ if (warnedDiagnosticKeys.has(key)) return;
4865
+ if (warnedDiagnosticKeys.size >= MAX_WARNED_KEYS) warnedDiagnosticKeys.clear();
4866
+ warnedDiagnosticKeys.add(key);
4867
+ console.warn(`[axl] ${message} See ${DOCS_URL}.`);
4868
+ }
4869
+
3994
4870
  // src/config.ts
3995
4871
  function defineConfig(config) {
3996
4872
  return config;
@@ -4066,14 +4942,233 @@ function resolveConfig(config) {
4066
4942
  return resolved;
4067
4943
  }
4068
4944
 
4945
+ // src/tool-model-output.ts
4946
+ var import_node_util = require("util");
4947
+ function invalid(path, reason) {
4948
+ return new Error(`Invalid tool model output at ${path}: ${reason}`);
4949
+ }
4950
+ function propertyPath(parent, key) {
4951
+ if (/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key)) return `${parent}.${key}`;
4952
+ let escaped = "";
4953
+ for (const character of key) {
4954
+ const code = character.charCodeAt(0);
4955
+ if (character === "\\" || character === '"') escaped += `\\${character}`;
4956
+ else if (code <= 31 || code === 8232 || code === 8233) {
4957
+ escaped += `\\u${code.toString(16).padStart(4, "0")}`;
4958
+ } else escaped += character;
4959
+ }
4960
+ return `${parent}["${escaped}"]`;
4961
+ }
4962
+ function normalize(value, path, stack) {
4963
+ if (value === null || typeof value === "string" || typeof value === "boolean") return value;
4964
+ if (typeof value === "number") {
4965
+ if (!Number.isFinite(value)) throw invalid(path, "number must be finite");
4966
+ return value;
4967
+ }
4968
+ if (value === void 0) throw invalid(path, "undefined is only allowed on object properties");
4969
+ if (typeof value === "bigint" || typeof value === "function" || typeof value === "symbol") {
4970
+ throw invalid(path, `unsupported ${typeof value}`);
4971
+ }
4972
+ if (typeof value !== "object") throw invalid(path, `unsupported ${typeof value}`);
4973
+ let prototype;
4974
+ let descriptors;
4975
+ try {
4976
+ prototype = Object.getPrototypeOf(value);
4977
+ descriptors = Object.getOwnPropertyDescriptors(value);
4978
+ Object.setPrototypeOf(descriptors, null);
4979
+ } catch (cause) {
4980
+ throw invalid(
4981
+ path,
4982
+ `object inspection failed (${cause instanceof Error ? cause.name : "error"})`
4983
+ );
4984
+ }
4985
+ if (stack.has(value)) throw invalid(path, "cyclic reference");
4986
+ for (const key of Reflect.ownKeys(descriptors)) {
4987
+ const descriptor = descriptors[key];
4988
+ if (descriptor && !("value" in descriptor)) {
4989
+ throw invalid(
4990
+ typeof key === "string" ? propertyPath(path, key) : `${path}[symbol]`,
4991
+ "accessor properties are not supported"
4992
+ );
4993
+ }
4994
+ if (typeof key === "symbol" && descriptor?.enumerable) {
4995
+ throw invalid(`${path}[symbol]`, "enumerable symbol keys are not supported");
4996
+ }
4997
+ }
4998
+ if (typeof descriptors.toJSON?.value === "function") {
4999
+ throw invalid(propertyPath(path, "toJSON"), "custom toJSON is not supported");
5000
+ }
5001
+ const array = Array.isArray(value);
5002
+ if (array && prototype !== Array.prototype && prototype !== null) {
5003
+ throw invalid(path, "array subclasses are not supported");
5004
+ }
5005
+ const inheritedToJSON = prototype === Array.prototype || prototype === Object.prototype ? Object.getOwnPropertyDescriptor(prototype, "toJSON") : void 0;
5006
+ if (typeof inheritedToJSON?.value === "function") {
5007
+ throw invalid(propertyPath(path, "toJSON"), "inherited custom toJSON is not supported");
5008
+ }
5009
+ const inheritedThen = prototype === Array.prototype || prototype === Object.prototype ? Object.getOwnPropertyDescriptor(prototype, "then") : void 0;
5010
+ if (typeof descriptors.then?.value === "function" || inheritedThen !== void 0 && (!("value" in inheritedThen) || typeof inheritedThen.value === "function")) {
5011
+ throw invalid(path, "thenable output is not supported");
5012
+ }
5013
+ stack.add(value);
5014
+ try {
5015
+ if (array) {
5016
+ const normalized2 = [];
5017
+ const length = descriptors.length?.value;
5018
+ if (!Number.isSafeInteger(length) || length < 0) {
5019
+ throw invalid(propertyPath(path, "length"), "array length is invalid");
5020
+ }
5021
+ for (let index = 0; index < length; index++) {
5022
+ const descriptor = descriptors[String(index)];
5023
+ if (!descriptor || !("value" in descriptor)) {
5024
+ throw invalid(`${path}[${index}]`, "arrays must be dense data properties");
5025
+ }
5026
+ if (descriptor.value === void 0) {
5027
+ throw invalid(`${path}[${index}]`, "array values cannot be undefined");
5028
+ }
5029
+ normalized2.push(normalize(descriptor.value, `${path}[${index}]`, stack));
5030
+ }
5031
+ for (const key of Object.keys(descriptors)) {
5032
+ const descriptor = descriptors[key];
5033
+ if (!descriptor?.enumerable) continue;
5034
+ const index = Number(key);
5035
+ if (!Number.isInteger(index) || index < 0 || index >= length || String(index) !== key) {
5036
+ throw invalid(propertyPath(path, key), "non-index array properties are not supported");
5037
+ }
5038
+ }
5039
+ Object.setPrototypeOf(normalized2, null);
5040
+ return normalized2;
5041
+ }
5042
+ if (prototype !== Object.prototype && prototype !== null) {
5043
+ throw invalid(path, "only plain objects and null-prototype records are supported");
5044
+ }
5045
+ const normalized = /* @__PURE__ */ Object.create(null);
5046
+ for (const key of Object.keys(descriptors)) {
5047
+ const descriptor = descriptors[key];
5048
+ if (!descriptor?.enumerable) continue;
5049
+ if (!("value" in descriptor)) {
5050
+ throw invalid(propertyPath(path, key), "accessor properties are not supported");
5051
+ }
5052
+ if (descriptor.value === void 0) continue;
5053
+ normalized[key] = normalize(descriptor.value, propertyPath(path, key), stack);
5054
+ }
5055
+ return normalized;
5056
+ } finally {
5057
+ stack.delete(value);
5058
+ }
5059
+ }
5060
+ function serializeToolModelOutput(toolName, output) {
5061
+ try {
5062
+ if (import_node_util.types.isPromise(output)) {
5063
+ void Promise.prototype.then.call(output, void 0, () => void 0);
5064
+ }
5065
+ if (typeof output === "string") return output;
5066
+ return JSON.stringify(normalize(output, "$", /* @__PURE__ */ new WeakSet()));
5067
+ } catch (cause) {
5068
+ throw new ToolModelOutputError(toolName, cause);
5069
+ }
5070
+ }
5071
+
4069
5072
  // src/context.ts
4070
5073
  var signalStorage = new import_node_async_hooks.AsyncLocalStorage();
5074
+ function isErrorShaped(value) {
5075
+ return value !== null && typeof value === "object" && "error" in value;
5076
+ }
5077
+ function isErrorShapedSafely(value) {
5078
+ try {
5079
+ return isErrorShaped(value);
5080
+ } catch {
5081
+ return false;
5082
+ }
5083
+ }
5084
+ function recordToolSpanResult(span, value, suppressInspectionErrors) {
5085
+ let isError = false;
5086
+ let errorMessage;
5087
+ const inspect = () => {
5088
+ if (isErrorShaped(value)) {
5089
+ isError = true;
5090
+ errorMessage = value.error;
5091
+ } else {
5092
+ isError = false;
5093
+ }
5094
+ };
5095
+ if (suppressInspectionErrors) {
5096
+ try {
5097
+ inspect();
5098
+ } catch {
5099
+ isError = false;
5100
+ errorMessage = void 0;
5101
+ }
5102
+ } else {
5103
+ inspect();
5104
+ }
5105
+ span.setAttribute("axl.tool.success", !isError);
5106
+ if (isError) span.setStatus("error", errorMessage);
5107
+ }
5108
+ function prepareToolModelOutput(toolName, project) {
5109
+ let output;
5110
+ try {
5111
+ output = project();
5112
+ } catch (cause) {
5113
+ throw new ToolModelOutputError(toolName, cause);
5114
+ }
5115
+ return serializeToolModelOutput(toolName, output);
5116
+ }
5117
+ function finalizeToolResult(options) {
5118
+ const { toolName, configuredTool, outcome, legacyContent, emitEnd, beforeProjection } = options;
5119
+ const projectionEligible = configuredTool !== void 0 && !configuredTool.sensitive && outcome.kind === "returned" && configuredTool.toModelOutput !== void 0;
5120
+ if (projectionEligible) {
5121
+ emitEnd();
5122
+ beforeProjection();
5123
+ const project = configuredTool.toModelOutput;
5124
+ return prepareToolModelOutput(toolName, () => project(outcome.value));
5125
+ }
5126
+ const content = legacyContent();
5127
+ emitEnd();
5128
+ return content;
5129
+ }
4071
5130
  var askStorage = new import_node_async_hooks.AsyncLocalStorage();
5131
+ var inlineSchemaCache = /* @__PURE__ */ new WeakMap();
4072
5132
  function zodToJsonSchema(schema) {
5133
+ const cached = inlineSchemaCache.get(schema);
5134
+ if (cached !== void 0) return cached;
4073
5135
  const result = import_zod.z.toJSONSchema(schema, { unrepresentable: "any" });
4074
5136
  delete result.$schema;
5137
+ Object.freeze(result);
5138
+ inlineSchemaCache.set(schema, result);
4075
5139
  return result;
4076
5140
  }
5141
+ var promptSchemaCache = /* @__PURE__ */ new WeakMap();
5142
+ var inlineSchemaTokenCache = /* @__PURE__ */ new WeakMap();
5143
+ function estimateInlineSchemaTokens(schema) {
5144
+ const cached = inlineSchemaTokenCache.get(schema);
5145
+ if (cached !== void 0) return cached;
5146
+ const tokens = estimateTokens(JSON.stringify(zodToJsonSchema(schema)));
5147
+ inlineSchemaTokenCache.set(schema, tokens);
5148
+ return tokens;
5149
+ }
5150
+ var nativeSchemaCache = /* @__PURE__ */ new WeakMap();
5151
+ function deriveNativeSchema(schema) {
5152
+ const cached = nativeSchemaCache.get(schema);
5153
+ if (cached !== void 0) return cached;
5154
+ const json = import_zod.z.toJSONSchema(schema, { unrepresentable: "any", io: "input" });
5155
+ delete json.$schema;
5156
+ nativeSchemaCache.set(schema, json);
5157
+ return json;
5158
+ }
5159
+ function renderSchemaForPrompt(schema) {
5160
+ const cached = promptSchemaCache.get(schema);
5161
+ if (cached !== void 0) return cached;
5162
+ const json = import_zod.z.toJSONSchema(schema, {
5163
+ unrepresentable: "any",
5164
+ io: "input",
5165
+ reused: "ref"
5166
+ });
5167
+ delete json.$schema;
5168
+ const rendered = JSON.stringify(json);
5169
+ promptSchemaCache.set(schema, rendered);
5170
+ return rendered;
5171
+ }
4077
5172
  function estimateTokens(text) {
4078
5173
  return Math.ceil(text.length / 4);
4079
5174
  }
@@ -4454,12 +5549,14 @@ var WorkflowContext = class _WorkflowContext {
4454
5549
  depth,
4455
5550
  agent: agent2._name,
4456
5551
  stepRef,
4457
- askCost: { value: 0 }
5552
+ askCost: { value: 0 },
5553
+ askUnpriced: false
4458
5554
  };
4459
5555
  return askStorage.run(frame, async () => {
4460
5556
  const askStart = Date.now();
4461
5557
  this.emitEvent({ type: "ask_start", prompt });
4462
5558
  const costBefore = this.budgetContext?.totalCost ?? 0;
5559
+ const unpricedCountBefore = this.budgetContext?.unpricedCount ?? 0;
4463
5560
  const resolveCtx = options?.metadata ? { metadata: { ...this.metadata, ...options.metadata } } : { metadata: this.metadata };
4464
5561
  const usageCapture = {};
4465
5562
  const doCall = async () => {
@@ -4505,6 +5602,7 @@ var WorkflowContext = class _WorkflowContext {
4505
5602
  ) : await doCall();
4506
5603
  outcome = { ok: true, result };
4507
5604
  const costAfter = this.budgetContext?.totalCost ?? 0;
5605
+ const unpricedCountAfter = this.budgetContext?.unpricedCount ?? 0;
4508
5606
  if (this.onAgentCallComplete) {
4509
5607
  try {
4510
5608
  this.onAgentCallComplete({
@@ -4513,6 +5611,7 @@ var WorkflowContext = class _WorkflowContext {
4513
5611
  response: typeof result === "string" ? result : JSON.stringify(result),
4514
5612
  model: agent2.resolveModel(resolveCtx),
4515
5613
  cost: costAfter - costBefore,
5614
+ unpriced: frame.askUnpriced || unpricedCountAfter > unpricedCountBefore,
4516
5615
  duration: Date.now() - askStart,
4517
5616
  promptVersion: agent2._config.version,
4518
5617
  temperature: options?.temperature ?? agent2._config.temperature,
@@ -4547,6 +5646,7 @@ var WorkflowContext = class _WorkflowContext {
4547
5646
  error: "ask_end emitted without outcome \u2014 internal bug"
4548
5647
  },
4549
5648
  cost: frame.askCost.value,
5649
+ ...frame.askUnpriced ? { unpriced: true } : {},
4550
5650
  duration: Date.now() - askStart
4551
5651
  });
4552
5652
  } catch (emitErr) {
@@ -4630,15 +5730,32 @@ var WorkflowContext = class _WorkflowContext {
4630
5730
  messages.push(msg);
4631
5731
  }
4632
5732
  }
5733
+ const schemaPromptMode = options?.schemaPrompt ?? agent2._config.schemaPrompt ?? "json-schema";
4633
5734
  let userContent = prompt;
5735
+ let appendedSchemaText;
4634
5736
  if (options?.schema) {
4635
- const jsonSchema = zodToJsonSchema(options.schema);
4636
- userContent += `
5737
+ const schema = options.schema;
5738
+ if (schemaPromptMode === "none") {
5739
+ } else if (schemaPromptMode === "json-schema") {
5740
+ appendedSchemaText = renderSchemaForPrompt(schema);
5741
+ userContent += `
4637
5742
 
4638
5743
  Respond with valid JSON matching this schema:
4639
- ${JSON.stringify(jsonSchema, null, 2)}`;
5744
+ ${appendedSchemaText}`;
5745
+ } else {
5746
+ appendedSchemaText = typeof schemaPromptMode.render === "function" ? schemaPromptMode.render(schema) : schemaPromptMode.render;
5747
+ userContent += `
5748
+
5749
+ ${appendedSchemaText}`;
5750
+ }
4640
5751
  }
4641
5752
  messages.push({ role: "user", content: userContent });
5753
+ this.emitSchemaDiagnostics(agent2, options, toolDefs, {
5754
+ mode: schemaPromptMode,
5755
+ appendedSchemaText,
5756
+ provider,
5757
+ model
5758
+ });
4642
5759
  if (handoffMessages && handoffMessages.length > 0) {
4643
5760
  const handoffContext = handoffMessages.filter(
4644
5761
  (m) => m.role === "assistant" || m.role === "tool"
@@ -4704,6 +5821,7 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
4704
5821
  let lastStartAttempt = 1;
4705
5822
  let lastStartMaxAttempts = 1;
4706
5823
  while (turns < maxTurns) {
5824
+ this.currentSignal?.throwIfAborted();
4707
5825
  if (Date.now() - startTime > timeoutMs) {
4708
5826
  throw new TimeoutError("ctx.ask()", timeoutMs);
4709
5827
  }
@@ -4751,7 +5869,17 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
4751
5869
  signal: this.currentSignal
4752
5870
  };
4753
5871
  if (options?.schema && toolDefs.length === 0) {
4754
- chatOptions.responseFormat = { type: "json_object" };
5872
+ const nativeOut = options?.nativeStructuredOutput ?? agent2._config.nativeStructuredOutput ?? false;
5873
+ chatOptions.responseFormat = nativeOut ? {
5874
+ type: "json_schema",
5875
+ json_schema: {
5876
+ name: "response",
5877
+ // Input-side rendering — see `deriveNativeSchema`. Keeps the
5878
+ // native schema consistent with the prompt and non-empty for
5879
+ // `.transform()` schemas.
5880
+ schema: deriveNativeSchema(options.schema)
5881
+ }
5882
+ } : { type: "json_object" };
4755
5883
  }
4756
5884
  const callbackMeta = this.currentCallbackMeta(agent2._name);
4757
5885
  const callParams = {
@@ -4795,6 +5923,7 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
4795
5923
  this.onAgentStart?.({ agent: agent2._name, model: modelUri }, callbackMeta);
4796
5924
  let response;
4797
5925
  try {
5926
+ this.currentSignal?.throwIfAborted();
4798
5927
  if (this._streamingEnabled) {
4799
5928
  let content2 = "";
4800
5929
  const toolCalls = [];
@@ -4869,10 +5998,7 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
4869
5998
  }
4870
5999
  });
4871
6000
  }
4872
- response ??= {
4873
- content: content2,
4874
- usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 }
4875
- };
6001
+ response ??= { content: content2 };
4876
6002
  if (toolCalls.length > 0) {
4877
6003
  response.tool_calls = toolCalls;
4878
6004
  }
@@ -4896,7 +6022,11 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
4896
6022
  response: "",
4897
6023
  turn: turns,
4898
6024
  ...retryReason ? { retryReason } : {},
4899
- error: err instanceof Error ? err.message : String(err)
6025
+ error: err instanceof Error ? err.message : String(err),
6026
+ // Enrich with typed-error metadata when available. `body` is
6027
+ // deliberately NOT emitted — it's redaction-eligible (see
6028
+ // docs/security.md). `data.error` already carries the message.
6029
+ ...err instanceof ProviderError ? { status: err.status, retryable: err.retryable } : {}
4900
6030
  }
4901
6031
  });
4902
6032
  throw err;
@@ -4960,10 +6090,13 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
4960
6090
  depth: handoffTargetDepth,
4961
6091
  agent: descriptor.agent._name,
4962
6092
  stepRef: sourceFrame?.stepRef ?? this.stepRefRoot,
4963
- askCost: { value: 0 }
6093
+ askCost: { value: 0 },
6094
+ askUnpriced: false
4964
6095
  };
4965
6096
  const handoffOptions = options ? {
4966
6097
  schema: options.schema,
6098
+ schemaPrompt: options.schemaPrompt,
6099
+ nativeStructuredOutput: options.nativeStructuredOutput,
4967
6100
  retries: options.retries,
4968
6101
  metadata: options.metadata,
4969
6102
  validate: options.validate,
@@ -4984,6 +6117,7 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
4984
6117
  type: "ask_end",
4985
6118
  outcome: { ok: true, result },
4986
6119
  cost: targetFrame.askCost.value,
6120
+ ...targetFrame.askUnpriced ? { unpriced: true } : {},
4987
6121
  duration: Date.now() - targetAskStart
4988
6122
  });
4989
6123
  return result;
@@ -4995,6 +6129,7 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
4995
6129
  error: err instanceof Error ? err.message : String(err)
4996
6130
  },
4997
6131
  cost: targetFrame.askCost.value,
6132
+ ...targetFrame.askUnpriced ? { unpriced: true } : {},
4998
6133
  duration: Date.now() - targetAskStart
4999
6134
  });
5000
6135
  throw err;
@@ -5090,6 +6225,8 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
5090
6225
  return await handoffFn();
5091
6226
  }
5092
6227
  }
6228
+ const tool2 = agent2._config.tools?.find((candidate) => candidate.name === toolName);
6229
+ const isMcpTool = !tool2 && this.mcpManager?.isMcpTool(toolName);
5093
6230
  const toolOverride = this.toolOverrides?.get(toolName);
5094
6231
  if (toolOverride) {
5095
6232
  let toolArgs2;
@@ -5114,38 +6251,47 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
5114
6251
  callbackMeta
5115
6252
  );
5116
6253
  const toolStart2 = Date.now();
6254
+ const suppressErrorShapeInspection2 = tool2?.sensitive === true || tool2?.toModelOutput !== void 0;
5117
6255
  const executeOverride = async () => {
5118
- let toolResult3;
5119
6256
  try {
5120
- toolResult3 = await toolOverride(toolArgs2);
6257
+ return { kind: "returned", value: await toolOverride(toolArgs2) };
5121
6258
  } catch (err) {
5122
- toolResult3 = { error: err instanceof Error ? err.message : String(err) };
6259
+ return {
6260
+ kind: "thrown",
6261
+ value: { error: err instanceof Error ? err.message : String(err) }
6262
+ };
5123
6263
  }
5124
- return toolResult3;
5125
6264
  };
5126
- const toolResult2 = this.spanManager ? await this.spanManager.withSpanAsync(
6265
+ const outcome = this.spanManager ? await this.spanManager.withSpanAsync(
5127
6266
  "axl.tool.call",
5128
6267
  {
5129
6268
  "axl.tool.name": toolName,
5130
6269
  "axl.agent.name": agent2._name
5131
6270
  },
5132
6271
  async (span) => {
5133
- const r = await executeOverride();
6272
+ const result = await executeOverride();
5134
6273
  span.setAttribute("axl.tool.duration", Date.now() - toolStart2);
5135
- const isError = r && typeof r === "object" && "error" in r;
5136
- span.setAttribute("axl.tool.success", !isError);
5137
- if (isError)
5138
- span.setStatus("error", r.error);
5139
- return r;
6274
+ recordToolSpanResult(span, result.value, suppressErrorShapeInspection2);
6275
+ return result;
5140
6276
  }
5141
6277
  ) : await executeOverride();
5142
- const resultContent2 = JSON.stringify(toolResult2);
5143
- this.emitEvent({
5144
- type: "tool_call_end",
5145
- agent: agent2._name,
5146
- tool: toolName,
5147
- duration: Date.now() - toolStart2,
5148
- data: { args: toolArgs2, result: toolResult2, callId: toolCall.id }
6278
+ const toolResult2 = outcome.value;
6279
+ const resultContent2 = finalizeToolResult({
6280
+ toolName,
6281
+ configuredTool: tool2,
6282
+ outcome,
6283
+ legacyContent: () => tool2?.sensitive ? "[REDACTED - sensitive tool output]" : JSON.stringify(toolResult2),
6284
+ emitEnd: () => {
6285
+ const toolDuration2 = Date.now() - toolStart2;
6286
+ this.emitEvent({
6287
+ type: "tool_call_end",
6288
+ agent: agent2._name,
6289
+ tool: toolName,
6290
+ duration: toolDuration2,
6291
+ data: { args: toolArgs2, result: toolResult2, callId: toolCall.id }
6292
+ });
6293
+ },
6294
+ beforeProjection: () => this.currentSignal?.throwIfAborted()
5149
6295
  });
5150
6296
  currentMessages.push({
5151
6297
  role: "tool",
@@ -5154,8 +6300,6 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
5154
6300
  });
5155
6301
  continue;
5156
6302
  }
5157
- const tool2 = agent2._config.tools?.find((t) => t.name === toolName);
5158
- const isMcpTool = !tool2 && this.mcpManager?.isMcpTool(toolName);
5159
6303
  if (!tool2 && !isMcpTool) {
5160
6304
  this.emitEvent({ type: "tool_denied", agent: agent2._name, tool: toolName });
5161
6305
  currentMessages.push({
@@ -5184,6 +6328,7 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
5184
6328
  });
5185
6329
  this.onToolCall?.({ name: toolName, args: toolArgs, callId: toolCall.id }, callbackMeta);
5186
6330
  const toolStart = Date.now();
6331
+ const suppressErrorShapeInspection = tool2?.sensitive === true || tool2?.toModelOutput !== void 0;
5187
6332
  if (tool2 && tool2.requireApproval) {
5188
6333
  const approvalFn = async () => {
5189
6334
  const decision = await this.awaitHuman({
@@ -5245,12 +6390,12 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
5245
6390
  }
5246
6391
  }
5247
6392
  const executeTool = async () => {
5248
- let toolResult2;
6393
+ let outcome;
5249
6394
  let resultContent2;
5250
6395
  if (isMcpTool && this.mcpManager) {
5251
6396
  try {
5252
6397
  const mcpResult = await this.mcpManager.callTool(toolName, toolArgs);
5253
- toolResult2 = mcpResult;
6398
+ outcome = { kind: "returned", value: mcpResult };
5254
6399
  resultContent2 = mcpResult.content.map(
5255
6400
  (c) => c.type === "text" ? c.text : `[${c.type}]`
5256
6401
  ).join("\n");
@@ -5258,34 +6403,50 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
5258
6403
  resultContent2 = `Error: ${resultContent2}`;
5259
6404
  }
5260
6405
  } catch (err) {
5261
- toolResult2 = { error: err instanceof Error ? err.message : String(err) };
5262
- resultContent2 = JSON.stringify(toolResult2);
6406
+ outcome = {
6407
+ kind: "thrown",
6408
+ value: { error: err instanceof Error ? err.message : String(err) }
6409
+ };
6410
+ resultContent2 = JSON.stringify(outcome.value);
5263
6411
  }
5264
6412
  } else if (tool2) {
5265
6413
  const childCtx = this.createChildContext();
5266
6414
  try {
5267
- toolResult2 = await tool2._execute(toolArgs, childCtx);
6415
+ outcome = { kind: "returned", value: await tool2._execute(toolArgs, childCtx) };
5268
6416
  } catch (err) {
5269
- toolResult2 = { error: err instanceof Error ? err.message : String(err) };
6417
+ outcome = {
6418
+ kind: "thrown",
6419
+ value: { error: err instanceof Error ? err.message : String(err) }
6420
+ };
5270
6421
  }
5271
- if (tool2.hooks?.after && !(toolResult2 && typeof toolResult2 === "object" && "error" in toolResult2)) {
6422
+ if (outcome.kind === "returned" && tool2.hooks?.after && !(suppressErrorShapeInspection ? isErrorShapedSafely(outcome.value) : isErrorShaped(outcome.value))) {
5272
6423
  try {
5273
- toolResult2 = await tool2.hooks.after(toolResult2, this);
6424
+ outcome = {
6425
+ kind: "returned",
6426
+ value: await tool2.hooks.after(outcome.value, this)
6427
+ };
5274
6428
  } catch (err) {
5275
- toolResult2 = {
5276
- error: `After hook error: ${err instanceof Error ? err.message : String(err)}`
6429
+ outcome = {
6430
+ kind: "thrown",
6431
+ value: {
6432
+ error: `After hook error: ${err instanceof Error ? err.message : String(err)}`
6433
+ }
5277
6434
  };
5278
6435
  }
5279
6436
  }
5280
- resultContent2 = tool2.sensitive ? "[REDACTED - sensitive tool output]" : JSON.stringify(toolResult2);
6437
+ if (tool2.sensitive) {
6438
+ resultContent2 = "[REDACTED - sensitive tool output]";
6439
+ } else if (outcome.kind === "thrown" || tool2.toModelOutput === void 0) {
6440
+ resultContent2 = JSON.stringify(outcome.value);
6441
+ }
5281
6442
  } else {
5282
- toolResult2 = void 0;
6443
+ outcome = { kind: "returned", value: void 0 };
5283
6444
  resultContent2 = "Tool execution error";
5284
6445
  }
5285
- return { toolResult: toolResult2, resultContent: resultContent2 };
6446
+ return { outcome, resultContent: resultContent2 };
5286
6447
  };
5287
6448
  const traceName = isMcpTool && this.mcpManager ? this.mcpManager.getQualifiedName(toolName) ?? toolName : toolName;
5288
- const { toolResult, resultContent } = this.spanManager ? await this.spanManager.withSpanAsync(
6449
+ const execution = this.spanManager ? await this.spanManager.withSpanAsync(
5289
6450
  "axl.tool.call",
5290
6451
  {
5291
6452
  "axl.tool.name": traceName,
@@ -5294,22 +6455,27 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
5294
6455
  async (span) => {
5295
6456
  const r = await executeTool();
5296
6457
  span.setAttribute("axl.tool.duration", Date.now() - toolStart);
5297
- const isError = r.toolResult && typeof r.toolResult === "object" && "error" in r.toolResult;
5298
- span.setAttribute("axl.tool.success", !isError);
5299
- if (isError)
5300
- span.setStatus(
5301
- "error",
5302
- r.toolResult.error
5303
- );
6458
+ recordToolSpanResult(span, r.outcome.value, suppressErrorShapeInspection);
5304
6459
  return r;
5305
6460
  }
5306
6461
  ) : await executeTool();
5307
- this.emitEvent({
5308
- type: "tool_call_end",
5309
- agent: agent2._name,
5310
- tool: traceName,
5311
- duration: Date.now() - toolStart,
5312
- data: { args: toolArgs, result: toolResult, callId: toolCall.id }
6462
+ const toolResult = execution.outcome.value;
6463
+ const toolDuration = Date.now() - toolStart;
6464
+ const resultContent = finalizeToolResult({
6465
+ toolName,
6466
+ configuredTool: tool2,
6467
+ outcome: execution.outcome,
6468
+ legacyContent: () => execution.resultContent,
6469
+ emitEnd: () => {
6470
+ this.emitEvent({
6471
+ type: "tool_call_end",
6472
+ agent: agent2._name,
6473
+ tool: traceName,
6474
+ duration: toolDuration,
6475
+ data: { args: toolArgs, result: toolResult, callId: toolCall.id }
6476
+ });
6477
+ },
6478
+ beforeProjection: () => this.currentSignal?.throwIfAborted()
5313
6479
  });
5314
6480
  currentMessages.push({
5315
6481
  role: "tool",
@@ -5533,6 +6699,148 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
5533
6699
  ...providerMetadata ? { providerMetadata } : {}
5534
6700
  });
5535
6701
  }
6702
+ /**
6703
+ * Emit `schema_diagnostic` events for the silent structured-output cliffs
6704
+ * (spec 22, Problems B + E). Called once per ask. Covers:
6705
+ * - the appended **prompt** schema (oversized; dropped refinements; or the
6706
+ * zero-guidance `schemaPrompt:'none'` footgun),
6707
+ * - each user **tool-def** schema (oversized, dropped refinements),
6708
+ * - the **streaming** gate (disabled by a non-object root or by tools),
6709
+ * - **native structured output** the resolved provider can't honor (R10).
6710
+ *
6711
+ * The structured event always fires (persisted + streamed). For the genuinely
6712
+ * surprising, low-frequency cliffs it ALSO fires a one-time deduped
6713
+ * `console.warn` so the median consumer — who never wires up `ctx.events` and
6714
+ * runs with the trace console off — still sees it. `oversized` is event-only
6715
+ * (O3: it's tunable/noisy); the `tools` streaming cause is event-only too
6716
+ * (expected behavior for any tool-using agent, not a surprise).
6717
+ */
6718
+ emitSchemaDiagnostics(agent2, options, toolDefs, ctx) {
6719
+ const silent = this.config.diagnostics?.silent;
6720
+ const threshold = this.config.diagnostics?.schemaOversizedTokens ?? DEFAULT_SCHEMA_OVERSIZED_TOKENS;
6721
+ const schema = options?.schema;
6722
+ if (schema) {
6723
+ if (ctx.mode === "none") {
6724
+ this.emitEvent({
6725
+ type: "schema_diagnostic",
6726
+ agent: agent2._name,
6727
+ data: { kind: "schema_prompt_none_no_guidance" }
6728
+ });
6729
+ warnSchemaDiagnosticOnce(
6730
+ `${agent2._name}\0schema_prompt_none`,
6731
+ `schemaPrompt:'none' is set on agent '${agent2._name}' with an output schema and no custom guidance \u2014 the model receives zero shape hints while the reply is still parsed against the schema, which invites a parse-failure retry loop. Provide a custom { render } or drop 'none'.`,
6732
+ silent
6733
+ );
6734
+ } else {
6735
+ const promptTokens = estimateTokens(ctx.appendedSchemaText ?? "");
6736
+ if (promptTokens > threshold) {
6737
+ this.emitEvent({
6738
+ type: "schema_diagnostic",
6739
+ agent: agent2._name,
6740
+ data: {
6741
+ kind: "prompt_schema_oversized",
6742
+ estimatedTokens: promptTokens,
6743
+ threshold,
6744
+ site: "prompt"
6745
+ }
6746
+ });
6747
+ }
6748
+ if (ctx.mode === "json-schema") {
6749
+ const dropped = detectDroppedRefinements(schema);
6750
+ if (dropped.count > 0) {
6751
+ this.emitEvent({
6752
+ type: "schema_diagnostic",
6753
+ agent: agent2._name,
6754
+ data: {
6755
+ kind: "dropped_refinements",
6756
+ count: dropped.count,
6757
+ paths: dropped.paths,
6758
+ site: "prompt"
6759
+ }
6760
+ });
6761
+ warnSchemaDiagnosticOnce(
6762
+ `${agent2._name}\0dropped_refinements\0prompt\0${dropped.count}\0${dropped.paths.join(",")}`,
6763
+ `${dropped.count} refinement(s) on the output schema for agent '${agent2._name}' (at ${dropped.paths.join(", ")}) are dropped from the model-facing JSON Schema \u2014 the model is never told the rule, then parsing rejects it. Surface it in the prompt (schemaPrompt) or repair with ctx.verify.`,
6764
+ silent
6765
+ );
6766
+ }
6767
+ }
6768
+ }
6769
+ const nativeOut = options?.nativeStructuredOutput ?? agent2._config.nativeStructuredOutput;
6770
+ if (nativeOut && toolDefs.length === 0) {
6771
+ const support = ctx.provider.nativeStructuredOutputSupport?.(ctx.model) ?? "schema";
6772
+ if (support !== "schema") {
6773
+ this.emitEvent({
6774
+ type: "schema_diagnostic",
6775
+ agent: agent2._name,
6776
+ data: {
6777
+ kind: "native_output_unsupported",
6778
+ ...ctx.provider.name ? { provider: ctx.provider.name } : {},
6779
+ support
6780
+ }
6781
+ });
6782
+ warnSchemaDiagnosticOnce(
6783
+ `${agent2._name}\0native_output_unsupported\0${ctx.provider.name ?? ctx.model}`,
6784
+ `nativeStructuredOutput is set on agent '${agent2._name}' but provider '${ctx.provider.name ?? ctx.model}' ${support === "downgraded" ? "downgrades it to plain JSON mode" : support === "lossy" ? "sanitizes the schema lossily" : "ignores it structurally"} \u2014 the schema shape is not natively enforced. The prompt text remains the parse contract; the call proceeds.`,
6785
+ silent
6786
+ );
6787
+ }
6788
+ }
6789
+ const rootIsObject = schema instanceof import_zod.z.ZodObject;
6790
+ const hasTools = toolDefs.length > 0;
6791
+ if (this._streamingEnabled && (!rootIsObject || hasTools)) {
6792
+ const cause = !rootIsObject ? "non-object" : "tools";
6793
+ const rootType = schema.constructor?.name ?? "unknown";
6794
+ this.emitEvent({
6795
+ type: "schema_diagnostic",
6796
+ agent: agent2._name,
6797
+ data: { kind: "streaming_disabled", rootType, cause }
6798
+ });
6799
+ if (cause === "non-object") {
6800
+ warnSchemaDiagnosticOnce(
6801
+ `${agent2._name}\0streaming_disabled\0${rootType}`,
6802
+ `Progressive object streaming is disabled for agent '${agent2._name}' because the output schema root is ${rootType}, not a ZodObject. Wrap it \u2014 z.object({ result: <yourSchema> }) \u2014 to re-enable partial_object streaming.`,
6803
+ silent
6804
+ );
6805
+ }
6806
+ }
6807
+ }
6808
+ for (const tool2 of agent2._config.tools ?? []) {
6809
+ const toolTokens = estimateInlineSchemaTokens(tool2.inputSchema);
6810
+ if (toolTokens > threshold) {
6811
+ this.emitEvent({
6812
+ type: "schema_diagnostic",
6813
+ agent: agent2._name,
6814
+ data: {
6815
+ kind: "prompt_schema_oversized",
6816
+ estimatedTokens: toolTokens,
6817
+ threshold,
6818
+ site: "tool",
6819
+ tool: tool2.name
6820
+ }
6821
+ });
6822
+ }
6823
+ const droppedTool = detectDroppedRefinements(tool2.inputSchema);
6824
+ if (droppedTool.count > 0) {
6825
+ this.emitEvent({
6826
+ type: "schema_diagnostic",
6827
+ agent: agent2._name,
6828
+ data: {
6829
+ kind: "dropped_refinements",
6830
+ count: droppedTool.count,
6831
+ paths: droppedTool.paths,
6832
+ site: "tool",
6833
+ tool: tool2.name
6834
+ }
6835
+ });
6836
+ warnSchemaDiagnosticOnce(
6837
+ `${agent2._name}\0dropped_refinements\0tool\0${tool2.name}\0${droppedTool.count}`,
6838
+ `${droppedTool.count} refinement(s) on tool '${tool2.name}' input schema (at ${droppedTool.paths.join(", ")}) are dropped from the tool definition \u2014 the model is never told the constraint. Validate inside the tool handler instead.`,
6839
+ silent
6840
+ );
6841
+ }
6842
+ }
6843
+ }
5536
6844
  buildToolDefs(agent2, resolvedHandoffs) {
5537
6845
  const defs = [];
5538
6846
  if (agent2._config.tools) {
@@ -6025,24 +7333,44 @@ ${summaryResponse.content}`
6025
7333
  limit,
6026
7334
  exceeded: false,
6027
7335
  policy,
6028
- abortController: controller
7336
+ abortController: controller,
7337
+ unpriced: false,
7338
+ unpricedCount: 0,
7339
+ unpricedWarned: false
6029
7340
  };
6030
7341
  const executeBudget = async () => {
6031
7342
  try {
6032
7343
  const value = budgetSignal ? await signalStorage.run(budgetSignal, fn) : await fn();
6033
7344
  const totalCost = this.budgetContext.totalCost;
6034
7345
  const exceeded = this.budgetContext.exceeded;
6035
- return { value, budgetExceeded: exceeded, totalCost };
7346
+ const unpriced = this.budgetContext.unpriced;
7347
+ return { value, budgetExceeded: exceeded, totalCost, unpriced };
6036
7348
  } catch (err) {
6037
7349
  if (this.budgetContext.exceeded) {
6038
- return { value: null, budgetExceeded: true, totalCost: this.budgetContext.totalCost };
7350
+ return {
7351
+ value: null,
7352
+ budgetExceeded: true,
7353
+ totalCost: this.budgetContext.totalCost,
7354
+ unpriced: this.budgetContext.unpriced
7355
+ };
6039
7356
  }
6040
7357
  if (err instanceof DOMException && err.name === "AbortError" && controller) {
6041
- return { value: null, budgetExceeded: true, totalCost: this.budgetContext.totalCost };
7358
+ return {
7359
+ value: null,
7360
+ budgetExceeded: true,
7361
+ totalCost: this.budgetContext.totalCost,
7362
+ unpriced: this.budgetContext.unpriced
7363
+ };
6042
7364
  }
6043
7365
  throw err;
6044
7366
  } finally {
6045
- if (parentBudget) parentBudget.totalCost += this.budgetContext.totalCost;
7367
+ if (parentBudget) {
7368
+ parentBudget.totalCost += this.budgetContext.totalCost;
7369
+ if (this.budgetContext.unpriced) {
7370
+ parentBudget.unpriced = true;
7371
+ parentBudget.unpricedCount += this.budgetContext.unpricedCount;
7372
+ }
7373
+ }
6046
7374
  this.budgetContext = parentBudget;
6047
7375
  }
6048
7376
  };
@@ -6063,13 +7391,22 @@ ${summaryResponse.content}`
6063
7391
  }
6064
7392
  return executeBudget();
6065
7393
  }
6066
- /** Get the current budget status, or null if not inside a budget block. */
7394
+ /**
7395
+ * Get the current budget status, or null if not inside a budget block. Inside
7396
+ * nested budgets this reflects the **innermost active** block (the nested block's
7397
+ * `finally` restores the parent before control returns to it).
7398
+ *
7399
+ * `unpriced` is true when this block ran a model with no usable cost (unpriced /
7400
+ * pricing-table miss): `spent` is then a LOWER BOUND and `remaining` an upper bound —
7401
+ * the limit cannot be enforced on the unmeasured spend. See {@link BudgetResult.unpriced}.
7402
+ */
6067
7403
  getBudgetStatus() {
6068
7404
  if (!this.budgetContext) return null;
6069
7405
  return {
6070
7406
  spent: this.budgetContext.totalCost,
6071
7407
  limit: this.budgetContext.limit,
6072
- remaining: Math.max(0, this.budgetContext.limit - this.budgetContext.totalCost)
7408
+ remaining: Math.max(0, this.budgetContext.limit - this.budgetContext.totalCost),
7409
+ unpriced: this.budgetContext.unpriced
6073
7410
  };
6074
7411
  }
6075
7412
  // ── ctx.race() ────────────────────────────────────────────────────────
@@ -6651,6 +7988,8 @@ ${summaryResponse.content}`
6651
7988
  });
6652
7989
  return this.ask(agents[0], prompt, {
6653
7990
  schema: options?.schema,
7991
+ schemaPrompt: options?.schemaPrompt,
7992
+ nativeStructuredOutput: options?.nativeStructuredOutput,
6654
7993
  retries: options?.retries,
6655
7994
  metadata: options?.metadata,
6656
7995
  validate: options?.validate,
@@ -6695,6 +8034,8 @@ ${summaryResponse.content}`
6695
8034
  });
6696
8035
  return this.ask(routerAgent, prompt, {
6697
8036
  schema: options?.schema,
8037
+ schemaPrompt: options?.schemaPrompt,
8038
+ nativeStructuredOutput: options?.nativeStructuredOutput,
6698
8039
  retries: options?.retries,
6699
8040
  metadata: options?.metadata,
6700
8041
  validate: options?.validate,
@@ -6713,8 +8054,25 @@ ${summaryResponse.content}`
6713
8054
  const frame = askStorage.getStore();
6714
8055
  const step = (frame?.stepRef ?? this.stepRefRoot).value++;
6715
8056
  const cost = partial.cost;
6716
- if (frame && typeof cost === "number" && Number.isFinite(cost) && cost >= 0 && COST_BEARING_LEAF_TYPES.includes(partial.type)) {
6717
- frame.askCost.value += cost;
8057
+ if (COST_BEARING_LEAF_TYPES.includes(partial.type)) {
8058
+ const usable = isUsableCost(cost);
8059
+ const unpriced = !usable && hasPositiveTokens(
8060
+ partial
8061
+ );
8062
+ if (frame) {
8063
+ if (usable) frame.askCost.value += cost;
8064
+ else if (unpriced) frame.askUnpriced = true;
8065
+ }
8066
+ if (unpriced && this.budgetContext) {
8067
+ this.budgetContext.unpriced = true;
8068
+ this.budgetContext.unpricedCount += 1;
8069
+ if (!this.budgetContext.unpricedWarned && Number.isFinite(this.budgetContext.limit)) {
8070
+ this.budgetContext.unpricedWarned = true;
8071
+ console.warn(
8072
+ "Budget honesty: unpriced work detected \u2014 this budget's cost limit is a lower bound and is NOT enforced on unpriced models. See docs/observability.md#budget-honesty."
8073
+ );
8074
+ }
8075
+ }
6718
8076
  }
6719
8077
  const event = {
6720
8078
  executionId: this.executionId,
@@ -8541,7 +9899,7 @@ var StreamingFlusher = class {
8541
9899
  await Promise.allSettled(pendings);
8542
9900
  }
8543
9901
  };
8544
- function isAbortError(err) {
9902
+ function isAbortError2(err) {
8545
9903
  return typeof err === "object" && err !== null && err.name === "AbortError";
8546
9904
  }
8547
9905
  function pushEventBounded(execInfo, event, cap) {
@@ -8804,7 +10162,7 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
8804
10162
  this.persistExecution(execInfo);
8805
10163
  return result;
8806
10164
  } catch (err) {
8807
- const aborted = isAbortError(err);
10165
+ const aborted = isAbortError2(err);
8808
10166
  execInfo.status = "failed";
8809
10167
  execInfo.completedAt = Date.now();
8810
10168
  execInfo.duration = execInfo.completedAt - execInfo.startedAt;
@@ -9078,7 +10436,10 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
9078
10436
  totalCost: 0,
9079
10437
  limit: budgetLimit,
9080
10438
  exceeded: false,
9081
- policy: "finish_and_stop"
10439
+ policy: "finish_and_stop",
10440
+ unpriced: false,
10441
+ unpricedCount: 0,
10442
+ unpricedWarned: false
9082
10443
  }
9083
10444
  });
9084
10445
  }
@@ -9113,6 +10474,7 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
9113
10474
  status: "running",
9114
10475
  events: [],
9115
10476
  totalCost: 0,
10477
+ unpriced: false,
9116
10478
  startedAt: Date.now(),
9117
10479
  duration: 0,
9118
10480
  metadata: liftPersistedMetadata(options?.metadata)
@@ -9131,6 +10493,7 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
9131
10493
  onTrace: (event) => {
9132
10494
  pushEventBounded(execInfo, event, this.maxEventsPerExecution);
9133
10495
  execInfo.totalCost += eventCostContribution(event);
10496
+ if (isUnpricedLeaf(event)) execInfo.unpriced = true;
9134
10497
  if (this.streamableExecutionIds.has(executionId)) {
9135
10498
  this.streamingFlusher?.append(executionId, event);
9136
10499
  }
@@ -9177,7 +10540,10 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
9177
10540
  totalCost: 0,
9178
10541
  limit: Infinity,
9179
10542
  exceeded: false,
9180
- policy: "finish_and_stop"
10543
+ policy: "finish_and_stop",
10544
+ unpriced: false,
10545
+ unpricedCount: 0,
10546
+ unpricedWarned: false
9181
10547
  }
9182
10548
  });
9183
10549
  return this.spanManager.withSpanAsync(
@@ -9221,6 +10587,7 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
9221
10587
  status: "running",
9222
10588
  events: [],
9223
10589
  totalCost: 0,
10590
+ unpriced: false,
9224
10591
  startedAt: Date.now(),
9225
10592
  duration: 0,
9226
10593
  metadata: liftPersistedMetadata(options?.metadata)
@@ -9244,6 +10611,7 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
9244
10611
  onTrace: (event) => {
9245
10612
  pushEventBounded(execInfo, event, this.maxEventsPerExecution);
9246
10613
  execInfo.totalCost += eventCostContribution(event);
10614
+ if (isUnpricedLeaf(event)) execInfo.unpriced = true;
9247
10615
  if (this.streamableExecutionIds.has(executionId)) {
9248
10616
  this.streamingFlusher?.append(executionId, event);
9249
10617
  }
@@ -9291,7 +10659,10 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
9291
10659
  totalCost: 0,
9292
10660
  limit: Infinity,
9293
10661
  exceeded: false,
9294
- policy: "finish_and_stop"
10662
+ policy: "finish_and_stop",
10663
+ unpriced: false,
10664
+ unpricedCount: 0,
10665
+ unpricedWarned: false
9295
10666
  }
9296
10667
  });
9297
10668
  return this.spanManager.withSpanAsync(
@@ -9453,12 +10824,14 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
9453
10824
  const completedAt = lastEvent.timestamp ?? startedAt;
9454
10825
  const workflowName = workflowStartEvent?.workflow ?? RECOVERED_UNKNOWN_WORKFLOW;
9455
10826
  const totalCost = boundedEvents.reduce((sum, e) => sum + eventCostContribution(e), 0);
10827
+ const unpriced = boundedEvents.some(isUnpricedLeaf);
9456
10828
  const synthesized = {
9457
10829
  executionId: id,
9458
10830
  workflow: workflowName,
9459
10831
  status: "failed",
9460
10832
  events: boundedEvents,
9461
10833
  totalCost,
10834
+ unpriced,
9462
10835
  startedAt,
9463
10836
  completedAt,
9464
10837
  duration: completedAt - startedAt,
@@ -9760,8 +11133,8 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
9760
11133
  * Works with both `createContext()` and `execute()` calls inside `fn`.
9761
11134
  */
9762
11135
  async trackCost(fn) {
9763
- const { result, cost } = await this.trackExecution(fn);
9764
- return { result, cost };
11136
+ const { result, cost, unpriced } = await this.trackExecution(fn);
11137
+ return { result, cost, unpriced };
9765
11138
  }
9766
11139
  /**
9767
11140
  * Track cost and execution metadata across any runtime operations within the given function.
@@ -9806,10 +11179,12 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
9806
11179
  const workflowCalls = /* @__PURE__ */ new Map();
9807
11180
  const tokens = { input: 0, output: 0, reasoning: 0 };
9808
11181
  let agentCalls = 0;
11182
+ let unpriced = false;
9809
11183
  const capturedTraces = options?.captureTraces ? [] : void 0;
9810
11184
  const listener = (event) => {
9811
11185
  if (!scope.trackedIds.has(event.executionId)) return;
9812
11186
  scope.totalCost += eventCostContribution(event);
11187
+ if (isUnpricedLeaf(event)) unpriced = true;
9813
11188
  if (event.type === "agent_call_end") {
9814
11189
  if (event.model) modelCalls.set(event.model, (modelCalls.get(event.model) ?? 0) + 1);
9815
11190
  agentCalls++;
@@ -9847,6 +11222,7 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
9847
11222
  return {
9848
11223
  result,
9849
11224
  cost: scope.totalCost,
11225
+ unpriced,
9850
11226
  ...capturedTraces ? { traces: capturedTraces } : {},
9851
11227
  metadata: {
9852
11228
  models: [...modelCalls.keys()],
@@ -10741,22 +12117,40 @@ var OpenAIEmbedder = class {
10741
12117
  this.dimensions = options.dimensions ?? 1536;
10742
12118
  }
10743
12119
  async embed(texts, signal) {
10744
- const response = await fetchWithRetry(`${this.baseUrl}/v1/embeddings`, {
10745
- method: "POST",
10746
- headers: {
10747
- "Content-Type": "application/json",
10748
- Authorization: `Bearer ${this.apiKey}`
12120
+ const response = await fetchWithRetry(
12121
+ `${this.baseUrl}/v1/embeddings`,
12122
+ {
12123
+ method: "POST",
12124
+ headers: {
12125
+ "Content-Type": "application/json",
12126
+ Authorization: `Bearer ${this.apiKey}`
12127
+ },
12128
+ body: JSON.stringify({
12129
+ model: this.model,
12130
+ input: texts,
12131
+ dimensions: this.dimensions
12132
+ }),
12133
+ signal
10749
12134
  },
10750
- body: JSON.stringify({
10751
- model: this.model,
10752
- input: texts,
10753
- dimensions: this.dimensions
10754
- }),
10755
- signal
10756
- });
12135
+ // Participate in the same typed-error contract as the provider adapters: a
12136
+ // thrown network failure normalizes to ProviderError{status:0,provider:'openai'}.
12137
+ { provider: "openai" }
12138
+ );
10757
12139
  if (!response.ok) {
10758
12140
  const body = await response.text();
10759
- throw new Error(`OpenAI embeddings API error (${response.status}): ${body}`);
12141
+ let detail = body;
12142
+ try {
12143
+ const json2 = JSON.parse(body);
12144
+ detail = json2.error?.message ?? json2.message ?? body;
12145
+ } catch {
12146
+ }
12147
+ throw buildProviderError({
12148
+ provider: "openai",
12149
+ status: response.status,
12150
+ headers: response.headers,
12151
+ message: `OpenAI embeddings API error (${response.status}): ${detail}`,
12152
+ body
12153
+ });
10760
12154
  }
10761
12155
  const json = await response.json();
10762
12156
  const vectors = json.data.sort((a, b) => a.index - b.index).map((d) => d.embedding);
@@ -10898,57 +12292,88 @@ function cosineSimilarity2(a, b) {
10898
12292
  // Annotate the CommonJS export names for ESM import in node:
10899
12293
  0 && (module.exports = {
10900
12294
  AXL_EVENT_TYPES,
12295
+ AZURE_PROFILE,
10901
12296
  AnthropicProvider,
10902
12297
  AxlError,
10903
12298
  AxlEventBus,
10904
12299
  AxlRuntime,
10905
12300
  AxlStream,
12301
+ BEDROCK_PROFILE,
12302
+ BUILTIN_PROFILES,
10906
12303
  BudgetExceededError,
10907
12304
  CONFIG_CANDIDATES,
10908
12305
  COST_BEARING_LEAF_TYPES,
12306
+ DEEPSEEK_PROFILE,
10909
12307
  EventStreamOverflowError,
12308
+ GROQ_PROFILE,
10910
12309
  GeminiProvider,
10911
12310
  GuardrailError,
10912
12311
  InMemoryVectorStore,
12312
+ LLAMACPP_PROFILE,
12313
+ LMSTUDIO_PROFILE,
12314
+ MISTRAL_PROFILE,
10913
12315
  MaxTurnsError,
10914
12316
  McpManager,
10915
12317
  MemoryManager,
10916
12318
  MemoryStore,
10917
12319
  NoConsensus,
10918
12320
  NoopSpanManager,
12321
+ OLLAMA_PROFILE,
12322
+ OPENAI_PRICING,
12323
+ OPENAI_PROFILE,
12324
+ OPENROUTER_PROFILE,
12325
+ OpenAICompatibleProvider,
10919
12326
  OpenAIEmbedder,
10920
12327
  OpenAIProvider,
10921
12328
  OpenAIResponsesProvider,
12329
+ ProviderError,
10922
12330
  ProviderRegistry,
10923
12331
  QuorumNotMet,
10924
12332
  REDACTED,
10925
12333
  REDACTION_RULES,
10926
12334
  RateLimiter,
10927
12335
  RedisStore,
12336
+ SGLANG_PROFILE,
10928
12337
  SQLiteStore,
10929
12338
  Session,
10930
12339
  SqliteVectorStore,
12340
+ ThinkTagScanner,
10931
12341
  TimeoutError,
10932
12342
  ToolDenied,
12343
+ ToolModelOutputError,
12344
+ VLLM_PROFILE,
10933
12345
  ValidationError,
10934
12346
  VerifyError,
10935
12347
  WorkflowContext,
12348
+ XAI_PROFILE,
10936
12349
  agent,
10937
12350
  createSpanManager,
10938
12351
  defineConfig,
12352
+ estimateOpenAICost,
10939
12353
  eventCostContribution,
10940
12354
  expandGlob,
10941
12355
  extractJson,
12356
+ extractThinkTags,
10942
12357
  findConfig,
12358
+ hasPositiveTokens,
10943
12359
  importModule,
10944
12360
  isCostBearingLeaf,
12361
+ isRetryableStatus,
10945
12362
  isRootLevel,
12363
+ isUnpricedLeaf,
12364
+ isUsableCost,
10946
12365
  needsTsxLoader,
12366
+ openaiReasoningEmit,
10947
12367
  parsePartialJson,
10948
12368
  pickDefault,
10949
12369
  pickExport,
12370
+ priceFromTable,
12371
+ reasoningEffortEmit,
12372
+ reasoningObjectEmit,
10950
12373
  redactEvent,
10951
12374
  registerConditions,
12375
+ resolveApiKey,
12376
+ resolvePerModel,
10952
12377
  resolveRuntime,
10953
12378
  resolveThinkingOptions,
10954
12379
  stringStreamFromEvents,