@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.js CHANGED
@@ -73,6 +73,7 @@ function tool(config) {
73
73
  retry: retryPolicy,
74
74
  requireApproval: config.requireApproval ?? false,
75
75
  hooks: config.hooks,
76
+ ...config.toModelOutput !== void 0 ? { toModelOutput: config.toModelOutput } : {},
76
77
  async run(ctx, input) {
77
78
  const startTime = Date.now();
78
79
  try {
@@ -120,11 +121,229 @@ function resolveThinkingOptions(options) {
120
121
  hasBudgetOverride
121
122
  };
122
123
  }
124
+ async function resolveApiKey(source) {
125
+ if (typeof source === "function") return await source() ?? "";
126
+ return source ?? "";
127
+ }
128
+
129
+ // src/errors.ts
130
+ var AxlError = class extends Error {
131
+ code;
132
+ constructor(code, message) {
133
+ super(message);
134
+ this.name = "AxlError";
135
+ this.code = code;
136
+ }
137
+ };
138
+ var ToolModelOutputError = class extends AxlError {
139
+ toolName;
140
+ cause;
141
+ constructor(toolName, cause) {
142
+ super("TOOL_MODEL_OUTPUT_ERROR", `Failed to prepare model output for tool "${toolName}"`);
143
+ this.name = "ToolModelOutputError";
144
+ this.toolName = toolName;
145
+ this.cause = cause;
146
+ Object.defineProperty(this, "cause", { enumerable: false });
147
+ }
148
+ };
149
+ var VerifyError = class extends AxlError {
150
+ lastOutput;
151
+ zodError;
152
+ retries;
153
+ constructor(lastOutput, zodError, retries) {
154
+ super("VERIFY_ERROR", `Schema validation failed after ${retries} retries: ${zodError.message}`);
155
+ this.name = "VerifyError";
156
+ this.lastOutput = lastOutput;
157
+ this.zodError = zodError;
158
+ this.retries = retries;
159
+ }
160
+ };
161
+ var QuorumNotMet = class extends AxlError {
162
+ results;
163
+ constructor(required, actual, results) {
164
+ super("QUORUM_NOT_MET", `Quorum not met: needed ${required} successes, got ${actual}`);
165
+ this.name = "QuorumNotMet";
166
+ this.results = results;
167
+ }
168
+ };
169
+ var NoConsensus = class extends AxlError {
170
+ constructor(reason) {
171
+ super("NO_CONSENSUS", `No consensus: ${reason}`);
172
+ this.name = "NoConsensus";
173
+ }
174
+ };
175
+ var TimeoutError = class extends AxlError {
176
+ constructor(operation, timeoutMs) {
177
+ super("TIMEOUT", `${operation} exceeded timeout of ${timeoutMs}ms`);
178
+ this.name = "TimeoutError";
179
+ }
180
+ };
181
+ function formatBudgetCost(cost) {
182
+ if (Number.isNaN(cost)) return "$NaN";
183
+ if (cost === Infinity) return "$Infinity";
184
+ if (cost === -Infinity) return "-$Infinity";
185
+ if (cost === 0) return "$0.00";
186
+ const sign = cost < 0 ? "-" : "";
187
+ const abs = Math.abs(cost);
188
+ if (abs < 1e-6) return `${sign}< $0.000001`;
189
+ if (abs < 1e-4) {
190
+ const [mantissa, exponent] = abs.toExponential(2).split("e");
191
+ return `${sign}$${mantissa}e${parseInt(exponent, 10)}`;
192
+ }
193
+ if (abs < 0.01) return `${sign}$${abs.toFixed(6)}`;
194
+ return `${sign}$${abs.toFixed(2)}`;
195
+ }
196
+ var BudgetExceededError = class extends AxlError {
197
+ limit;
198
+ spent;
199
+ policy;
200
+ constructor(limit, spent, policy) {
201
+ super(
202
+ "BUDGET_EXCEEDED",
203
+ `Budget exceeded: spent ${formatBudgetCost(spent)} of ${formatBudgetCost(
204
+ limit
205
+ )} limit (policy: ${policy})`
206
+ );
207
+ this.name = "BudgetExceededError";
208
+ this.limit = limit;
209
+ this.spent = spent;
210
+ this.policy = policy;
211
+ }
212
+ };
213
+ var MaxTurnsError = class extends AxlError {
214
+ maxTurns;
215
+ constructor(operation, maxTurns) {
216
+ super("MAX_TURNS", `${operation} exceeded maximum of ${maxTurns} turns`);
217
+ this.name = "MaxTurnsError";
218
+ this.maxTurns = maxTurns;
219
+ }
220
+ };
221
+ var GuardrailError = class extends AxlError {
222
+ guardrailType;
223
+ reason;
224
+ constructor(guardrailType, reason) {
225
+ super("GUARDRAIL_BLOCKED", `${guardrailType} guardrail blocked: ${reason}`);
226
+ this.name = "GuardrailError";
227
+ this.guardrailType = guardrailType;
228
+ this.reason = reason;
229
+ }
230
+ };
231
+ var ValidationError = class extends AxlError {
232
+ lastOutput;
233
+ reason;
234
+ retries;
235
+ constructor(lastOutput, reason, retries) {
236
+ super("VALIDATION_ERROR", `Validation failed after ${retries} retries: ${reason}`);
237
+ this.name = "ValidationError";
238
+ this.lastOutput = lastOutput;
239
+ this.reason = reason;
240
+ this.retries = retries;
241
+ }
242
+ };
243
+ var ToolDenied = class extends AxlError {
244
+ toolName;
245
+ agentName;
246
+ constructor(toolName, agentName) {
247
+ super(
248
+ "TOOL_DENIED",
249
+ `Agent "${agentName}" attempted to call tool "${toolName}" which is not in its ACL`
250
+ );
251
+ this.name = "ToolDenied";
252
+ this.toolName = toolName;
253
+ this.agentName = agentName;
254
+ }
255
+ };
256
+
257
+ // src/providers/errors.ts
258
+ var ProviderError = class extends AxlError {
259
+ /** Adapter/profile name, e.g. `'openai'`, `'anthropic'`, `'google'`. */
260
+ provider;
261
+ /** HTTP status code; `0` for non-HTTP (network) failures. */
262
+ status;
263
+ /** Semantic failover hint (see module doc) — NOT the transport-retry set. */
264
+ retryable;
265
+ /** Parsed `Retry-After` delay in ms (RAW/unclamped), when the header was present. */
266
+ retryAfterMs;
267
+ /** Provider request id from response headers, when present. */
268
+ requestId;
269
+ /**
270
+ * Raw provider error body. Lives ONLY on the error object — it is
271
+ * intentionally never placed on the event stream (it can echo prompt text
272
+ * and is redaction-eligible). See `docs/security.md`.
273
+ */
274
+ body;
275
+ constructor(args) {
276
+ super("PROVIDER_ERROR", args.message);
277
+ this.name = "ProviderError";
278
+ this.provider = args.provider;
279
+ this.status = args.status;
280
+ this.retryable = args.retryable;
281
+ this.retryAfterMs = args.retryAfterMs;
282
+ this.requestId = args.requestId;
283
+ this.body = args.body;
284
+ }
285
+ };
286
+ function isRetryableStatus(status) {
287
+ switch (status) {
288
+ case 0:
289
+ return true;
290
+ case 408:
291
+ // request timeout
292
+ case 429:
293
+ // rate limit
294
+ case 500:
295
+ case 502:
296
+ case 503:
297
+ case 504:
298
+ case 529:
299
+ return true;
300
+ // Explicitly non-retryable (bad request / auth / not found / payload):
301
+ // 400, 401, 403, 404, 413, 422 fall through to the default.
302
+ default:
303
+ return false;
304
+ }
305
+ }
306
+ function parseRetryAfter(headers) {
307
+ const raw = headers.get("retry-after");
308
+ if (!raw) return void 0;
309
+ const trimmed = raw.trim();
310
+ if (/^\d+(\.\d+)?$/.test(trimmed)) {
311
+ const ms = Number(trimmed) * 1e3;
312
+ return ms > 0 ? ms : void 0;
313
+ }
314
+ const dateMs = Date.parse(trimmed);
315
+ if (Number.isNaN(dateMs)) return void 0;
316
+ const delta = dateMs - Date.now();
317
+ return delta > 0 ? delta : void 0;
318
+ }
319
+ var REQUEST_ID_HEADERS = ["x-request-id", "request-id"];
320
+ function extractRequestId(headers) {
321
+ for (const name of REQUEST_ID_HEADERS) {
322
+ const value = headers.get(name);
323
+ if (value) return value;
324
+ }
325
+ return void 0;
326
+ }
327
+ function buildProviderError(args) {
328
+ return new ProviderError({
329
+ provider: args.provider,
330
+ status: args.status,
331
+ retryable: isRetryableStatus(args.status),
332
+ message: args.message,
333
+ retryAfterMs: args.headers ? parseRetryAfter(args.headers) : void 0,
334
+ requestId: args.headers ? extractRequestId(args.headers) : void 0,
335
+ body: args.body
336
+ });
337
+ }
123
338
 
124
339
  // src/providers/retry.ts
125
340
  var RETRYABLE_STATUS_CODES = /* @__PURE__ */ new Set([429, 503, 529]);
126
341
  var MAX_RETRIES = 2;
127
342
  var BASE_DELAY_MS = 1e3;
343
+ var MAX_BACKOFF_MS = 6e4;
344
+ function jitter(ms) {
345
+ return ms * (0.75 + Math.random() * 0.5);
346
+ }
128
347
  function sleep2(ms, signal) {
129
348
  return new Promise((resolve2) => {
130
349
  if (signal?.aborted) {
@@ -142,9 +361,14 @@ function sleep2(ms, signal) {
142
361
  );
143
362
  });
144
363
  }
364
+ function isAbortError(err, signal) {
365
+ if (signal?.aborted) return true;
366
+ return typeof err === "object" && err !== null && "name" in err && err.name === "AbortError";
367
+ }
145
368
  async function fetchWithRetry(input, init, opts) {
146
369
  const maxRetries = opts?.maxRetries ?? MAX_RETRIES;
147
370
  const governor = opts?.governor;
371
+ const provider = opts?.provider ?? "unknown";
148
372
  let acquired = false;
149
373
  if (governor) {
150
374
  await governor.acquire(init?.signal ?? void 0);
@@ -152,7 +376,21 @@ async function fetchWithRetry(input, init, opts) {
152
376
  }
153
377
  try {
154
378
  for (let attempt = 0; ; attempt++) {
155
- const res = await fetch(input, init);
379
+ let res;
380
+ try {
381
+ res = await fetch(input, init);
382
+ } catch (err) {
383
+ if (isAbortError(err, init?.signal ?? void 0)) throw err;
384
+ if (attempt >= maxRetries) {
385
+ throw buildProviderError({
386
+ provider,
387
+ status: 0,
388
+ message: err instanceof Error ? err.message : String(err)
389
+ });
390
+ }
391
+ await sleep2(jitter(BASE_DELAY_MS * 2 ** attempt), init?.signal ?? void 0);
392
+ continue;
393
+ }
156
394
  governor?.observe(res);
157
395
  if (res.ok || !RETRYABLE_STATUS_CODES.has(res.status) || attempt >= maxRetries) {
158
396
  return res;
@@ -160,15 +398,9 @@ async function fetchWithRetry(input, init, opts) {
160
398
  if (init?.signal?.aborted) {
161
399
  return res;
162
400
  }
163
- const retryAfter = res.headers.get("retry-after");
164
- let delay;
165
- if (retryAfter && !isNaN(Number(retryAfter))) {
166
- delay = Number(retryAfter) * 1e3;
167
- } else {
168
- delay = BASE_DELAY_MS * 2 ** attempt;
169
- }
170
- delay *= 0.75 + Math.random() * 0.5;
171
- await sleep2(delay, init?.signal ?? void 0);
401
+ const retryAfterMs = parseRetryAfter(res.headers);
402
+ const baseDelay = retryAfterMs !== void 0 ? Math.min(retryAfterMs, MAX_BACKOFF_MS) : BASE_DELAY_MS * 2 ** attempt;
403
+ await sleep2(jitter(baseDelay), init?.signal ?? void 0);
172
404
  }
173
405
  } finally {
174
406
  if (acquired) governor.release();
@@ -316,254 +548,467 @@ var RateLimiter = class {
316
548
  }
317
549
  };
318
550
 
319
- // src/providers/openai.ts
320
- var OPENAI_PRICING = {
321
- // gpt-4o era — cache reads at 50% of input rate
322
- "gpt-4o": [25e-7, 1e-5, 0.5],
323
- "gpt-4o-mini": [15e-8, 6e-7, 0.5],
324
- "gpt-4-turbo": [1e-5, 3e-5, 0.5],
325
- "gpt-4": [3e-5, 6e-5, 0.5],
326
- "gpt-3.5-turbo": [5e-7, 15e-7, 0.5],
327
- o1: [15e-6, 6e-5, 0.5],
328
- "o1-mini": [3e-6, 12e-6, 0.5],
329
- "o1-pro": [15e-5, 6e-4, 0.5],
330
- // gpt-4.1 / o3 / o4 era — cache reads at 25% of input rate
331
- "gpt-4.1": [2e-6, 8e-6, 0.25],
332
- "gpt-4.1-mini": [4e-7, 16e-7, 0.25],
333
- "gpt-4.1-nano": [1e-7, 4e-7, 0.25],
334
- o3: [1e-5, 4e-5, 0.25],
335
- "o3-mini": [11e-7, 44e-7, 0.25],
336
- "o3-pro": [2e-5, 8e-5, 0.25],
337
- "o4-mini": [11e-7, 44e-7, 0.25],
338
- // gpt-5 era — cache reads at 10% of input rate
339
- "gpt-5": [125e-8, 1e-5, 0.1],
340
- "gpt-5-mini": [25e-8, 2e-6, 0.1],
341
- "gpt-5-nano": [5e-8, 4e-7, 0.1],
342
- "gpt-5.1": [125e-8, 1e-5, 0.1],
343
- "gpt-5.2": [175e-8, 14e-6, 0.1],
344
- "gpt-5.3": [175e-8, 14e-6, 0.1],
345
- "gpt-5.4": [25e-7, 15e-6, 0.1],
346
- "gpt-5.4-pro": [3e-5, 18e-5, 0.1],
347
- // gpt-5.5 era (flagship released 2026-04) — cache reads at 10% of input rate
348
- "gpt-5.5": [5e-6, 3e-5, 0.1],
349
- "gpt-5.5-pro": [3e-5, 18e-5, 0.1]
350
- };
351
- var PRICING_KEYS_BY_LENGTH = Object.keys(OPENAI_PRICING).sort((a, b) => b.length - a.length);
352
- function estimateOpenAICost(model, promptTokens, completionTokens, cachedTokens) {
353
- let pricing = OPENAI_PRICING[model];
551
+ // src/providers/openai-compatible.ts
552
+ function resolvePerModel(value, model, fallback) {
553
+ if (value === void 0) return fallback;
554
+ return typeof value === "function" ? value(model) : value;
555
+ }
556
+ var SORTED_KEYS_CACHE = /* @__PURE__ */ new WeakMap();
557
+ function sortedKeys(table) {
558
+ let keys = SORTED_KEYS_CACHE.get(table);
559
+ if (!keys) {
560
+ keys = Object.keys(table).sort((a, b) => b.length - a.length);
561
+ SORTED_KEYS_CACHE.set(table, keys);
562
+ }
563
+ return keys;
564
+ }
565
+ function priceFromTable(table, model, promptTokens, completionTokens, cachedTokens) {
566
+ let pricing = table[model];
354
567
  if (!pricing) {
355
- for (const key of PRICING_KEYS_BY_LENGTH) {
568
+ for (const key of sortedKeys(table)) {
356
569
  if (model.startsWith(key)) {
357
- pricing = OPENAI_PRICING[key];
570
+ pricing = table[key];
358
571
  break;
359
572
  }
360
573
  }
361
574
  }
362
- if (!pricing) return 0;
575
+ if (!pricing) return void 0;
363
576
  const [inputRate, outputRate, cacheMultiplier] = pricing;
364
577
  const cached = cachedTokens ?? 0;
365
578
  const inputCost = (promptTokens - cached) * inputRate + cached * inputRate * cacheMultiplier;
366
579
  return inputCost + completionTokens * outputRate;
367
580
  }
368
- function isOSeriesModel(model) {
369
- return /^(o1|o3|o4-mini)/.test(model);
370
- }
371
- function supportsReasoningEffort(model) {
372
- return isOSeriesModel(model) || /^gpt-5/.test(model);
581
+ var THINK_OPEN = "<think>";
582
+ var THINK_CLOSE = "</think>";
583
+ function partialTagSuffixLen(s, tag) {
584
+ const max = Math.min(s.length, tag.length - 1);
585
+ for (let n = max; n > 0; n--) {
586
+ if (tag.startsWith(s.slice(s.length - n))) return n;
587
+ }
588
+ return 0;
373
589
  }
374
- function supportsReasoningNone(model) {
375
- return /^gpt-5\.[1-9]/.test(model);
590
+ var ThinkTagScanner = class {
591
+ buffer = "";
592
+ inside = false;
593
+ push(chunk) {
594
+ this.buffer += chunk;
595
+ let text = "";
596
+ let thinking = "";
597
+ for (; ; ) {
598
+ const tag = this.inside ? THINK_CLOSE : THINK_OPEN;
599
+ const idx = this.buffer.indexOf(tag);
600
+ if (idx === -1) {
601
+ const hold = partialTagSuffixLen(this.buffer, tag);
602
+ const emit = this.buffer.slice(0, this.buffer.length - hold);
603
+ if (this.inside) thinking += emit;
604
+ else text += emit;
605
+ this.buffer = this.buffer.slice(this.buffer.length - hold);
606
+ break;
607
+ }
608
+ const before = this.buffer.slice(0, idx);
609
+ if (this.inside) thinking += before;
610
+ else text += before;
611
+ this.buffer = this.buffer.slice(idx + tag.length);
612
+ this.inside = !this.inside;
613
+ }
614
+ return { text, thinking };
615
+ }
616
+ /** Flush any residual buffer at stream end (treat as the current channel). */
617
+ flush() {
618
+ const rest = this.buffer;
619
+ this.buffer = "";
620
+ return this.inside ? { text: "", thinking: rest } : { text: rest, thinking: "" };
621
+ }
622
+ };
623
+ function extractThinkTags(content) {
624
+ const scanner = new ThinkTagScanner();
625
+ const a = scanner.push(content);
626
+ const b = scanner.flush();
627
+ return { content: a.text + b.text, thinking: a.thinking + b.thinking };
376
628
  }
377
- function supportsXhigh(model) {
378
- return /^gpt-5\.([2-9]|\d{2,})/.test(model);
629
+ function reasoningEffortEmit(map) {
630
+ return (body, resolved, model) => {
631
+ const value = map(resolved, model);
632
+ if (value) body.reasoning_effort = value;
633
+ };
379
634
  }
380
- function clampReasoningEffort(model, effort) {
381
- if (model.startsWith("gpt-5-pro")) return "high";
382
- if (effort === "none" && !supportsReasoningNone(model)) return "minimal";
383
- if (effort === "xhigh" && !supportsXhigh(model)) return "high";
384
- return effort;
635
+ function reasoningObjectEmit(mapEffort) {
636
+ return (body, resolved, model) => {
637
+ if (resolved.hasBudgetOverride) {
638
+ body.reasoning = { max_tokens: resolved.thinkingBudget };
639
+ } else if (resolved.activeEffort) {
640
+ body.reasoning = { effort: mapEffort(resolved.activeEffort, model) };
641
+ } else if (resolved.thinkingDisabled) {
642
+ body.reasoning = { enabled: false };
643
+ }
644
+ };
385
645
  }
386
- function effortToReasoningEffort(effort) {
387
- return effort === "max" ? "xhigh" : effort;
646
+ var ROUND_TRIP_FIELDS = ["reasoning_content", "reasoning", "reasoning_details"];
647
+ function isRoundTripField(field) {
648
+ return typeof field === "string" && ROUND_TRIP_FIELDS.includes(field);
388
649
  }
389
- function budgetToReasoningEffort(budget) {
390
- if (budget <= 1024) return "low";
391
- if (budget <= 8192) return "medium";
392
- return "high";
650
+ function normalizeMessageContent(content) {
651
+ if (typeof content === "string") return content;
652
+ if (content == null) return "";
653
+ if (!Array.isArray(content)) return "";
654
+ return content.map((part) => {
655
+ if (typeof part === "string") return part;
656
+ if (!part || typeof part !== "object") return "";
657
+ if (typeof part.text === "string") return part.text;
658
+ if (typeof part.content === "string") return part.content;
659
+ return "";
660
+ }).join("");
393
661
  }
394
- var OpenAIProvider = class {
395
- name = "openai";
662
+ var OpenAICompatibleProvider = class {
663
+ name;
664
+ /** `json_schema` is honored natively when the profile's `supportsJsonSchema`
665
+ * capability is true for this model (the default); otherwise the engine
666
+ * downgrades the request to plain `json_object` (see `buildRequestBody`),
667
+ * which does NOT enforce the schema shape. */
668
+ nativeStructuredOutputSupport(model) {
669
+ const supportsSchema = resolvePerModel(
670
+ this.profile.capabilities?.supportsJsonSchema,
671
+ model,
672
+ true
673
+ );
674
+ return supportsSchema ? "schema" : "downgraded";
675
+ }
676
+ profile;
396
677
  baseUrl;
397
- apiKey;
678
+ /** A key string, or a resolver invoked per request (expiring tokens). */
679
+ apiKeySource;
680
+ authHeader;
398
681
  governor;
399
- constructor(options = {}) {
400
- this.apiKey = options.apiKey ?? process.env.OPENAI_API_KEY ?? "";
401
- this.baseUrl = (options.baseUrl ?? process.env.OPENAI_BASE_URL ?? "https://api.openai.com/v1").replace(/\/$/, "");
682
+ constructor(options) {
683
+ const p = options.profile;
684
+ this.profile = p;
685
+ this.name = p.name;
686
+ this.apiKeySource = options.apiKey ?? (p.envApiKey ? process.env[p.envApiKey] : void 0) ?? "";
687
+ this.authHeader = options.authHeader;
688
+ const envBase = p.envBaseUrl ? process.env[p.envBaseUrl] : void 0;
689
+ const explicitBase = options.baseUrl ?? envBase;
690
+ this.baseUrl = (explicitBase ?? p.defaultBaseUrl).replace(/\/$/, "");
402
691
  this.governor = options.rateLimit ? new RateLimiter(options.rateLimit) : void 0;
403
- if (!this.apiKey) {
404
- throw new Error("OpenAI API key is required. Set OPENAI_API_KEY or pass apiKey in options.");
692
+ const label = p.label ?? p.name;
693
+ if (p.requireExplicitBaseUrl && explicitBase === void 0) {
694
+ const env = p.envBaseUrl ? ` or ${p.envBaseUrl}` : "";
695
+ throw new Error(
696
+ `${label} requires a base URL. Set providers.${p.name}.baseUrl${env} (it is resource-specific and has no default).`
697
+ );
698
+ }
699
+ if (typeof this.apiKeySource === "string" && !this.apiKeySource && !p.allowMissingApiKey) {
700
+ const env = p.envApiKey ?? "the API key env var";
701
+ throw new Error(`${label} API key is required. Set ${env} or pass apiKey in options.`);
405
702
  }
406
703
  }
407
- // ---------------------------------------------------------------------------
408
- // chat - non-streaming completion
409
- // ---------------------------------------------------------------------------
704
+ /** Resolve the API key for one request, validating against allowMissingApiKey. */
705
+ async resolveKey() {
706
+ const key = await resolveApiKey(this.apiKeySource);
707
+ if (!key && !this.profile.allowMissingApiKey) {
708
+ const label = this.profile.label ?? this.profile.name;
709
+ const env = this.profile.envApiKey ?? "the API key env var";
710
+ throw new Error(`${label} API key is required. Set ${env} or pass apiKey in options.`);
711
+ }
712
+ return key;
713
+ }
410
714
  async chat(messages, options) {
715
+ const headers = this.buildHeaders(await this.resolveKey());
411
716
  const body = this.buildRequestBody(messages, options, false);
412
717
  const res = await fetchWithRetry(
413
718
  `${this.baseUrl}/chat/completions`,
414
719
  {
415
720
  method: "POST",
416
- headers: {
417
- "Content-Type": "application/json",
418
- Authorization: `Bearer ${this.apiKey}`
419
- },
721
+ headers,
420
722
  body: JSON.stringify(body),
421
723
  signal: options.signal
422
724
  },
423
- { governor: this.governor }
725
+ { governor: this.governor, provider: this.name }
424
726
  );
425
727
  if (!res.ok) {
426
728
  const errorBody = await res.text();
427
- const message = this.extractErrorMessage(errorBody, res.status);
428
- throw new Error(message);
729
+ throw buildProviderError({
730
+ provider: this.name,
731
+ status: res.status,
732
+ headers: res.headers,
733
+ message: this.extractErrorMessage(errorBody, res.status),
734
+ body: errorBody
735
+ });
429
736
  }
430
737
  const json = await res.json();
431
- const choice = json.choices[0];
432
- const usage = json.usage ? {
433
- prompt_tokens: json.usage.prompt_tokens,
434
- completion_tokens: json.usage.completion_tokens,
435
- total_tokens: json.usage.total_tokens,
436
- reasoning_tokens: json.usage.completion_tokens_details?.reasoning_tokens,
437
- cached_tokens: json.usage.prompt_tokens_details?.cached_tokens
438
- } : void 0;
439
- const cost = usage ? estimateOpenAICost(
440
- options.model,
441
- usage.prompt_tokens,
442
- usage.completion_tokens,
443
- usage.cached_tokens
444
- ) : void 0;
445
- return {
446
- content: choice.message.content ?? "",
447
- tool_calls: choice.message.tool_calls?.map((tc) => ({
448
- id: tc.id,
449
- type: "function",
450
- function: {
451
- name: tc.function.name,
452
- arguments: tc.function.arguments
453
- }
454
- })),
455
- usage,
456
- cost
457
- };
738
+ return this.parseResponse(json, options.model);
458
739
  }
459
- // ---------------------------------------------------------------------------
460
- // stream - SSE streaming completion
461
- // ---------------------------------------------------------------------------
462
740
  async *stream(messages, options) {
741
+ const headers = this.buildHeaders(await this.resolveKey());
463
742
  const body = this.buildRequestBody(messages, options, true);
464
743
  const res = await fetchWithRetry(
465
744
  `${this.baseUrl}/chat/completions`,
466
745
  {
467
746
  method: "POST",
468
- headers: {
469
- "Content-Type": "application/json",
470
- Authorization: `Bearer ${this.apiKey}`
471
- },
747
+ headers,
472
748
  body: JSON.stringify(body),
473
749
  signal: options.signal
474
750
  },
475
- { governor: this.governor }
751
+ { governor: this.governor, provider: this.name }
476
752
  );
477
753
  if (!res.ok) {
478
754
  const errorBody = await res.text();
479
- const message = this.extractErrorMessage(errorBody, res.status);
480
- throw new Error(message);
755
+ throw buildProviderError({
756
+ provider: this.name,
757
+ status: res.status,
758
+ headers: res.headers,
759
+ message: this.extractErrorMessage(errorBody, res.status),
760
+ body: errorBody
761
+ });
481
762
  }
482
763
  if (!res.body) {
483
- throw new Error("OpenAI stream response has no body");
764
+ throw new Error(`${this.profile.label ?? this.profile.name} stream response has no body`);
484
765
  }
485
766
  yield* this.parseSSEStream(res.body, options.model);
486
767
  }
487
768
  // ---------------------------------------------------------------------------
488
- // Internal helpers
769
+ // Request building
489
770
  // ---------------------------------------------------------------------------
490
- buildRequestBody(messages, options, stream) {
491
- const oSeries = isOSeriesModel(options.model);
492
- const reasoningCapable = supportsReasoningEffort(options.model);
493
- const { thinkingBudget, thinkingDisabled, activeEffort, hasBudgetOverride } = resolveThinkingOptions(options);
494
- let wireEffort;
495
- if (reasoningCapable) {
496
- if (hasBudgetOverride) {
497
- wireEffort = clampReasoningEffort(options.model, budgetToReasoningEffort(thinkingBudget));
498
- } else if (!thinkingDisabled && activeEffort) {
499
- wireEffort = clampReasoningEffort(options.model, effortToReasoningEffort(activeEffort));
500
- } else if (thinkingDisabled) {
501
- wireEffort = clampReasoningEffort(options.model, "none");
502
- }
771
+ buildHeaders(apiKey) {
772
+ const headers = {
773
+ "Content-Type": "application/json",
774
+ ...this.profile.headers ?? {}
775
+ };
776
+ if (apiKey) {
777
+ const auth = this.authHeader ?? this.profile.authHeader ?? "bearer";
778
+ if (auth === "bearer") headers.Authorization = `Bearer ${apiKey}`;
779
+ else if (auth === "api-key") headers["api-key"] = apiKey;
780
+ else headers[auth.header] = auth.scheme ? `${auth.scheme} ${apiKey}` : apiKey;
503
781
  }
504
- const stripTemp = oSeries || reasoningCapable && wireEffort !== void 0;
782
+ return headers;
783
+ }
784
+ buildRequestBody(messages, options, stream) {
785
+ const profile = this.profile;
786
+ const resolved = resolveThinkingOptions(options);
505
787
  const body = {
506
788
  model: options.model,
507
- messages: messages.map((m) => this.formatMessage(m, oSeries)),
789
+ messages: messages.map((m) => this.formatMessage(m, options.model)),
508
790
  stream
509
791
  };
510
- if (options.temperature !== void 0 && !stripTemp) {
792
+ const emitResult = profile.reasoning.emit(body, resolved, options.model) ?? {};
793
+ const stripTemperature = emitResult.stripTemperature ?? false;
794
+ if (options.temperature !== void 0 && !stripTemperature) {
511
795
  body.temperature = options.temperature;
512
796
  }
513
797
  if (options.maxTokens !== void 0) {
514
- body.max_completion_tokens = options.maxTokens;
798
+ body[profile.maxTokensField ?? "max_completion_tokens"] = options.maxTokens;
515
799
  }
516
800
  if (options.stop) body.stop = options.stop;
517
801
  if (options.tools && options.tools.length > 0) {
518
802
  body.tools = options.tools;
519
- if (!oSeries) {
803
+ if (resolvePerModel(profile.parallelToolCalls, options.model, false)) {
520
804
  body.parallel_tool_calls = true;
521
805
  }
522
806
  }
523
- if (options.toolChoice !== void 0) {
524
- body.tool_choice = options.toolChoice;
525
- }
807
+ if (options.toolChoice !== void 0) body.tool_choice = options.toolChoice;
526
808
  if (options.responseFormat) {
527
- body.response_format = options.responseFormat;
809
+ const supportsSchema = resolvePerModel(
810
+ profile.capabilities?.supportsJsonSchema,
811
+ options.model,
812
+ true
813
+ );
814
+ body.response_format = options.responseFormat.type === "json_schema" && !supportsSchema ? { type: "json_object" } : options.responseFormat;
528
815
  }
529
- if (wireEffort) body.reasoning_effort = wireEffort;
530
- if (stream) {
816
+ if (stream && (profile.capabilities?.supportsStreamUsage ?? true)) {
531
817
  body.stream_options = { include_usage: true };
532
818
  }
533
- if (options.providerOptions) {
534
- Object.assign(body, options.providerOptions);
819
+ if (profile.requestDefaults) Object.assign(body, profile.requestDefaults);
820
+ const userKeys = options.providerOptions ? Object.keys(options.providerOptions) : [];
821
+ if (options.providerOptions) Object.assign(body, options.providerOptions);
822
+ const forbidden = resolvePerModel(profile.capabilities?.forbiddenParams, options.model, []);
823
+ for (const key of forbidden) {
824
+ if (!userKeys.includes(key)) delete body[key];
535
825
  }
536
826
  return body;
537
827
  }
538
- /** Extract a human-readable message from an API error response body. */
828
+ formatMessage(msg, model) {
829
+ const out = {
830
+ role: this.profile.roleFor ? this.profile.roleFor(msg.role, model) : msg.role,
831
+ content: msg.content
832
+ };
833
+ if (msg.name && (this.profile.capabilities?.emitsMessageName ?? true)) out.name = msg.name;
834
+ if (msg.tool_calls) out.tool_calls = msg.tool_calls;
835
+ if (msg.tool_call_id) out.tool_call_id = msg.tool_call_id;
836
+ if (this.profile.reasoning.roundTrip === "on-tool-call-turns" && msg.role === "assistant" && msg.tool_calls && msg.tool_calls.length > 0) {
837
+ const rt = msg.providerMetadata?.openaiCompatReasoning;
838
+ if (rt && rt.provider === this.name && isRoundTripField(rt.field) && rt.value !== void 0 && rt.value !== null) {
839
+ out[rt.field] = rt.value;
840
+ }
841
+ }
842
+ return out;
843
+ }
844
+ // ---------------------------------------------------------------------------
845
+ // Response parsing
846
+ // ---------------------------------------------------------------------------
539
847
  extractErrorMessage(body, status) {
848
+ const label = this.profile.label ?? this.profile.name;
540
849
  try {
541
850
  const json = JSON.parse(body);
542
- if (json.error?.message) {
543
- return `OpenAI API error (${status}): ${json.error.message}`;
544
- }
851
+ const message = json.error?.message ?? json.message ?? json.detail;
852
+ if (message) return `${label} API error (${status}): ${message}`;
545
853
  } catch {
546
854
  }
547
- return `OpenAI API error (${status}): ${body}`;
855
+ return `${label} API error (${status}): ${body}`;
548
856
  }
549
- formatMessage(msg, oSeries) {
550
- const out = {
551
- role: msg.role === "system" && oSeries ? "developer" : msg.role,
552
- content: msg.content
857
+ toUsage(raw) {
858
+ if (!raw) return void 0;
859
+ return {
860
+ prompt_tokens: raw.prompt_tokens,
861
+ completion_tokens: raw.completion_tokens,
862
+ total_tokens: raw.total_tokens,
863
+ reasoning_tokens: raw.completion_tokens_details?.reasoning_tokens,
864
+ cached_tokens: raw.prompt_tokens_details?.cached_tokens
553
865
  };
554
- if (msg.name) out.name = msg.name;
555
- if (msg.tool_calls) out.tool_calls = msg.tool_calls;
556
- if (msg.tool_call_id) out.tool_call_id = msg.tool_call_id;
557
- return out;
558
866
  }
867
+ computeCost(model, usage, reportedCost) {
868
+ const p = this.profile.pricing;
869
+ if (p.kind === "zero") return 0;
870
+ if (!usage) return void 0;
871
+ switch (p.kind) {
872
+ case "unknown":
873
+ return void 0;
874
+ case "from-response":
875
+ return typeof reportedCost === "number" && Number.isFinite(reportedCost) ? reportedCost : void 0;
876
+ case "table": {
877
+ const c = priceFromTable(
878
+ p.table,
879
+ model,
880
+ usage.prompt_tokens,
881
+ usage.completion_tokens,
882
+ usage.cached_tokens
883
+ );
884
+ return c !== void 0 && Number.isFinite(c) ? c : void 0;
885
+ }
886
+ default: {
887
+ const _exhaustive = p;
888
+ throw new Error(`Unhandled pricing source: ${JSON.stringify(_exhaustive)}`);
889
+ }
890
+ }
891
+ }
892
+ parseResponse(json, model) {
893
+ const message = json.choices?.[0]?.message ?? {};
894
+ let content = normalizeMessageContent(message.content);
895
+ let thinking;
896
+ let roundTrip;
897
+ switch (this.profile.reasoning.capture) {
898
+ case "reasoning_content":
899
+ if (typeof message.reasoning_content === "string") {
900
+ thinking = message.reasoning_content;
901
+ roundTrip = {
902
+ provider: this.name,
903
+ field: "reasoning_content",
904
+ value: message.reasoning_content
905
+ };
906
+ }
907
+ break;
908
+ case "reasoning":
909
+ if (typeof message.reasoning === "string") {
910
+ thinking = message.reasoning;
911
+ roundTrip = { provider: this.name, field: "reasoning", value: message.reasoning };
912
+ }
913
+ break;
914
+ case "reasoning_details":
915
+ if (typeof message.reasoning === "string") thinking = message.reasoning;
916
+ if (message.reasoning_details !== void 0) {
917
+ roundTrip = {
918
+ provider: this.name,
919
+ field: "reasoning_details",
920
+ value: message.reasoning_details
921
+ };
922
+ }
923
+ break;
924
+ case "think_tags": {
925
+ const ex = extractThinkTags(content);
926
+ content = ex.content;
927
+ if (ex.thinking) thinking = ex.thinking;
928
+ break;
929
+ }
930
+ case "none":
931
+ break;
932
+ default: {
933
+ const _exhaustive = this.profile.reasoning.capture;
934
+ throw new Error(`Unhandled reasoning capture: ${String(_exhaustive)}`);
935
+ }
936
+ }
937
+ const usage = this.toUsage(json.usage);
938
+ const cost = this.computeCost(model, usage, json.usage?.cost);
939
+ return {
940
+ content,
941
+ thinking_content: thinking || void 0,
942
+ tool_calls: message.tool_calls?.map((tc) => ({
943
+ id: tc.id,
944
+ type: "function",
945
+ function: { name: tc.function.name, arguments: tc.function.arguments }
946
+ })),
947
+ usage,
948
+ cost,
949
+ providerMetadata: this.roundTripMetadata(roundTrip)
950
+ };
951
+ }
952
+ roundTripMetadata(rt) {
953
+ if (!rt || this.profile.reasoning.roundTrip === void 0 || this.profile.reasoning.roundTrip === "none") {
954
+ return void 0;
955
+ }
956
+ return rt.value === void 0 || rt.value === null ? void 0 : { openaiCompatReasoning: rt };
957
+ }
958
+ // ---------------------------------------------------------------------------
959
+ // SSE streaming
960
+ // ---------------------------------------------------------------------------
559
961
  async *parseSSEStream(body, model) {
560
962
  const reader = body.getReader();
561
963
  const decoder = new TextDecoder();
562
964
  let buffer = "";
563
965
  let usageData;
966
+ const capture = this.profile.reasoning.capture;
967
+ const thinkScanner = capture === "think_tags" ? new ThinkTagScanner() : void 0;
968
+ let reasoningText = "";
969
+ const reasoningDetails = [];
564
970
  const indexToId = /* @__PURE__ */ new Map();
971
+ const pendingToolCalls = /* @__PURE__ */ new Map();
972
+ const flushPendingToolCalls = () => {
973
+ const out = [];
974
+ for (const [index, buf] of pendingToolCalls) {
975
+ out.push({
976
+ type: "tool_call_delta",
977
+ id: `__toolcall_${index}`,
978
+ name: buf.name,
979
+ arguments: buf.arguments
980
+ });
981
+ }
982
+ pendingToolCalls.clear();
983
+ return out;
984
+ };
985
+ const makeDone = () => {
986
+ const usage = this.toUsage(usageData);
987
+ let providerMetadata;
988
+ if (this.profile.reasoning.roundTrip === "on-tool-call-turns") {
989
+ if (capture === "reasoning_details" && reasoningDetails.length > 0) {
990
+ providerMetadata = {
991
+ openaiCompatReasoning: {
992
+ provider: this.name,
993
+ field: "reasoning_details",
994
+ value: reasoningDetails
995
+ }
996
+ };
997
+ } else if ((capture === "reasoning_content" || capture === "reasoning") && reasoningText) {
998
+ providerMetadata = {
999
+ openaiCompatReasoning: { provider: this.name, field: capture, value: reasoningText }
1000
+ };
1001
+ }
1002
+ }
1003
+ return {
1004
+ type: "done",
1005
+ usage,
1006
+ cost: this.computeCost(model, usage, usageData?.cost),
1007
+ providerMetadata
1008
+ };
1009
+ };
565
1010
  try {
566
- while (true) {
1011
+ for (; ; ) {
567
1012
  const { done, value } = await reader.read();
568
1013
  if (done) break;
569
1014
  buffer += decoder.decode(value, { stream: true });
@@ -573,47 +1018,76 @@ var OpenAIProvider = class {
573
1018
  const trimmed = line.trim();
574
1019
  if (!trimmed || trimmed.startsWith(":")) continue;
575
1020
  if (trimmed === "data: [DONE]") {
576
- yield {
577
- type: "done",
578
- usage: usageData,
579
- cost: usageData ? estimateOpenAICost(
580
- model,
581
- usageData.prompt_tokens,
582
- usageData.completion_tokens,
583
- usageData.cached_tokens
584
- ) : void 0
585
- };
1021
+ if (thinkScanner) {
1022
+ const flushed = thinkScanner.flush();
1023
+ if (flushed.text) yield { type: "text_delta", content: flushed.text };
1024
+ if (flushed.thinking) yield { type: "thinking_delta", content: flushed.thinking };
1025
+ }
1026
+ yield* flushPendingToolCalls();
1027
+ yield makeDone();
586
1028
  return;
587
1029
  }
588
- if (trimmed.startsWith("data: ")) {
589
- const jsonStr = trimmed.slice(6);
590
- let parsed;
591
- try {
592
- parsed = JSON.parse(jsonStr);
593
- } catch {
594
- continue;
595
- }
596
- if (parsed.usage) {
597
- usageData = {
598
- prompt_tokens: parsed.usage.prompt_tokens,
599
- completion_tokens: parsed.usage.completion_tokens,
600
- total_tokens: parsed.usage.total_tokens,
601
- reasoning_tokens: parsed.usage.completion_tokens_details?.reasoning_tokens,
602
- cached_tokens: parsed.usage.prompt_tokens_details?.cached_tokens
603
- };
604
- continue;
605
- }
606
- const delta = parsed.choices?.[0]?.delta;
607
- if (!delta) continue;
608
- if (delta.content) {
1030
+ if (!trimmed.startsWith("data: ")) continue;
1031
+ let parsed;
1032
+ try {
1033
+ parsed = JSON.parse(trimmed.slice(6));
1034
+ } catch {
1035
+ continue;
1036
+ }
1037
+ if (parsed.error) {
1038
+ const message = typeof parsed.error.message === "string" ? parsed.error.message : `${this.profile.label ?? this.profile.name} stream error`;
1039
+ throw new ProviderError({
1040
+ provider: this.name,
1041
+ status: 0,
1042
+ retryable: false,
1043
+ message,
1044
+ body: JSON.stringify(parsed.error)
1045
+ });
1046
+ }
1047
+ if (parsed.usage) {
1048
+ usageData = parsed.usage;
1049
+ if (!parsed.choices || parsed.choices.length === 0) continue;
1050
+ }
1051
+ const delta = parsed.choices?.[0]?.delta;
1052
+ if (!delta) continue;
1053
+ const deltaReasoning = capture === "reasoning_content" ? delta.reasoning_content : capture === "reasoning" || capture === "reasoning_details" ? delta.reasoning : void 0;
1054
+ if (typeof deltaReasoning === "string" && deltaReasoning.length > 0) {
1055
+ reasoningText += deltaReasoning;
1056
+ yield { type: "thinking_delta", content: deltaReasoning };
1057
+ }
1058
+ if (capture === "reasoning_details" && Array.isArray(delta.reasoning_details)) {
1059
+ reasoningDetails.push(...delta.reasoning_details);
1060
+ }
1061
+ if (typeof delta.content === "string" && delta.content.length > 0) {
1062
+ if (thinkScanner) {
1063
+ const { text, thinking } = thinkScanner.push(delta.content);
1064
+ if (text) yield { type: "text_delta", content: text };
1065
+ if (thinking) yield { type: "thinking_delta", content: thinking };
1066
+ } else {
609
1067
  yield { type: "text_delta", content: delta.content };
610
1068
  }
611
- if (delta.tool_calls) {
612
- for (const tc of delta.tool_calls) {
613
- if (tc.id) {
614
- indexToId.set(tc.index, tc.id);
615
- }
616
- const id = indexToId.get(tc.index) ?? `__pending_${tc.index}`;
1069
+ }
1070
+ if (delta.tool_calls) {
1071
+ for (const tc of delta.tool_calls) {
1072
+ if (tc.id) indexToId.set(tc.index, tc.id);
1073
+ const id = indexToId.get(tc.index);
1074
+ if (id === void 0) {
1075
+ const buf2 = pendingToolCalls.get(tc.index) ?? { arguments: "" };
1076
+ if (tc.function?.name) buf2.name = tc.function.name;
1077
+ if (tc.function?.arguments) buf2.arguments += tc.function.arguments;
1078
+ pendingToolCalls.set(tc.index, buf2);
1079
+ continue;
1080
+ }
1081
+ const buf = pendingToolCalls.get(tc.index);
1082
+ if (buf) {
1083
+ pendingToolCalls.delete(tc.index);
1084
+ yield {
1085
+ type: "tool_call_delta",
1086
+ id,
1087
+ name: buf.name ?? tc.function?.name,
1088
+ arguments: buf.arguments + (tc.function?.arguments ?? "")
1089
+ };
1090
+ } else {
617
1091
  yield {
618
1092
  type: "tool_call_delta",
619
1093
  id,
@@ -625,58 +1099,176 @@ var OpenAIProvider = class {
625
1099
  }
626
1100
  }
627
1101
  }
628
- yield {
629
- type: "done",
630
- usage: usageData,
631
- cost: usageData ? estimateOpenAICost(
632
- model,
633
- usageData.prompt_tokens,
634
- usageData.completion_tokens,
635
- usageData.cached_tokens
636
- ) : void 0
637
- };
1102
+ if (thinkScanner) {
1103
+ const flushed = thinkScanner.flush();
1104
+ if (flushed.text) yield { type: "text_delta", content: flushed.text };
1105
+ if (flushed.thinking) yield { type: "thinking_delta", content: flushed.thinking };
1106
+ }
1107
+ yield* flushPendingToolCalls();
1108
+ throw new ProviderError({
1109
+ provider: this.name,
1110
+ status: 0,
1111
+ retryable: true,
1112
+ message: `${this.profile.label ?? this.profile.name} stream ended before [DONE]`
1113
+ });
638
1114
  } finally {
639
1115
  reader.releaseLock();
640
1116
  }
641
1117
  }
642
1118
  };
643
1119
 
1120
+ // src/providers/openai.ts
1121
+ var OPENAI_PRICING = {
1122
+ // gpt-4o era — cache reads at 50% of input rate
1123
+ "gpt-4o": [25e-7, 1e-5, 0.5],
1124
+ "gpt-4o-mini": [15e-8, 6e-7, 0.5],
1125
+ "gpt-4-turbo": [1e-5, 3e-5, 0.5],
1126
+ "gpt-4": [3e-5, 6e-5, 0.5],
1127
+ "gpt-3.5-turbo": [5e-7, 15e-7, 0.5],
1128
+ o1: [15e-6, 6e-5, 0.5],
1129
+ "o1-mini": [3e-6, 12e-6, 0.5],
1130
+ "o1-pro": [15e-5, 6e-4, 0.5],
1131
+ // gpt-4.1 / o3 / o4 era — cache reads at 25% of input rate
1132
+ "gpt-4.1": [2e-6, 8e-6, 0.25],
1133
+ "gpt-4.1-mini": [4e-7, 16e-7, 0.25],
1134
+ "gpt-4.1-nano": [1e-7, 4e-7, 0.25],
1135
+ o3: [1e-5, 4e-5, 0.25],
1136
+ "o3-mini": [11e-7, 44e-7, 0.25],
1137
+ "o3-pro": [2e-5, 8e-5, 0.25],
1138
+ "o4-mini": [11e-7, 44e-7, 0.25],
1139
+ // gpt-5 era — cache reads at 10% of input rate
1140
+ "gpt-5": [125e-8, 1e-5, 0.1],
1141
+ "gpt-5-mini": [25e-8, 2e-6, 0.1],
1142
+ "gpt-5-nano": [5e-8, 4e-7, 0.1],
1143
+ "gpt-5.1": [125e-8, 1e-5, 0.1],
1144
+ "gpt-5.2": [175e-8, 14e-6, 0.1],
1145
+ "gpt-5.3": [175e-8, 14e-6, 0.1],
1146
+ "gpt-5.4": [25e-7, 15e-6, 0.1],
1147
+ "gpt-5.4-pro": [3e-5, 18e-5, 0.1],
1148
+ // gpt-5.5 era (flagship released 2026-04) — cache reads at 10% of input rate
1149
+ "gpt-5.5": [5e-6, 3e-5, 0.1],
1150
+ "gpt-5.5-pro": [3e-5, 18e-5, 0.1]
1151
+ };
1152
+ function estimateOpenAICost(model, promptTokens, completionTokens, cachedTokens) {
1153
+ return priceFromTable(OPENAI_PRICING, model, promptTokens, completionTokens, cachedTokens);
1154
+ }
1155
+ function isOSeriesModel(model) {
1156
+ return /^(o1|o3|o4-mini)/.test(model);
1157
+ }
1158
+ function supportsReasoningEffort(model) {
1159
+ return isOSeriesModel(model) || /^gpt-5/.test(model);
1160
+ }
1161
+ function supportsReasoningNone(model) {
1162
+ return /^gpt-5\.[1-9]/.test(model);
1163
+ }
1164
+ function supportsXhigh(model) {
1165
+ return /^gpt-5\.([2-9]|\d{2,})/.test(model);
1166
+ }
1167
+ function clampReasoningEffort(model, effort) {
1168
+ if (model.startsWith("gpt-5-pro")) return "high";
1169
+ if (effort === "none" && !supportsReasoningNone(model)) return "minimal";
1170
+ if (effort === "xhigh" && !supportsXhigh(model)) return "high";
1171
+ return effort;
1172
+ }
1173
+ function effortToReasoningEffort(effort) {
1174
+ return effort === "max" ? "xhigh" : effort;
1175
+ }
1176
+ function budgetToReasoningEffort(budget) {
1177
+ if (budget <= 1024) return "low";
1178
+ if (budget <= 8192) return "medium";
1179
+ return "high";
1180
+ }
1181
+ var openaiReasoningEmit = (body, resolved, model) => {
1182
+ const oSeries = isOSeriesModel(model);
1183
+ const reasoningCapable = supportsReasoningEffort(model);
1184
+ const { thinkingBudget, thinkingDisabled, activeEffort, hasBudgetOverride } = resolved;
1185
+ let wireEffort;
1186
+ if (reasoningCapable) {
1187
+ if (hasBudgetOverride) {
1188
+ wireEffort = clampReasoningEffort(model, budgetToReasoningEffort(thinkingBudget));
1189
+ } else if (!thinkingDisabled && activeEffort) {
1190
+ wireEffort = clampReasoningEffort(model, effortToReasoningEffort(activeEffort));
1191
+ } else if (thinkingDisabled) {
1192
+ wireEffort = clampReasoningEffort(model, "none");
1193
+ }
1194
+ }
1195
+ if (wireEffort) body.reasoning_effort = wireEffort;
1196
+ return { stripTemperature: oSeries || reasoningCapable && wireEffort !== void 0 };
1197
+ };
1198
+ var OPENAI_PROFILE = {
1199
+ name: "openai",
1200
+ label: "OpenAI",
1201
+ defaultBaseUrl: "https://api.openai.com/v1",
1202
+ envApiKey: "OPENAI_API_KEY",
1203
+ envBaseUrl: "OPENAI_BASE_URL",
1204
+ pricing: { kind: "table", table: OPENAI_PRICING },
1205
+ reasoning: { emit: openaiReasoningEmit, capture: "none" },
1206
+ roleFor: (role, model) => role === "system" && isOSeriesModel(model) ? "developer" : role,
1207
+ maxTokensField: "max_completion_tokens",
1208
+ parallelToolCalls: (model) => !isOSeriesModel(model)
1209
+ };
1210
+ var OpenAIProvider = class extends OpenAICompatibleProvider {
1211
+ constructor(options = {}) {
1212
+ super({ profile: OPENAI_PROFILE, ...options });
1213
+ }
1214
+ };
1215
+
644
1216
  // src/providers/openai-responses.ts
645
1217
  var OpenAIResponsesProvider = class {
646
1218
  name = "openai-responses";
1219
+ /** The Responses API honors `json_schema` (structured outputs) natively. */
1220
+ nativeStructuredOutputSupport() {
1221
+ return "schema";
1222
+ }
647
1223
  baseUrl;
648
- apiKey;
1224
+ apiKeySource;
649
1225
  governor;
650
1226
  constructor(options = {}) {
651
- this.apiKey = options.apiKey ?? process.env.OPENAI_API_KEY ?? "";
1227
+ this.apiKeySource = options.apiKey ?? process.env.OPENAI_API_KEY ?? "";
652
1228
  this.baseUrl = (options.baseUrl ?? process.env.OPENAI_BASE_URL ?? "https://api.openai.com/v1").replace(/\/$/, "");
653
1229
  this.governor = options.rateLimit ? new RateLimiter(options.rateLimit) : void 0;
654
- if (!this.apiKey) {
1230
+ if (typeof this.apiKeySource === "string" && !this.apiKeySource) {
1231
+ throw new Error("OpenAI API key is required. Set OPENAI_API_KEY or pass apiKey in options.");
1232
+ }
1233
+ }
1234
+ /** Resolve the API key for one request (supports an expiring-token callback). */
1235
+ async resolveKey() {
1236
+ const key = await resolveApiKey(this.apiKeySource);
1237
+ if (!key) {
655
1238
  throw new Error("OpenAI API key is required. Set OPENAI_API_KEY or pass apiKey in options.");
656
1239
  }
1240
+ return key;
1241
+ }
1242
+ /** Build request headers from a resolved key (used by both chat and stream). */
1243
+ buildHeaders(apiKey) {
1244
+ return { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` };
657
1245
  }
658
1246
  // ---------------------------------------------------------------------------
659
1247
  // chat - non-streaming
660
1248
  // ---------------------------------------------------------------------------
661
1249
  async chat(messages, options) {
1250
+ const headers = this.buildHeaders(await this.resolveKey());
662
1251
  const body = this.buildRequestBody(messages, options, false);
663
1252
  const res = await fetchWithRetry(
664
1253
  `${this.baseUrl}/responses`,
665
1254
  {
666
1255
  method: "POST",
667
- headers: {
668
- "Content-Type": "application/json",
669
- Authorization: `Bearer ${this.apiKey}`
670
- },
1256
+ headers,
671
1257
  body: JSON.stringify(body),
672
1258
  signal: options.signal
673
1259
  },
674
- { governor: this.governor }
1260
+ { governor: this.governor, provider: this.name }
675
1261
  );
676
1262
  if (!res.ok) {
677
1263
  const errorBody = await res.text();
678
1264
  const message = this.extractErrorMessage(errorBody, res.status);
679
- throw new Error(message);
1265
+ throw buildProviderError({
1266
+ provider: this.name,
1267
+ status: res.status,
1268
+ headers: res.headers,
1269
+ message,
1270
+ body: errorBody
1271
+ });
680
1272
  }
681
1273
  const json = await res.json();
682
1274
  return this.parseResponse(json, options.model);
@@ -685,24 +1277,28 @@ var OpenAIResponsesProvider = class {
685
1277
  // stream - SSE streaming
686
1278
  // ---------------------------------------------------------------------------
687
1279
  async *stream(messages, options) {
1280
+ const headers = this.buildHeaders(await this.resolveKey());
688
1281
  const body = this.buildRequestBody(messages, options, true);
689
1282
  const res = await fetchWithRetry(
690
1283
  `${this.baseUrl}/responses`,
691
1284
  {
692
1285
  method: "POST",
693
- headers: {
694
- "Content-Type": "application/json",
695
- Authorization: `Bearer ${this.apiKey}`
696
- },
1286
+ headers,
697
1287
  body: JSON.stringify(body),
698
1288
  signal: options.signal
699
1289
  },
700
- { governor: this.governor }
1290
+ { governor: this.governor, provider: this.name }
701
1291
  );
702
1292
  if (!res.ok) {
703
1293
  const errorBody = await res.text();
704
1294
  const message = this.extractErrorMessage(errorBody, res.status);
705
- throw new Error(message);
1295
+ throw buildProviderError({
1296
+ provider: this.name,
1297
+ status: res.status,
1298
+ headers: res.headers,
1299
+ message,
1300
+ body: errorBody
1301
+ });
706
1302
  }
707
1303
  if (!res.body) {
708
1304
  throw new Error("OpenAI Responses stream has no body");
@@ -991,7 +1587,12 @@ var OpenAIResponsesProvider = class {
991
1587
  }
992
1588
  case "response.failed": {
993
1589
  const errorMsg = data.response?.error?.message ?? data.response?.status_details?.error?.message ?? "Unknown error";
994
- throw new Error(`OpenAI Responses API error: ${errorMsg}`);
1590
+ throw new ProviderError({
1591
+ provider: this.name,
1592
+ status: 0,
1593
+ retryable: false,
1594
+ message: `OpenAI Responses API error: ${errorMsg}`
1595
+ });
995
1596
  }
996
1597
  default:
997
1598
  return null;
@@ -1045,7 +1646,7 @@ function estimateAnthropicCost(model, inputTokens, outputTokens, cacheReadTokens
1045
1646
  }
1046
1647
  }
1047
1648
  }
1048
- if (!pricing) return 0;
1649
+ if (!pricing) return void 0;
1049
1650
  const [inputRate, outputRate] = pricing;
1050
1651
  const cacheRead = cacheReadTokens ?? 0;
1051
1652
  const cacheWrite = cacheWriteTokens ?? 0;
@@ -1079,63 +1680,90 @@ function clampAnthropicEffort(model, effort) {
1079
1680
  }
1080
1681
  var AnthropicProvider = class {
1081
1682
  name = "anthropic";
1683
+ /** Anthropic ignores native `json_schema` structurally — Axl uses a system
1684
+ * prompt JSON instruction + client-side Zod validation (see buildRequest). */
1685
+ nativeStructuredOutputSupport() {
1686
+ return "unsupported";
1687
+ }
1082
1688
  baseUrl;
1083
- apiKey;
1084
- currentModel;
1689
+ apiKeySource;
1085
1690
  governor;
1086
1691
  constructor(options = {}) {
1087
- this.apiKey = options.apiKey ?? process.env.ANTHROPIC_API_KEY ?? "";
1692
+ this.apiKeySource = options.apiKey ?? process.env.ANTHROPIC_API_KEY ?? "";
1088
1693
  this.baseUrl = (options.baseUrl ?? "https://api.anthropic.com/v1").replace(/\/$/, "");
1089
1694
  this.governor = options.rateLimit ? new RateLimiter(options.rateLimit) : void 0;
1090
- if (!this.apiKey) {
1695
+ if (typeof this.apiKeySource === "string" && !this.apiKeySource) {
1696
+ throw new Error(
1697
+ "Anthropic API key is required. Set ANTHROPIC_API_KEY or pass apiKey in options."
1698
+ );
1699
+ }
1700
+ }
1701
+ /** Resolve the API key for one request (supports an expiring-token callback). */
1702
+ async resolveKey() {
1703
+ const key = await resolveApiKey(this.apiKeySource);
1704
+ if (!key) {
1091
1705
  throw new Error(
1092
1706
  "Anthropic API key is required. Set ANTHROPIC_API_KEY or pass apiKey in options."
1093
1707
  );
1094
1708
  }
1709
+ return key;
1095
1710
  }
1096
1711
  // ---------------------------------------------------------------------------
1097
1712
  // chat - non-streaming completion
1098
1713
  // ---------------------------------------------------------------------------
1099
1714
  async chat(messages, options) {
1100
- this.currentModel = options.model;
1715
+ const headers = this.buildHeaders(await this.resolveKey());
1101
1716
  const body = this.buildRequestBody(messages, options, false);
1102
1717
  const res = await fetchWithRetry(
1103
1718
  `${this.baseUrl}/messages`,
1104
1719
  {
1105
1720
  method: "POST",
1106
- headers: this.buildHeaders(),
1721
+ headers,
1107
1722
  body: JSON.stringify(body),
1108
1723
  signal: options.signal
1109
1724
  },
1110
- { governor: this.governor }
1725
+ { governor: this.governor, provider: this.name }
1111
1726
  );
1112
1727
  if (!res.ok) {
1113
1728
  const errorBody = await res.text();
1114
1729
  const message = this.extractErrorMessage(errorBody, res.status);
1115
- throw new Error(message);
1730
+ throw buildProviderError({
1731
+ provider: this.name,
1732
+ status: res.status,
1733
+ headers: res.headers,
1734
+ message,
1735
+ body: errorBody
1736
+ });
1116
1737
  }
1117
1738
  const json = await res.json();
1118
- return this.parseResponse(json);
1739
+ return this.parseResponse(json, options.model);
1119
1740
  }
1120
1741
  // ---------------------------------------------------------------------------
1121
1742
  // stream - SSE streaming completion
1122
1743
  // ---------------------------------------------------------------------------
1123
1744
  async *stream(messages, options) {
1745
+ const headers = this.buildHeaders(await this.resolveKey());
1124
1746
  const body = this.buildRequestBody(messages, options, true);
1125
1747
  const res = await fetchWithRetry(
1126
1748
  `${this.baseUrl}/messages`,
1127
1749
  {
1128
1750
  method: "POST",
1129
- headers: this.buildHeaders(),
1751
+ headers,
1130
1752
  body: JSON.stringify(body),
1131
1753
  signal: options.signal
1132
1754
  },
1133
- { governor: this.governor }
1755
+ { governor: this.governor, provider: this.name }
1134
1756
  );
1135
1757
  if (!res.ok) {
1136
1758
  const errorBody = await res.text();
1137
1759
  const message = this.extractErrorMessage(errorBody, res.status);
1138
- throw new Error(message);
1760
+ throw buildProviderError({
1761
+ provider: this.name,
1762
+ status: res.status,
1763
+ headers: res.headers,
1764
+ message,
1765
+ body: errorBody
1766
+ });
1139
1767
  }
1140
1768
  if (!res.body) {
1141
1769
  throw new Error("Anthropic stream response has no body");
@@ -1145,10 +1773,10 @@ var AnthropicProvider = class {
1145
1773
  // ---------------------------------------------------------------------------
1146
1774
  // Internal: request building
1147
1775
  // ---------------------------------------------------------------------------
1148
- buildHeaders() {
1776
+ buildHeaders(apiKey) {
1149
1777
  return {
1150
1778
  "Content-Type": "application/json",
1151
- "x-api-key": this.apiKey,
1779
+ "x-api-key": apiKey,
1152
1780
  "anthropic-version": ANTHROPIC_API_VERSION
1153
1781
  };
1154
1782
  }
@@ -1333,7 +1961,7 @@ ${jsonInstruction}` : jsonInstruction;
1333
1961
  // ---------------------------------------------------------------------------
1334
1962
  // Internal: response parsing
1335
1963
  // ---------------------------------------------------------------------------
1336
- parseResponse(json) {
1964
+ parseResponse(json, model) {
1337
1965
  let content = "";
1338
1966
  let thinkingContent = "";
1339
1967
  const toolCalls = [];
@@ -1362,13 +1990,7 @@ ${jsonInstruction}` : jsonInstruction;
1362
1990
  total_tokens: totalInput + json.usage.output_tokens,
1363
1991
  cached_tokens: cacheRead > 0 ? cacheRead : void 0
1364
1992
  } : void 0;
1365
- const cost = json.usage ? estimateAnthropicCost(
1366
- this.currentModel ?? "",
1367
- totalInput,
1368
- json.usage.output_tokens,
1369
- cacheRead,
1370
- cacheWrite
1371
- ) : void 0;
1993
+ const cost = json.usage ? estimateAnthropicCost(model, totalInput, json.usage.output_tokens, cacheRead, cacheWrite) : void 0;
1372
1994
  return {
1373
1995
  content,
1374
1996
  thinking_content: thinkingContent || void 0,
@@ -1544,6 +2166,15 @@ function sanitizeSchemaForGemini(schema) {
1544
2166
  }
1545
2167
  return out;
1546
2168
  }
2169
+ function parseGeminiFunctionResponse(content) {
2170
+ let parsed;
2171
+ try {
2172
+ parsed = JSON.parse(content);
2173
+ } catch {
2174
+ return { result: content };
2175
+ }
2176
+ return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : { result: parsed };
2177
+ }
1547
2178
  var GEMINI_PRICING = {
1548
2179
  "gemini-2.5-pro": [125e-8, 1e-5],
1549
2180
  "gemini-2.5-flash": [3e-7, 25e-7],
@@ -1570,7 +2201,7 @@ function estimateGeminiCost(model, inputTokens, outputTokens, cachedTokens) {
1570
2201
  }
1571
2202
  }
1572
2203
  }
1573
- if (!pricing) return 0;
2204
+ if (!pricing) return void 0;
1574
2205
  const [inputRate, outputRate] = pricing;
1575
2206
  const cached = cachedTokens ?? 0;
1576
2207
  const inputCost = (inputTokens - cached) * inputRate + cached * inputRate * 0.1;
@@ -1615,40 +2246,61 @@ function warnGemini3xEffortNone(model) {
1615
2246
  }
1616
2247
  var GeminiProvider = class {
1617
2248
  name = "google";
2249
+ /** Gemini accepts a `responseSchema` but `sanitizeSchemaForGemini` strips
2250
+ * keywords it doesn't support (`$ref`/`$defs`/`additionalProperties`/…), so a
2251
+ * derived schema can lose constraints — lossy, not faithful. */
2252
+ nativeStructuredOutputSupport() {
2253
+ return "lossy";
2254
+ }
1618
2255
  baseUrl;
1619
- apiKey;
2256
+ apiKeySource;
1620
2257
  callCounter = 0;
1621
2258
  governor;
1622
2259
  constructor(options = {}) {
1623
- this.apiKey = options.apiKey ?? process.env.GOOGLE_API_KEY ?? process.env.GEMINI_API_KEY ?? "";
2260
+ this.apiKeySource = options.apiKey ?? process.env.GOOGLE_API_KEY ?? process.env.GEMINI_API_KEY ?? "";
1624
2261
  this.baseUrl = (options.baseUrl ?? "https://generativelanguage.googleapis.com/v1beta").replace(
1625
2262
  /\/$/,
1626
2263
  ""
1627
2264
  );
1628
2265
  this.governor = options.rateLimit ? new RateLimiter(options.rateLimit) : void 0;
1629
- if (!this.apiKey) {
2266
+ if (typeof this.apiKeySource === "string" && !this.apiKeySource) {
2267
+ throw new Error("Google API key is required. Set GOOGLE_API_KEY or pass apiKey in options.");
2268
+ }
2269
+ }
2270
+ /** Resolve the API key for one request (supports an expiring-token callback). */
2271
+ async resolveKey() {
2272
+ const key = await resolveApiKey(this.apiKeySource);
2273
+ if (!key) {
1630
2274
  throw new Error("Google API key is required. Set GOOGLE_API_KEY or pass apiKey in options.");
1631
2275
  }
2276
+ return key;
1632
2277
  }
1633
2278
  // ---------------------------------------------------------------------------
1634
2279
  // chat - non-streaming completion
1635
2280
  // ---------------------------------------------------------------------------
1636
2281
  async chat(messages, options) {
2282
+ const headers = this.buildHeaders(await this.resolveKey());
1637
2283
  const body = this.buildRequestBody(messages, options);
1638
2284
  const res = await fetchWithRetry(
1639
2285
  `${this.baseUrl}/models/${options.model}:generateContent`,
1640
2286
  {
1641
2287
  method: "POST",
1642
- headers: this.buildHeaders(),
2288
+ headers,
1643
2289
  body: JSON.stringify(body),
1644
2290
  signal: options.signal
1645
2291
  },
1646
- { governor: this.governor }
2292
+ { governor: this.governor, provider: this.name }
1647
2293
  );
1648
2294
  if (!res.ok) {
1649
2295
  const errorBody = await res.text();
1650
2296
  const message = this.extractErrorMessage(errorBody, res.status);
1651
- throw new Error(message);
2297
+ throw buildProviderError({
2298
+ provider: this.name,
2299
+ status: res.status,
2300
+ headers: res.headers,
2301
+ message,
2302
+ body: errorBody
2303
+ });
1652
2304
  }
1653
2305
  const json = await res.json();
1654
2306
  return this.parseResponse(json, options.model);
@@ -1657,21 +2309,28 @@ var GeminiProvider = class {
1657
2309
  // stream - SSE streaming completion
1658
2310
  // ---------------------------------------------------------------------------
1659
2311
  async *stream(messages, options) {
2312
+ const headers = this.buildHeaders(await this.resolveKey());
1660
2313
  const body = this.buildRequestBody(messages, options);
1661
2314
  const res = await fetchWithRetry(
1662
2315
  `${this.baseUrl}/models/${options.model}:streamGenerateContent?alt=sse`,
1663
2316
  {
1664
2317
  method: "POST",
1665
- headers: this.buildHeaders(),
2318
+ headers,
1666
2319
  body: JSON.stringify(body),
1667
2320
  signal: options.signal
1668
2321
  },
1669
- { governor: this.governor }
2322
+ { governor: this.governor, provider: this.name }
1670
2323
  );
1671
2324
  if (!res.ok) {
1672
2325
  const errorBody = await res.text();
1673
2326
  const message = this.extractErrorMessage(errorBody, res.status);
1674
- throw new Error(message);
2327
+ throw buildProviderError({
2328
+ provider: this.name,
2329
+ status: res.status,
2330
+ headers: res.headers,
2331
+ message,
2332
+ body: errorBody
2333
+ });
1675
2334
  }
1676
2335
  if (!res.body) {
1677
2336
  throw new Error("Gemini stream response has no body");
@@ -1681,10 +2340,10 @@ var GeminiProvider = class {
1681
2340
  // ---------------------------------------------------------------------------
1682
2341
  // Internal: request building
1683
2342
  // ---------------------------------------------------------------------------
1684
- buildHeaders() {
2343
+ buildHeaders(apiKey) {
1685
2344
  return {
1686
2345
  "Content-Type": "application/json",
1687
- "x-goog-api-key": this.apiKey
2346
+ "x-goog-api-key": apiKey
1688
2347
  };
1689
2348
  }
1690
2349
  extractErrorMessage(body, status) {
@@ -1851,15 +2510,9 @@ var GeminiProvider = class {
1851
2510
  }
1852
2511
  } else if (msg.role === "tool") {
1853
2512
  const functionName = toolCallIdToName.get(msg.tool_call_id) ?? "unknown";
1854
- let responseData;
1855
- try {
1856
- responseData = JSON.parse(msg.content);
1857
- } catch {
1858
- responseData = { result: msg.content };
1859
- }
1860
2513
  const functionResponse = {
1861
2514
  name: functionName,
1862
- response: responseData
2515
+ response: parseGeminiFunctionResponse(msg.content)
1863
2516
  };
1864
2517
  if (msg.tool_call_id && geminiNativeIds.has(msg.tool_call_id)) {
1865
2518
  functionResponse.id = msg.tool_call_id;
@@ -2041,6 +2694,203 @@ var GeminiProvider = class {
2041
2694
  }
2042
2695
  };
2043
2696
 
2697
+ // src/providers/profiles/openrouter.ts
2698
+ var OPENROUTER_PROFILE = {
2699
+ name: "openrouter",
2700
+ label: "OpenRouter",
2701
+ defaultBaseUrl: "https://openrouter.ai/api/v1",
2702
+ envApiKey: "OPENROUTER_API_KEY",
2703
+ envBaseUrl: "OPENROUTER_BASE_URL",
2704
+ pricing: { kind: "from-response" },
2705
+ // Ask OpenRouter to report per-call cost in usage.cost.
2706
+ requestDefaults: { usage: { include: true } },
2707
+ reasoning: {
2708
+ // OpenRouter's effort vocabulary is low|medium|high; xhigh/max clamp to high.
2709
+ emit: reasoningObjectEmit(
2710
+ (effort) => effort === "xhigh" || effort === "max" ? "high" : effort
2711
+ ),
2712
+ capture: "reasoning_details",
2713
+ roundTrip: "on-tool-call-turns"
2714
+ },
2715
+ // OpenRouter normalizes to max_tokens across its backends.
2716
+ maxTokensField: "max_tokens"
2717
+ };
2718
+
2719
+ // src/providers/profiles/azure.ts
2720
+ var AZURE_PROFILE = {
2721
+ name: "azure",
2722
+ label: "Azure OpenAI",
2723
+ // Never actually used — requireExplicitBaseUrl forces a real value first.
2724
+ defaultBaseUrl: "https://YOUR-RESOURCE.openai.azure.com/openai/v1",
2725
+ requireExplicitBaseUrl: true,
2726
+ envApiKey: "AZURE_OPENAI_API_KEY",
2727
+ envBaseUrl: "AZURE_OPENAI_BASE_URL",
2728
+ authHeader: "api-key",
2729
+ pricing: { kind: "table", table: OPENAI_PRICING },
2730
+ reasoning: { emit: openaiReasoningEmit, capture: "none" },
2731
+ roleFor: (role, model) => role === "system" && isOSeriesModel(model) ? "developer" : role,
2732
+ maxTokensField: "max_completion_tokens",
2733
+ parallelToolCalls: (model) => !isOSeriesModel(model)
2734
+ };
2735
+
2736
+ // src/providers/profiles/xai.ts
2737
+ var acceptsReasoningEffort = (model) => model.startsWith("grok-3-mini");
2738
+ var isReasoningModel = (model) => /^grok-(3-mini|4)/.test(model);
2739
+ var XAI_PROFILE = {
2740
+ name: "xai",
2741
+ label: "xAI",
2742
+ defaultBaseUrl: "https://api.x.ai/v1",
2743
+ envApiKey: "XAI_API_KEY",
2744
+ envBaseUrl: "XAI_BASE_URL",
2745
+ pricing: { kind: "unknown" },
2746
+ reasoning: {
2747
+ emit: reasoningEffortEmit((resolved, model) => {
2748
+ if (!acceptsReasoningEffort(model)) return void 0;
2749
+ if (resolved.thinkingDisabled || !resolved.activeEffort) return void 0;
2750
+ return resolved.activeEffort === "low" ? "low" : "high";
2751
+ }),
2752
+ capture: "reasoning"
2753
+ },
2754
+ capabilities: {
2755
+ forbiddenParams: (model) => isReasoningModel(model) ? ["stop", "presence_penalty", "frequency_penalty"] : []
2756
+ }
2757
+ };
2758
+
2759
+ // src/providers/profiles/deepseek.ts
2760
+ var isReasoner = (model) => /reasoner|v4/i.test(model);
2761
+ var deepseekEmit = (_body, _resolved, model) => ({
2762
+ stripTemperature: isReasoner(model)
2763
+ });
2764
+ var DEEPSEEK_PROFILE = {
2765
+ name: "deepseek",
2766
+ label: "DeepSeek",
2767
+ defaultBaseUrl: "https://api.deepseek.com/v1",
2768
+ envApiKey: "DEEPSEEK_API_KEY",
2769
+ envBaseUrl: "DEEPSEEK_BASE_URL",
2770
+ pricing: { kind: "unknown" },
2771
+ reasoning: {
2772
+ emit: deepseekEmit,
2773
+ capture: "reasoning_content",
2774
+ roundTrip: "on-tool-call-turns"
2775
+ },
2776
+ capabilities: {
2777
+ supportsJsonSchema: false,
2778
+ forbiddenParams: (model) => isReasoner(model) ? ["top_p", "presence_penalty", "frequency_penalty"] : []
2779
+ }
2780
+ };
2781
+
2782
+ // src/providers/profiles/mistral.ts
2783
+ var acceptsReasoningEffort2 = (model) => /^mistral-(small|medium)/i.test(model);
2784
+ var MISTRAL_PROFILE = {
2785
+ name: "mistral",
2786
+ label: "Mistral",
2787
+ defaultBaseUrl: "https://api.mistral.ai/v1",
2788
+ envApiKey: "MISTRAL_API_KEY",
2789
+ envBaseUrl: "MISTRAL_BASE_URL",
2790
+ pricing: { kind: "unknown" },
2791
+ reasoning: {
2792
+ emit: reasoningEffortEmit((resolved, model) => {
2793
+ if (!acceptsReasoningEffort2(model)) return void 0;
2794
+ if (resolved.thinkingDisabled || !resolved.activeEffort) return void 0;
2795
+ return "high";
2796
+ }),
2797
+ capture: "reasoning_content"
2798
+ }
2799
+ };
2800
+
2801
+ // src/providers/profiles/groq.ts
2802
+ var isGptOss = (model) => /gpt-oss/i.test(model);
2803
+ var GROQ_PROFILE = {
2804
+ name: "groq",
2805
+ label: "Groq",
2806
+ defaultBaseUrl: "https://api.groq.com/openai/v1",
2807
+ envApiKey: "GROQ_API_KEY",
2808
+ envBaseUrl: "GROQ_BASE_URL",
2809
+ pricing: { kind: "unknown" },
2810
+ reasoning: {
2811
+ emit: reasoningEffortEmit((resolved, model) => {
2812
+ if (!isGptOss(model) || resolved.thinkingDisabled || !resolved.activeEffort) {
2813
+ return void 0;
2814
+ }
2815
+ const e = resolved.activeEffort;
2816
+ return e === "xhigh" || e === "max" ? "high" : e;
2817
+ }),
2818
+ capture: "reasoning"
2819
+ },
2820
+ capabilities: {
2821
+ emitsMessageName: false,
2822
+ // Groq's `response_format: json_schema` is supported only on a subset of
2823
+ // models — the `openai/gpt-oss-*` family accepts it; llama/gemma/etc 400
2824
+ // with "This model does not support response format `json_schema`".
2825
+ // Per-model so `nativeStructuredOutput` engages the native path only where
2826
+ // it works and otherwise downgrades cleanly to `json_object` (verified live).
2827
+ supportsJsonSchema: isGptOss
2828
+ }
2829
+ };
2830
+
2831
+ // src/providers/profiles/bedrock.ts
2832
+ var isGptOss2 = (model) => /gpt-oss/i.test(model);
2833
+ var BEDROCK_PROFILE = {
2834
+ name: "bedrock",
2835
+ label: "AWS Bedrock",
2836
+ // Never used — requireExplicitBaseUrl forces a real (region-specific) value first.
2837
+ defaultBaseUrl: "https://bedrock-mantle.us-east-1.api.aws/v1",
2838
+ requireExplicitBaseUrl: true,
2839
+ envApiKey: "AWS_BEARER_TOKEN_BEDROCK",
2840
+ envBaseUrl: "BEDROCK_BASE_URL",
2841
+ pricing: { kind: "unknown" },
2842
+ reasoning: {
2843
+ emit: reasoningEffortEmit((resolved, model) => {
2844
+ if (!isGptOss2(model) || resolved.thinkingDisabled || !resolved.activeEffort) {
2845
+ return void 0;
2846
+ }
2847
+ const e = resolved.activeEffort;
2848
+ return e === "xhigh" || e === "max" ? "high" : e;
2849
+ }),
2850
+ capture: "reasoning"
2851
+ }
2852
+ };
2853
+
2854
+ // src/providers/profiles/local.ts
2855
+ function localProfile(name, label, port, envApiKey) {
2856
+ return {
2857
+ name,
2858
+ label,
2859
+ defaultBaseUrl: `http://localhost:${port}/v1`,
2860
+ envApiKey,
2861
+ envBaseUrl: `${envApiKey.replace(/_API_KEY$/, "")}_BASE_URL`,
2862
+ allowMissingApiKey: true,
2863
+ pricing: { kind: "zero" },
2864
+ reasoning: { emit: () => {
2865
+ }, capture: "think_tags" },
2866
+ maxTokensField: "max_tokens"
2867
+ };
2868
+ }
2869
+ var OLLAMA_PROFILE = localProfile("ollama", "Ollama", 11434, "OLLAMA_API_KEY");
2870
+ var VLLM_PROFILE = localProfile("vllm", "vLLM", 8e3, "VLLM_API_KEY");
2871
+ var LMSTUDIO_PROFILE = localProfile("lmstudio", "LM Studio", 1234, "LMSTUDIO_API_KEY");
2872
+ var LLAMACPP_PROFILE = localProfile("llamacpp", "llama.cpp", 8080, "LLAMACPP_API_KEY");
2873
+ var SGLANG_PROFILE = localProfile("sglang", "SGLang", 3e4, "SGLANG_API_KEY");
2874
+ var LOCAL_PROFILES = [
2875
+ OLLAMA_PROFILE,
2876
+ VLLM_PROFILE,
2877
+ LMSTUDIO_PROFILE,
2878
+ LLAMACPP_PROFILE,
2879
+ SGLANG_PROFILE
2880
+ ];
2881
+
2882
+ // src/providers/profiles/index.ts
2883
+ var BUILTIN_PROFILES = [
2884
+ OPENROUTER_PROFILE,
2885
+ AZURE_PROFILE,
2886
+ XAI_PROFILE,
2887
+ DEEPSEEK_PROFILE,
2888
+ MISTRAL_PROFILE,
2889
+ GROQ_PROFILE,
2890
+ BEDROCK_PROFILE,
2891
+ ...LOCAL_PROFILES
2892
+ ];
2893
+
2044
2894
  // src/providers/registry.ts
2045
2895
  var builtinFactories = {
2046
2896
  openai: (config) => {
@@ -2080,6 +2930,23 @@ var builtinFactories = {
2080
2930
  });
2081
2931
  }
2082
2932
  };
2933
+ function presetFactory(profile) {
2934
+ return (config) => {
2935
+ const opts = config.providers?.[profile.name] ?? {};
2936
+ return new OpenAICompatibleProvider({
2937
+ profile,
2938
+ apiKey: opts.apiKey,
2939
+ baseUrl: opts.baseUrl,
2940
+ authHeader: opts.authHeader,
2941
+ rateLimit: opts.rateLimit
2942
+ });
2943
+ };
2944
+ }
2945
+ for (const profile of BUILTIN_PROFILES) {
2946
+ if (!(profile.name in builtinFactories)) {
2947
+ builtinFactories[profile.name] = presetFactory(profile);
2948
+ }
2949
+ }
2083
2950
  var ProviderRegistry = class {
2084
2951
  /** Cached provider instances, keyed by provider name */
2085
2952
  instances = /* @__PURE__ */ new Map();
@@ -2144,175 +3011,58 @@ var ProviderRegistry = class {
2144
3011
  }
2145
3012
  throw new Error(`Unknown provider "${name}". Registered providers: ${this.list().join(", ")}`);
2146
3013
  }
2147
- /**
2148
- * Resolve a "provider:model" URI string into a Provider instance and model name.
2149
- *
2150
- * Supported formats:
2151
- * - "openai:gpt-4o" -> provider=openai, model=gpt-4o
2152
- * - "anthropic:claude-3" -> provider=anthropic, model=claude-3
2153
- * - "gpt-4o" -> uses defaultProvider from config, model=gpt-4o
2154
- * - undefined / empty -> uses defaultProvider and defaultModel from config
2155
- *
2156
- * @param uri Provider:model string, or just a model name
2157
- * @param config Axl configuration for provider options and defaults
2158
- */
2159
- resolve(uri, config = {}) {
2160
- if (!uri) {
2161
- const providerName2 = config.defaultProvider ?? "openai";
2162
- const model2 = config.defaultModel ?? "gpt-4o";
2163
- return { provider: this.get(providerName2, config), model: model2 };
2164
- }
2165
- const colonIndex = uri.indexOf(":");
2166
- if (colonIndex === -1) {
2167
- const providerName2 = config.defaultProvider ?? "openai";
2168
- return { provider: this.get(providerName2, config), model: uri };
2169
- }
2170
- const providerName = uri.slice(0, colonIndex);
2171
- const model = uri.slice(colonIndex + 1);
2172
- if (!providerName || !model) {
2173
- throw new Error(
2174
- `Invalid provider URI "${uri}". Expected format: "provider:model" (e.g. "openai:gpt-4o")`
2175
- );
2176
- }
2177
- return { provider: this.get(providerName, config), model };
2178
- }
2179
- /**
2180
- * Clear all cached provider instances. Useful for testing or reconfiguration.
2181
- */
2182
- clearCache() {
2183
- this.instances.clear();
2184
- }
2185
- /**
2186
- * Clear all registered factories (including built-ins).
2187
- * Useful for test runtimes where only explicitly registered instances should be used.
2188
- */
2189
- clearFactories() {
2190
- this.factories.clear();
2191
- }
2192
- };
2193
- var defaultRegistry = new ProviderRegistry();
2194
-
2195
- // src/context.ts
2196
- import { AsyncLocalStorage } from "async_hooks";
2197
- import { randomUUID } from "crypto";
2198
- import { z, ZodError } from "zod";
2199
-
2200
- // src/errors.ts
2201
- var AxlError = class extends Error {
2202
- code;
2203
- constructor(code, message) {
2204
- super(message);
2205
- this.name = "AxlError";
2206
- this.code = code;
2207
- }
2208
- };
2209
- var VerifyError = class extends AxlError {
2210
- lastOutput;
2211
- zodError;
2212
- retries;
2213
- constructor(lastOutput, zodError, retries) {
2214
- super("VERIFY_ERROR", `Schema validation failed after ${retries} retries: ${zodError.message}`);
2215
- this.name = "VerifyError";
2216
- this.lastOutput = lastOutput;
2217
- this.zodError = zodError;
2218
- this.retries = retries;
2219
- }
2220
- };
2221
- var QuorumNotMet = class extends AxlError {
2222
- results;
2223
- constructor(required, actual, results) {
2224
- super("QUORUM_NOT_MET", `Quorum not met: needed ${required} successes, got ${actual}`);
2225
- this.name = "QuorumNotMet";
2226
- this.results = results;
2227
- }
2228
- };
2229
- var NoConsensus = class extends AxlError {
2230
- constructor(reason) {
2231
- super("NO_CONSENSUS", `No consensus: ${reason}`);
2232
- this.name = "NoConsensus";
2233
- }
2234
- };
2235
- var TimeoutError = class extends AxlError {
2236
- constructor(operation, timeoutMs) {
2237
- super("TIMEOUT", `${operation} exceeded timeout of ${timeoutMs}ms`);
2238
- this.name = "TimeoutError";
2239
- }
2240
- };
2241
- function formatBudgetCost(cost) {
2242
- if (Number.isNaN(cost)) return "$NaN";
2243
- if (cost === Infinity) return "$Infinity";
2244
- if (cost === -Infinity) return "-$Infinity";
2245
- if (cost === 0) return "$0.00";
2246
- const sign = cost < 0 ? "-" : "";
2247
- const abs = Math.abs(cost);
2248
- if (abs < 1e-6) return `${sign}< $0.000001`;
2249
- if (abs < 1e-4) {
2250
- const [mantissa, exponent] = abs.toExponential(2).split("e");
2251
- return `${sign}$${mantissa}e${parseInt(exponent, 10)}`;
2252
- }
2253
- if (abs < 0.01) return `${sign}$${abs.toFixed(6)}`;
2254
- return `${sign}$${abs.toFixed(2)}`;
2255
- }
2256
- var BudgetExceededError = class extends AxlError {
2257
- limit;
2258
- spent;
2259
- policy;
2260
- constructor(limit, spent, policy) {
2261
- super(
2262
- "BUDGET_EXCEEDED",
2263
- `Budget exceeded: spent ${formatBudgetCost(spent)} of ${formatBudgetCost(
2264
- limit
2265
- )} limit (policy: ${policy})`
2266
- );
2267
- this.name = "BudgetExceededError";
2268
- this.limit = limit;
2269
- this.spent = spent;
2270
- this.policy = policy;
2271
- }
2272
- };
2273
- var MaxTurnsError = class extends AxlError {
2274
- maxTurns;
2275
- constructor(operation, maxTurns) {
2276
- super("MAX_TURNS", `${operation} exceeded maximum of ${maxTurns} turns`);
2277
- this.name = "MaxTurnsError";
2278
- this.maxTurns = maxTurns;
2279
- }
2280
- };
2281
- var GuardrailError = class extends AxlError {
2282
- guardrailType;
2283
- reason;
2284
- constructor(guardrailType, reason) {
2285
- super("GUARDRAIL_BLOCKED", `${guardrailType} guardrail blocked: ${reason}`);
2286
- this.name = "GuardrailError";
2287
- this.guardrailType = guardrailType;
2288
- this.reason = reason;
3014
+ /**
3015
+ * Resolve a "provider:model" URI string into a Provider instance and model name.
3016
+ *
3017
+ * Supported formats:
3018
+ * - "openai:gpt-4o" -> provider=openai, model=gpt-4o
3019
+ * - "anthropic:claude-3" -> provider=anthropic, model=claude-3
3020
+ * - "gpt-4o" -> uses defaultProvider from config, model=gpt-4o
3021
+ * - undefined / empty -> uses defaultProvider and defaultModel from config
3022
+ *
3023
+ * @param uri Provider:model string, or just a model name
3024
+ * @param config Axl configuration for provider options and defaults
3025
+ */
3026
+ resolve(uri, config = {}) {
3027
+ if (!uri) {
3028
+ const providerName2 = config.defaultProvider ?? "openai";
3029
+ const model2 = config.defaultModel ?? "gpt-4o";
3030
+ return { provider: this.get(providerName2, config), model: model2 };
3031
+ }
3032
+ const colonIndex = uri.indexOf(":");
3033
+ if (colonIndex === -1) {
3034
+ const providerName2 = config.defaultProvider ?? "openai";
3035
+ return { provider: this.get(providerName2, config), model: uri };
3036
+ }
3037
+ const providerName = uri.slice(0, colonIndex);
3038
+ const model = uri.slice(colonIndex + 1);
3039
+ if (!providerName || !model) {
3040
+ throw new Error(
3041
+ `Invalid provider URI "${uri}". Expected format: "provider:model" (e.g. "openai:gpt-4o")`
3042
+ );
3043
+ }
3044
+ return { provider: this.get(providerName, config), model };
2289
3045
  }
2290
- };
2291
- var ValidationError = class extends AxlError {
2292
- lastOutput;
2293
- reason;
2294
- retries;
2295
- constructor(lastOutput, reason, retries) {
2296
- super("VALIDATION_ERROR", `Validation failed after ${retries} retries: ${reason}`);
2297
- this.name = "ValidationError";
2298
- this.lastOutput = lastOutput;
2299
- this.reason = reason;
2300
- this.retries = retries;
3046
+ /**
3047
+ * Clear all cached provider instances. Useful for testing or reconfiguration.
3048
+ */
3049
+ clearCache() {
3050
+ this.instances.clear();
2301
3051
  }
2302
- };
2303
- var ToolDenied = class extends AxlError {
2304
- toolName;
2305
- agentName;
2306
- constructor(toolName, agentName) {
2307
- super(
2308
- "TOOL_DENIED",
2309
- `Agent "${agentName}" attempted to call tool "${toolName}" which is not in its ACL`
2310
- );
2311
- this.name = "ToolDenied";
2312
- this.toolName = toolName;
2313
- this.agentName = agentName;
3052
+ /**
3053
+ * Clear all registered factories (including built-ins).
3054
+ * Useful for test runtimes where only explicitly registered instances should be used.
3055
+ */
3056
+ clearFactories() {
3057
+ this.factories.clear();
2314
3058
  }
2315
3059
  };
3060
+ var defaultRegistry = new ProviderRegistry();
3061
+
3062
+ // src/context.ts
3063
+ import { AsyncLocalStorage } from "async_hooks";
3064
+ import { randomUUID } from "crypto";
3065
+ import { z, ZodError } from "zod";
2316
3066
 
2317
3067
  // src/partial-json.ts
2318
3068
  var MAX_DEPTH = 256;
@@ -2888,6 +3638,14 @@ var COST_BEARING_LEAF_TYPES = [
2888
3638
  "memory_recall"
2889
3639
  ];
2890
3640
  var COST_LEAF_SET = new Set(COST_BEARING_LEAF_TYPES);
3641
+ function hasPositiveTokens(event) {
3642
+ const t = event.tokens;
3643
+ if (!t) return false;
3644
+ return (t.input ?? 0) + (t.output ?? 0) + (t.reasoning ?? 0) > 0;
3645
+ }
3646
+ function isUsableCost(cost) {
3647
+ return typeof cost === "number" && Number.isFinite(cost) && cost >= 0;
3648
+ }
2891
3649
  function eventCostContribution(event) {
2892
3650
  if (event.type === "ask_end") return 0;
2893
3651
  const c = event.cost;
@@ -2896,6 +3654,9 @@ function eventCostContribution(event) {
2896
3654
  function isCostBearingLeaf(event) {
2897
3655
  return COST_LEAF_SET.has(event.type);
2898
3656
  }
3657
+ function isUnpricedLeaf(event) {
3658
+ return event.type !== void 0 && COST_LEAF_SET.has(event.type) && !isUsableCost(event.cost) && hasPositiveTokens(event);
3659
+ }
2899
3660
  function isRootLevel(event) {
2900
3661
  const d = event.depth;
2901
3662
  return (d ?? 0) === 0;
@@ -2944,6 +3705,10 @@ var AXL_EVENT_TYPES = [
2944
3705
  "string_delta",
2945
3706
  // Verification
2946
3707
  "verify",
3708
+ // Schema capability diagnostics (spec 22, Problem E) — surface the silent
3709
+ // structured-output cliffs (oversized appended schema, dropped refinements,
3710
+ // streaming disabled, zero-guidance `schemaPrompt:'none'`).
3711
+ "schema_diagnostic",
2947
3712
  // Observability
2948
3713
  "log",
2949
3714
  "memory_remember",
@@ -2986,6 +3751,7 @@ var LIFECYCLE_TYPES = /* @__PURE__ */ new Set([
2986
3751
  "delegate",
2987
3752
  "pipeline",
2988
3753
  "verify",
3754
+ "schema_diagnostic",
2989
3755
  "workflow_start",
2990
3756
  "workflow_end",
2991
3757
  "checkpoint_save",
@@ -3790,6 +4556,11 @@ var REDACTION_RULES = {
3790
4556
  // Scrub only the delta — preserving path keeps "which fields stream" telemetry.
3791
4557
  string_delta: (e) => ({ ...e, data: { ...e.data, delta: REDACTED } }),
3792
4558
  verify: (e) => e.data.lastError !== void 0 ? { ...e, data: { ...e.data, lastError: REDACTED } } : e,
4559
+ // `schema_diagnostic` carries only structural schema metadata — token counts,
4560
+ // thresholds, JSON-Schema field paths, a root type name, a tool name — none of
4561
+ // which is user content or PII (field paths are schema-shape, preserved like
4562
+ // `string_delta.path`). Passthrough, as an explicit reviewed decision.
4563
+ schema_diagnostic: passthrough,
3793
4564
  log: (e) => {
3794
4565
  if (!e.data || typeof e.data !== "object" || Array.isArray(e.data)) return e;
3795
4566
  return { ...e, data: walkObjectOneLevel(e.data, LOG_PRESERVE_KEYS) };
@@ -3822,6 +4593,80 @@ function redactEvent(event) {
3822
4593
  return rule(event);
3823
4594
  }
3824
4595
 
4596
+ // src/schema-diagnostics.ts
4597
+ var DEFAULT_SCHEMA_OVERSIZED_TOKENS = 4e3;
4598
+ var refinementCache = /* @__PURE__ */ new WeakMap();
4599
+ function detectDroppedRefinements(schema) {
4600
+ const cached = refinementCache.get(schema);
4601
+ if (cached) return cached;
4602
+ const paths = /* @__PURE__ */ new Set();
4603
+ let count = 0;
4604
+ const visited = /* @__PURE__ */ new WeakSet();
4605
+ const walk = (node, path) => {
4606
+ if (node === null || typeof node !== "object") return;
4607
+ if (visited.has(node)) return;
4608
+ visited.add(node);
4609
+ const def = node._zod?.def;
4610
+ if (!def) return;
4611
+ const here = () => paths.add(path === "" ? "<root>" : path);
4612
+ const checks = def.checks;
4613
+ if (Array.isArray(checks)) {
4614
+ for (const check of checks) {
4615
+ const kind = check?._zod?.def?.check;
4616
+ if (kind === "custom") {
4617
+ count += 1;
4618
+ here();
4619
+ }
4620
+ }
4621
+ }
4622
+ if (def.check === "custom") {
4623
+ count += 1;
4624
+ here();
4625
+ }
4626
+ const join2 = (child) => path === "" ? child : `${path}.${child}`;
4627
+ const shape = def.shape;
4628
+ if (shape && typeof shape === "object") {
4629
+ for (const [key, child] of Object.entries(shape)) walk(child, join2(key));
4630
+ }
4631
+ if ("element" in def) walk(def.element, `${path}[]`);
4632
+ if (Array.isArray(def.options)) {
4633
+ for (const opt of def.options) walk(opt, path);
4634
+ }
4635
+ if ("innerType" in def) walk(def.innerType, path);
4636
+ if (Array.isArray(def.items)) {
4637
+ def.items.forEach((item, i) => walk(item, `${path}[${i}]`));
4638
+ }
4639
+ if ("rest" in def && def.rest) walk(def.rest, `${path}[]`);
4640
+ if ("catchall" in def && def.catchall) walk(def.catchall, `${path}[*]`);
4641
+ if ("keyType" in def) walk(def.keyType, `${path}[key]`);
4642
+ if ("valueType" in def) walk(def.valueType, `${path}[*]`);
4643
+ if ("in" in def) walk(def.in, path);
4644
+ if ("out" in def) walk(def.out, path);
4645
+ if ("left" in def) walk(def.left, path);
4646
+ if ("right" in def) walk(def.right, path);
4647
+ if (typeof def.getter === "function") {
4648
+ try {
4649
+ walk(def.getter(), path);
4650
+ } catch {
4651
+ }
4652
+ }
4653
+ };
4654
+ walk(schema, "");
4655
+ const result = { count, paths: [...paths] };
4656
+ refinementCache.set(schema, result);
4657
+ return result;
4658
+ }
4659
+ var DOCS_URL = "docs/observability.md#schema-diagnostics";
4660
+ var MAX_WARNED_KEYS = 2048;
4661
+ var warnedDiagnosticKeys = /* @__PURE__ */ new Set();
4662
+ function warnSchemaDiagnosticOnce(key, message, silent) {
4663
+ if (silent || process.env.AXL_DIAGNOSTICS_SILENT === "true") return;
4664
+ if (warnedDiagnosticKeys.has(key)) return;
4665
+ if (warnedDiagnosticKeys.size >= MAX_WARNED_KEYS) warnedDiagnosticKeys.clear();
4666
+ warnedDiagnosticKeys.add(key);
4667
+ console.warn(`[axl] ${message} See ${DOCS_URL}.`);
4668
+ }
4669
+
3825
4670
  // src/config.ts
3826
4671
  function defineConfig(config) {
3827
4672
  return config;
@@ -3897,14 +4742,233 @@ function resolveConfig(config) {
3897
4742
  return resolved;
3898
4743
  }
3899
4744
 
4745
+ // src/tool-model-output.ts
4746
+ import { types as nodeTypes } from "util";
4747
+ function invalid(path, reason) {
4748
+ return new Error(`Invalid tool model output at ${path}: ${reason}`);
4749
+ }
4750
+ function propertyPath(parent, key) {
4751
+ if (/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key)) return `${parent}.${key}`;
4752
+ let escaped = "";
4753
+ for (const character of key) {
4754
+ const code = character.charCodeAt(0);
4755
+ if (character === "\\" || character === '"') escaped += `\\${character}`;
4756
+ else if (code <= 31 || code === 8232 || code === 8233) {
4757
+ escaped += `\\u${code.toString(16).padStart(4, "0")}`;
4758
+ } else escaped += character;
4759
+ }
4760
+ return `${parent}["${escaped}"]`;
4761
+ }
4762
+ function normalize(value, path, stack) {
4763
+ if (value === null || typeof value === "string" || typeof value === "boolean") return value;
4764
+ if (typeof value === "number") {
4765
+ if (!Number.isFinite(value)) throw invalid(path, "number must be finite");
4766
+ return value;
4767
+ }
4768
+ if (value === void 0) throw invalid(path, "undefined is only allowed on object properties");
4769
+ if (typeof value === "bigint" || typeof value === "function" || typeof value === "symbol") {
4770
+ throw invalid(path, `unsupported ${typeof value}`);
4771
+ }
4772
+ if (typeof value !== "object") throw invalid(path, `unsupported ${typeof value}`);
4773
+ let prototype;
4774
+ let descriptors;
4775
+ try {
4776
+ prototype = Object.getPrototypeOf(value);
4777
+ descriptors = Object.getOwnPropertyDescriptors(value);
4778
+ Object.setPrototypeOf(descriptors, null);
4779
+ } catch (cause) {
4780
+ throw invalid(
4781
+ path,
4782
+ `object inspection failed (${cause instanceof Error ? cause.name : "error"})`
4783
+ );
4784
+ }
4785
+ if (stack.has(value)) throw invalid(path, "cyclic reference");
4786
+ for (const key of Reflect.ownKeys(descriptors)) {
4787
+ const descriptor = descriptors[key];
4788
+ if (descriptor && !("value" in descriptor)) {
4789
+ throw invalid(
4790
+ typeof key === "string" ? propertyPath(path, key) : `${path}[symbol]`,
4791
+ "accessor properties are not supported"
4792
+ );
4793
+ }
4794
+ if (typeof key === "symbol" && descriptor?.enumerable) {
4795
+ throw invalid(`${path}[symbol]`, "enumerable symbol keys are not supported");
4796
+ }
4797
+ }
4798
+ if (typeof descriptors.toJSON?.value === "function") {
4799
+ throw invalid(propertyPath(path, "toJSON"), "custom toJSON is not supported");
4800
+ }
4801
+ const array = Array.isArray(value);
4802
+ if (array && prototype !== Array.prototype && prototype !== null) {
4803
+ throw invalid(path, "array subclasses are not supported");
4804
+ }
4805
+ const inheritedToJSON = prototype === Array.prototype || prototype === Object.prototype ? Object.getOwnPropertyDescriptor(prototype, "toJSON") : void 0;
4806
+ if (typeof inheritedToJSON?.value === "function") {
4807
+ throw invalid(propertyPath(path, "toJSON"), "inherited custom toJSON is not supported");
4808
+ }
4809
+ const inheritedThen = prototype === Array.prototype || prototype === Object.prototype ? Object.getOwnPropertyDescriptor(prototype, "then") : void 0;
4810
+ if (typeof descriptors.then?.value === "function" || inheritedThen !== void 0 && (!("value" in inheritedThen) || typeof inheritedThen.value === "function")) {
4811
+ throw invalid(path, "thenable output is not supported");
4812
+ }
4813
+ stack.add(value);
4814
+ try {
4815
+ if (array) {
4816
+ const normalized2 = [];
4817
+ const length = descriptors.length?.value;
4818
+ if (!Number.isSafeInteger(length) || length < 0) {
4819
+ throw invalid(propertyPath(path, "length"), "array length is invalid");
4820
+ }
4821
+ for (let index = 0; index < length; index++) {
4822
+ const descriptor = descriptors[String(index)];
4823
+ if (!descriptor || !("value" in descriptor)) {
4824
+ throw invalid(`${path}[${index}]`, "arrays must be dense data properties");
4825
+ }
4826
+ if (descriptor.value === void 0) {
4827
+ throw invalid(`${path}[${index}]`, "array values cannot be undefined");
4828
+ }
4829
+ normalized2.push(normalize(descriptor.value, `${path}[${index}]`, stack));
4830
+ }
4831
+ for (const key of Object.keys(descriptors)) {
4832
+ const descriptor = descriptors[key];
4833
+ if (!descriptor?.enumerable) continue;
4834
+ const index = Number(key);
4835
+ if (!Number.isInteger(index) || index < 0 || index >= length || String(index) !== key) {
4836
+ throw invalid(propertyPath(path, key), "non-index array properties are not supported");
4837
+ }
4838
+ }
4839
+ Object.setPrototypeOf(normalized2, null);
4840
+ return normalized2;
4841
+ }
4842
+ if (prototype !== Object.prototype && prototype !== null) {
4843
+ throw invalid(path, "only plain objects and null-prototype records are supported");
4844
+ }
4845
+ const normalized = /* @__PURE__ */ Object.create(null);
4846
+ for (const key of Object.keys(descriptors)) {
4847
+ const descriptor = descriptors[key];
4848
+ if (!descriptor?.enumerable) continue;
4849
+ if (!("value" in descriptor)) {
4850
+ throw invalid(propertyPath(path, key), "accessor properties are not supported");
4851
+ }
4852
+ if (descriptor.value === void 0) continue;
4853
+ normalized[key] = normalize(descriptor.value, propertyPath(path, key), stack);
4854
+ }
4855
+ return normalized;
4856
+ } finally {
4857
+ stack.delete(value);
4858
+ }
4859
+ }
4860
+ function serializeToolModelOutput(toolName, output) {
4861
+ try {
4862
+ if (nodeTypes.isPromise(output)) {
4863
+ void Promise.prototype.then.call(output, void 0, () => void 0);
4864
+ }
4865
+ if (typeof output === "string") return output;
4866
+ return JSON.stringify(normalize(output, "$", /* @__PURE__ */ new WeakSet()));
4867
+ } catch (cause) {
4868
+ throw new ToolModelOutputError(toolName, cause);
4869
+ }
4870
+ }
4871
+
3900
4872
  // src/context.ts
3901
4873
  var signalStorage = new AsyncLocalStorage();
4874
+ function isErrorShaped(value) {
4875
+ return value !== null && typeof value === "object" && "error" in value;
4876
+ }
4877
+ function isErrorShapedSafely(value) {
4878
+ try {
4879
+ return isErrorShaped(value);
4880
+ } catch {
4881
+ return false;
4882
+ }
4883
+ }
4884
+ function recordToolSpanResult(span, value, suppressInspectionErrors) {
4885
+ let isError = false;
4886
+ let errorMessage;
4887
+ const inspect = () => {
4888
+ if (isErrorShaped(value)) {
4889
+ isError = true;
4890
+ errorMessage = value.error;
4891
+ } else {
4892
+ isError = false;
4893
+ }
4894
+ };
4895
+ if (suppressInspectionErrors) {
4896
+ try {
4897
+ inspect();
4898
+ } catch {
4899
+ isError = false;
4900
+ errorMessage = void 0;
4901
+ }
4902
+ } else {
4903
+ inspect();
4904
+ }
4905
+ span.setAttribute("axl.tool.success", !isError);
4906
+ if (isError) span.setStatus("error", errorMessage);
4907
+ }
4908
+ function prepareToolModelOutput(toolName, project) {
4909
+ let output;
4910
+ try {
4911
+ output = project();
4912
+ } catch (cause) {
4913
+ throw new ToolModelOutputError(toolName, cause);
4914
+ }
4915
+ return serializeToolModelOutput(toolName, output);
4916
+ }
4917
+ function finalizeToolResult(options) {
4918
+ const { toolName, configuredTool, outcome, legacyContent, emitEnd, beforeProjection } = options;
4919
+ const projectionEligible = configuredTool !== void 0 && !configuredTool.sensitive && outcome.kind === "returned" && configuredTool.toModelOutput !== void 0;
4920
+ if (projectionEligible) {
4921
+ emitEnd();
4922
+ beforeProjection();
4923
+ const project = configuredTool.toModelOutput;
4924
+ return prepareToolModelOutput(toolName, () => project(outcome.value));
4925
+ }
4926
+ const content = legacyContent();
4927
+ emitEnd();
4928
+ return content;
4929
+ }
3902
4930
  var askStorage = new AsyncLocalStorage();
4931
+ var inlineSchemaCache = /* @__PURE__ */ new WeakMap();
3903
4932
  function zodToJsonSchema(schema) {
4933
+ const cached = inlineSchemaCache.get(schema);
4934
+ if (cached !== void 0) return cached;
3904
4935
  const result = z.toJSONSchema(schema, { unrepresentable: "any" });
3905
4936
  delete result.$schema;
4937
+ Object.freeze(result);
4938
+ inlineSchemaCache.set(schema, result);
3906
4939
  return result;
3907
4940
  }
4941
+ var promptSchemaCache = /* @__PURE__ */ new WeakMap();
4942
+ var inlineSchemaTokenCache = /* @__PURE__ */ new WeakMap();
4943
+ function estimateInlineSchemaTokens(schema) {
4944
+ const cached = inlineSchemaTokenCache.get(schema);
4945
+ if (cached !== void 0) return cached;
4946
+ const tokens = estimateTokens(JSON.stringify(zodToJsonSchema(schema)));
4947
+ inlineSchemaTokenCache.set(schema, tokens);
4948
+ return tokens;
4949
+ }
4950
+ var nativeSchemaCache = /* @__PURE__ */ new WeakMap();
4951
+ function deriveNativeSchema(schema) {
4952
+ const cached = nativeSchemaCache.get(schema);
4953
+ if (cached !== void 0) return cached;
4954
+ const json = z.toJSONSchema(schema, { unrepresentable: "any", io: "input" });
4955
+ delete json.$schema;
4956
+ nativeSchemaCache.set(schema, json);
4957
+ return json;
4958
+ }
4959
+ function renderSchemaForPrompt(schema) {
4960
+ const cached = promptSchemaCache.get(schema);
4961
+ if (cached !== void 0) return cached;
4962
+ const json = z.toJSONSchema(schema, {
4963
+ unrepresentable: "any",
4964
+ io: "input",
4965
+ reused: "ref"
4966
+ });
4967
+ delete json.$schema;
4968
+ const rendered = JSON.stringify(json);
4969
+ promptSchemaCache.set(schema, rendered);
4970
+ return rendered;
4971
+ }
3908
4972
  function estimateTokens(text) {
3909
4973
  return Math.ceil(text.length / 4);
3910
4974
  }
@@ -4285,12 +5349,14 @@ var WorkflowContext = class _WorkflowContext {
4285
5349
  depth,
4286
5350
  agent: agent2._name,
4287
5351
  stepRef,
4288
- askCost: { value: 0 }
5352
+ askCost: { value: 0 },
5353
+ askUnpriced: false
4289
5354
  };
4290
5355
  return askStorage.run(frame, async () => {
4291
5356
  const askStart = Date.now();
4292
5357
  this.emitEvent({ type: "ask_start", prompt });
4293
5358
  const costBefore = this.budgetContext?.totalCost ?? 0;
5359
+ const unpricedCountBefore = this.budgetContext?.unpricedCount ?? 0;
4294
5360
  const resolveCtx = options?.metadata ? { metadata: { ...this.metadata, ...options.metadata } } : { metadata: this.metadata };
4295
5361
  const usageCapture = {};
4296
5362
  const doCall = async () => {
@@ -4336,6 +5402,7 @@ var WorkflowContext = class _WorkflowContext {
4336
5402
  ) : await doCall();
4337
5403
  outcome = { ok: true, result };
4338
5404
  const costAfter = this.budgetContext?.totalCost ?? 0;
5405
+ const unpricedCountAfter = this.budgetContext?.unpricedCount ?? 0;
4339
5406
  if (this.onAgentCallComplete) {
4340
5407
  try {
4341
5408
  this.onAgentCallComplete({
@@ -4344,6 +5411,7 @@ var WorkflowContext = class _WorkflowContext {
4344
5411
  response: typeof result === "string" ? result : JSON.stringify(result),
4345
5412
  model: agent2.resolveModel(resolveCtx),
4346
5413
  cost: costAfter - costBefore,
5414
+ unpriced: frame.askUnpriced || unpricedCountAfter > unpricedCountBefore,
4347
5415
  duration: Date.now() - askStart,
4348
5416
  promptVersion: agent2._config.version,
4349
5417
  temperature: options?.temperature ?? agent2._config.temperature,
@@ -4378,6 +5446,7 @@ var WorkflowContext = class _WorkflowContext {
4378
5446
  error: "ask_end emitted without outcome \u2014 internal bug"
4379
5447
  },
4380
5448
  cost: frame.askCost.value,
5449
+ ...frame.askUnpriced ? { unpriced: true } : {},
4381
5450
  duration: Date.now() - askStart
4382
5451
  });
4383
5452
  } catch (emitErr) {
@@ -4461,15 +5530,32 @@ var WorkflowContext = class _WorkflowContext {
4461
5530
  messages.push(msg);
4462
5531
  }
4463
5532
  }
5533
+ const schemaPromptMode = options?.schemaPrompt ?? agent2._config.schemaPrompt ?? "json-schema";
4464
5534
  let userContent = prompt;
5535
+ let appendedSchemaText;
4465
5536
  if (options?.schema) {
4466
- const jsonSchema = zodToJsonSchema(options.schema);
4467
- userContent += `
5537
+ const schema = options.schema;
5538
+ if (schemaPromptMode === "none") {
5539
+ } else if (schemaPromptMode === "json-schema") {
5540
+ appendedSchemaText = renderSchemaForPrompt(schema);
5541
+ userContent += `
4468
5542
 
4469
5543
  Respond with valid JSON matching this schema:
4470
- ${JSON.stringify(jsonSchema, null, 2)}`;
5544
+ ${appendedSchemaText}`;
5545
+ } else {
5546
+ appendedSchemaText = typeof schemaPromptMode.render === "function" ? schemaPromptMode.render(schema) : schemaPromptMode.render;
5547
+ userContent += `
5548
+
5549
+ ${appendedSchemaText}`;
5550
+ }
4471
5551
  }
4472
5552
  messages.push({ role: "user", content: userContent });
5553
+ this.emitSchemaDiagnostics(agent2, options, toolDefs, {
5554
+ mode: schemaPromptMode,
5555
+ appendedSchemaText,
5556
+ provider,
5557
+ model
5558
+ });
4473
5559
  if (handoffMessages && handoffMessages.length > 0) {
4474
5560
  const handoffContext = handoffMessages.filter(
4475
5561
  (m) => m.role === "assistant" || m.role === "tool"
@@ -4535,6 +5621,7 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
4535
5621
  let lastStartAttempt = 1;
4536
5622
  let lastStartMaxAttempts = 1;
4537
5623
  while (turns < maxTurns) {
5624
+ this.currentSignal?.throwIfAborted();
4538
5625
  if (Date.now() - startTime > timeoutMs) {
4539
5626
  throw new TimeoutError("ctx.ask()", timeoutMs);
4540
5627
  }
@@ -4582,7 +5669,17 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
4582
5669
  signal: this.currentSignal
4583
5670
  };
4584
5671
  if (options?.schema && toolDefs.length === 0) {
4585
- chatOptions.responseFormat = { type: "json_object" };
5672
+ const nativeOut = options?.nativeStructuredOutput ?? agent2._config.nativeStructuredOutput ?? false;
5673
+ chatOptions.responseFormat = nativeOut ? {
5674
+ type: "json_schema",
5675
+ json_schema: {
5676
+ name: "response",
5677
+ // Input-side rendering — see `deriveNativeSchema`. Keeps the
5678
+ // native schema consistent with the prompt and non-empty for
5679
+ // `.transform()` schemas.
5680
+ schema: deriveNativeSchema(options.schema)
5681
+ }
5682
+ } : { type: "json_object" };
4586
5683
  }
4587
5684
  const callbackMeta = this.currentCallbackMeta(agent2._name);
4588
5685
  const callParams = {
@@ -4626,6 +5723,7 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
4626
5723
  this.onAgentStart?.({ agent: agent2._name, model: modelUri }, callbackMeta);
4627
5724
  let response;
4628
5725
  try {
5726
+ this.currentSignal?.throwIfAborted();
4629
5727
  if (this._streamingEnabled) {
4630
5728
  let content2 = "";
4631
5729
  const toolCalls = [];
@@ -4700,10 +5798,7 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
4700
5798
  }
4701
5799
  });
4702
5800
  }
4703
- response ??= {
4704
- content: content2,
4705
- usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 }
4706
- };
5801
+ response ??= { content: content2 };
4707
5802
  if (toolCalls.length > 0) {
4708
5803
  response.tool_calls = toolCalls;
4709
5804
  }
@@ -4727,7 +5822,11 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
4727
5822
  response: "",
4728
5823
  turn: turns,
4729
5824
  ...retryReason ? { retryReason } : {},
4730
- error: err instanceof Error ? err.message : String(err)
5825
+ error: err instanceof Error ? err.message : String(err),
5826
+ // Enrich with typed-error metadata when available. `body` is
5827
+ // deliberately NOT emitted — it's redaction-eligible (see
5828
+ // docs/security.md). `data.error` already carries the message.
5829
+ ...err instanceof ProviderError ? { status: err.status, retryable: err.retryable } : {}
4731
5830
  }
4732
5831
  });
4733
5832
  throw err;
@@ -4791,10 +5890,13 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
4791
5890
  depth: handoffTargetDepth,
4792
5891
  agent: descriptor.agent._name,
4793
5892
  stepRef: sourceFrame?.stepRef ?? this.stepRefRoot,
4794
- askCost: { value: 0 }
5893
+ askCost: { value: 0 },
5894
+ askUnpriced: false
4795
5895
  };
4796
5896
  const handoffOptions = options ? {
4797
5897
  schema: options.schema,
5898
+ schemaPrompt: options.schemaPrompt,
5899
+ nativeStructuredOutput: options.nativeStructuredOutput,
4798
5900
  retries: options.retries,
4799
5901
  metadata: options.metadata,
4800
5902
  validate: options.validate,
@@ -4815,6 +5917,7 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
4815
5917
  type: "ask_end",
4816
5918
  outcome: { ok: true, result },
4817
5919
  cost: targetFrame.askCost.value,
5920
+ ...targetFrame.askUnpriced ? { unpriced: true } : {},
4818
5921
  duration: Date.now() - targetAskStart
4819
5922
  });
4820
5923
  return result;
@@ -4826,6 +5929,7 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
4826
5929
  error: err instanceof Error ? err.message : String(err)
4827
5930
  },
4828
5931
  cost: targetFrame.askCost.value,
5932
+ ...targetFrame.askUnpriced ? { unpriced: true } : {},
4829
5933
  duration: Date.now() - targetAskStart
4830
5934
  });
4831
5935
  throw err;
@@ -4921,6 +6025,8 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
4921
6025
  return await handoffFn();
4922
6026
  }
4923
6027
  }
6028
+ const tool2 = agent2._config.tools?.find((candidate) => candidate.name === toolName);
6029
+ const isMcpTool = !tool2 && this.mcpManager?.isMcpTool(toolName);
4924
6030
  const toolOverride = this.toolOverrides?.get(toolName);
4925
6031
  if (toolOverride) {
4926
6032
  let toolArgs2;
@@ -4945,38 +6051,47 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
4945
6051
  callbackMeta
4946
6052
  );
4947
6053
  const toolStart2 = Date.now();
6054
+ const suppressErrorShapeInspection2 = tool2?.sensitive === true || tool2?.toModelOutput !== void 0;
4948
6055
  const executeOverride = async () => {
4949
- let toolResult3;
4950
6056
  try {
4951
- toolResult3 = await toolOverride(toolArgs2);
6057
+ return { kind: "returned", value: await toolOverride(toolArgs2) };
4952
6058
  } catch (err) {
4953
- toolResult3 = { error: err instanceof Error ? err.message : String(err) };
6059
+ return {
6060
+ kind: "thrown",
6061
+ value: { error: err instanceof Error ? err.message : String(err) }
6062
+ };
4954
6063
  }
4955
- return toolResult3;
4956
6064
  };
4957
- const toolResult2 = this.spanManager ? await this.spanManager.withSpanAsync(
6065
+ const outcome = this.spanManager ? await this.spanManager.withSpanAsync(
4958
6066
  "axl.tool.call",
4959
6067
  {
4960
6068
  "axl.tool.name": toolName,
4961
6069
  "axl.agent.name": agent2._name
4962
6070
  },
4963
6071
  async (span) => {
4964
- const r = await executeOverride();
6072
+ const result = await executeOverride();
4965
6073
  span.setAttribute("axl.tool.duration", Date.now() - toolStart2);
4966
- const isError = r && typeof r === "object" && "error" in r;
4967
- span.setAttribute("axl.tool.success", !isError);
4968
- if (isError)
4969
- span.setStatus("error", r.error);
4970
- return r;
6074
+ recordToolSpanResult(span, result.value, suppressErrorShapeInspection2);
6075
+ return result;
4971
6076
  }
4972
6077
  ) : await executeOverride();
4973
- const resultContent2 = JSON.stringify(toolResult2);
4974
- this.emitEvent({
4975
- type: "tool_call_end",
4976
- agent: agent2._name,
4977
- tool: toolName,
4978
- duration: Date.now() - toolStart2,
4979
- data: { args: toolArgs2, result: toolResult2, callId: toolCall.id }
6078
+ const toolResult2 = outcome.value;
6079
+ const resultContent2 = finalizeToolResult({
6080
+ toolName,
6081
+ configuredTool: tool2,
6082
+ outcome,
6083
+ legacyContent: () => tool2?.sensitive ? "[REDACTED - sensitive tool output]" : JSON.stringify(toolResult2),
6084
+ emitEnd: () => {
6085
+ const toolDuration2 = Date.now() - toolStart2;
6086
+ this.emitEvent({
6087
+ type: "tool_call_end",
6088
+ agent: agent2._name,
6089
+ tool: toolName,
6090
+ duration: toolDuration2,
6091
+ data: { args: toolArgs2, result: toolResult2, callId: toolCall.id }
6092
+ });
6093
+ },
6094
+ beforeProjection: () => this.currentSignal?.throwIfAborted()
4980
6095
  });
4981
6096
  currentMessages.push({
4982
6097
  role: "tool",
@@ -4985,8 +6100,6 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
4985
6100
  });
4986
6101
  continue;
4987
6102
  }
4988
- const tool2 = agent2._config.tools?.find((t) => t.name === toolName);
4989
- const isMcpTool = !tool2 && this.mcpManager?.isMcpTool(toolName);
4990
6103
  if (!tool2 && !isMcpTool) {
4991
6104
  this.emitEvent({ type: "tool_denied", agent: agent2._name, tool: toolName });
4992
6105
  currentMessages.push({
@@ -5015,6 +6128,7 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
5015
6128
  });
5016
6129
  this.onToolCall?.({ name: toolName, args: toolArgs, callId: toolCall.id }, callbackMeta);
5017
6130
  const toolStart = Date.now();
6131
+ const suppressErrorShapeInspection = tool2?.sensitive === true || tool2?.toModelOutput !== void 0;
5018
6132
  if (tool2 && tool2.requireApproval) {
5019
6133
  const approvalFn = async () => {
5020
6134
  const decision = await this.awaitHuman({
@@ -5076,12 +6190,12 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
5076
6190
  }
5077
6191
  }
5078
6192
  const executeTool = async () => {
5079
- let toolResult2;
6193
+ let outcome;
5080
6194
  let resultContent2;
5081
6195
  if (isMcpTool && this.mcpManager) {
5082
6196
  try {
5083
6197
  const mcpResult = await this.mcpManager.callTool(toolName, toolArgs);
5084
- toolResult2 = mcpResult;
6198
+ outcome = { kind: "returned", value: mcpResult };
5085
6199
  resultContent2 = mcpResult.content.map(
5086
6200
  (c) => c.type === "text" ? c.text : `[${c.type}]`
5087
6201
  ).join("\n");
@@ -5089,34 +6203,50 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
5089
6203
  resultContent2 = `Error: ${resultContent2}`;
5090
6204
  }
5091
6205
  } catch (err) {
5092
- toolResult2 = { error: err instanceof Error ? err.message : String(err) };
5093
- resultContent2 = JSON.stringify(toolResult2);
6206
+ outcome = {
6207
+ kind: "thrown",
6208
+ value: { error: err instanceof Error ? err.message : String(err) }
6209
+ };
6210
+ resultContent2 = JSON.stringify(outcome.value);
5094
6211
  }
5095
6212
  } else if (tool2) {
5096
6213
  const childCtx = this.createChildContext();
5097
6214
  try {
5098
- toolResult2 = await tool2._execute(toolArgs, childCtx);
6215
+ outcome = { kind: "returned", value: await tool2._execute(toolArgs, childCtx) };
5099
6216
  } catch (err) {
5100
- toolResult2 = { error: err instanceof Error ? err.message : String(err) };
6217
+ outcome = {
6218
+ kind: "thrown",
6219
+ value: { error: err instanceof Error ? err.message : String(err) }
6220
+ };
5101
6221
  }
5102
- if (tool2.hooks?.after && !(toolResult2 && typeof toolResult2 === "object" && "error" in toolResult2)) {
6222
+ if (outcome.kind === "returned" && tool2.hooks?.after && !(suppressErrorShapeInspection ? isErrorShapedSafely(outcome.value) : isErrorShaped(outcome.value))) {
5103
6223
  try {
5104
- toolResult2 = await tool2.hooks.after(toolResult2, this);
6224
+ outcome = {
6225
+ kind: "returned",
6226
+ value: await tool2.hooks.after(outcome.value, this)
6227
+ };
5105
6228
  } catch (err) {
5106
- toolResult2 = {
5107
- error: `After hook error: ${err instanceof Error ? err.message : String(err)}`
6229
+ outcome = {
6230
+ kind: "thrown",
6231
+ value: {
6232
+ error: `After hook error: ${err instanceof Error ? err.message : String(err)}`
6233
+ }
5108
6234
  };
5109
6235
  }
5110
6236
  }
5111
- resultContent2 = tool2.sensitive ? "[REDACTED - sensitive tool output]" : JSON.stringify(toolResult2);
6237
+ if (tool2.sensitive) {
6238
+ resultContent2 = "[REDACTED - sensitive tool output]";
6239
+ } else if (outcome.kind === "thrown" || tool2.toModelOutput === void 0) {
6240
+ resultContent2 = JSON.stringify(outcome.value);
6241
+ }
5112
6242
  } else {
5113
- toolResult2 = void 0;
6243
+ outcome = { kind: "returned", value: void 0 };
5114
6244
  resultContent2 = "Tool execution error";
5115
6245
  }
5116
- return { toolResult: toolResult2, resultContent: resultContent2 };
6246
+ return { outcome, resultContent: resultContent2 };
5117
6247
  };
5118
6248
  const traceName = isMcpTool && this.mcpManager ? this.mcpManager.getQualifiedName(toolName) ?? toolName : toolName;
5119
- const { toolResult, resultContent } = this.spanManager ? await this.spanManager.withSpanAsync(
6249
+ const execution = this.spanManager ? await this.spanManager.withSpanAsync(
5120
6250
  "axl.tool.call",
5121
6251
  {
5122
6252
  "axl.tool.name": traceName,
@@ -5125,22 +6255,27 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
5125
6255
  async (span) => {
5126
6256
  const r = await executeTool();
5127
6257
  span.setAttribute("axl.tool.duration", Date.now() - toolStart);
5128
- const isError = r.toolResult && typeof r.toolResult === "object" && "error" in r.toolResult;
5129
- span.setAttribute("axl.tool.success", !isError);
5130
- if (isError)
5131
- span.setStatus(
5132
- "error",
5133
- r.toolResult.error
5134
- );
6258
+ recordToolSpanResult(span, r.outcome.value, suppressErrorShapeInspection);
5135
6259
  return r;
5136
6260
  }
5137
6261
  ) : await executeTool();
5138
- this.emitEvent({
5139
- type: "tool_call_end",
5140
- agent: agent2._name,
5141
- tool: traceName,
5142
- duration: Date.now() - toolStart,
5143
- data: { args: toolArgs, result: toolResult, callId: toolCall.id }
6262
+ const toolResult = execution.outcome.value;
6263
+ const toolDuration = Date.now() - toolStart;
6264
+ const resultContent = finalizeToolResult({
6265
+ toolName,
6266
+ configuredTool: tool2,
6267
+ outcome: execution.outcome,
6268
+ legacyContent: () => execution.resultContent,
6269
+ emitEnd: () => {
6270
+ this.emitEvent({
6271
+ type: "tool_call_end",
6272
+ agent: agent2._name,
6273
+ tool: traceName,
6274
+ duration: toolDuration,
6275
+ data: { args: toolArgs, result: toolResult, callId: toolCall.id }
6276
+ });
6277
+ },
6278
+ beforeProjection: () => this.currentSignal?.throwIfAborted()
5144
6279
  });
5145
6280
  currentMessages.push({
5146
6281
  role: "tool",
@@ -5364,6 +6499,148 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
5364
6499
  ...providerMetadata ? { providerMetadata } : {}
5365
6500
  });
5366
6501
  }
6502
+ /**
6503
+ * Emit `schema_diagnostic` events for the silent structured-output cliffs
6504
+ * (spec 22, Problems B + E). Called once per ask. Covers:
6505
+ * - the appended **prompt** schema (oversized; dropped refinements; or the
6506
+ * zero-guidance `schemaPrompt:'none'` footgun),
6507
+ * - each user **tool-def** schema (oversized, dropped refinements),
6508
+ * - the **streaming** gate (disabled by a non-object root or by tools),
6509
+ * - **native structured output** the resolved provider can't honor (R10).
6510
+ *
6511
+ * The structured event always fires (persisted + streamed). For the genuinely
6512
+ * surprising, low-frequency cliffs it ALSO fires a one-time deduped
6513
+ * `console.warn` so the median consumer — who never wires up `ctx.events` and
6514
+ * runs with the trace console off — still sees it. `oversized` is event-only
6515
+ * (O3: it's tunable/noisy); the `tools` streaming cause is event-only too
6516
+ * (expected behavior for any tool-using agent, not a surprise).
6517
+ */
6518
+ emitSchemaDiagnostics(agent2, options, toolDefs, ctx) {
6519
+ const silent = this.config.diagnostics?.silent;
6520
+ const threshold = this.config.diagnostics?.schemaOversizedTokens ?? DEFAULT_SCHEMA_OVERSIZED_TOKENS;
6521
+ const schema = options?.schema;
6522
+ if (schema) {
6523
+ if (ctx.mode === "none") {
6524
+ this.emitEvent({
6525
+ type: "schema_diagnostic",
6526
+ agent: agent2._name,
6527
+ data: { kind: "schema_prompt_none_no_guidance" }
6528
+ });
6529
+ warnSchemaDiagnosticOnce(
6530
+ `${agent2._name}\0schema_prompt_none`,
6531
+ `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'.`,
6532
+ silent
6533
+ );
6534
+ } else {
6535
+ const promptTokens = estimateTokens(ctx.appendedSchemaText ?? "");
6536
+ if (promptTokens > threshold) {
6537
+ this.emitEvent({
6538
+ type: "schema_diagnostic",
6539
+ agent: agent2._name,
6540
+ data: {
6541
+ kind: "prompt_schema_oversized",
6542
+ estimatedTokens: promptTokens,
6543
+ threshold,
6544
+ site: "prompt"
6545
+ }
6546
+ });
6547
+ }
6548
+ if (ctx.mode === "json-schema") {
6549
+ const dropped = detectDroppedRefinements(schema);
6550
+ if (dropped.count > 0) {
6551
+ this.emitEvent({
6552
+ type: "schema_diagnostic",
6553
+ agent: agent2._name,
6554
+ data: {
6555
+ kind: "dropped_refinements",
6556
+ count: dropped.count,
6557
+ paths: dropped.paths,
6558
+ site: "prompt"
6559
+ }
6560
+ });
6561
+ warnSchemaDiagnosticOnce(
6562
+ `${agent2._name}\0dropped_refinements\0prompt\0${dropped.count}\0${dropped.paths.join(",")}`,
6563
+ `${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.`,
6564
+ silent
6565
+ );
6566
+ }
6567
+ }
6568
+ }
6569
+ const nativeOut = options?.nativeStructuredOutput ?? agent2._config.nativeStructuredOutput;
6570
+ if (nativeOut && toolDefs.length === 0) {
6571
+ const support = ctx.provider.nativeStructuredOutputSupport?.(ctx.model) ?? "schema";
6572
+ if (support !== "schema") {
6573
+ this.emitEvent({
6574
+ type: "schema_diagnostic",
6575
+ agent: agent2._name,
6576
+ data: {
6577
+ kind: "native_output_unsupported",
6578
+ ...ctx.provider.name ? { provider: ctx.provider.name } : {},
6579
+ support
6580
+ }
6581
+ });
6582
+ warnSchemaDiagnosticOnce(
6583
+ `${agent2._name}\0native_output_unsupported\0${ctx.provider.name ?? ctx.model}`,
6584
+ `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.`,
6585
+ silent
6586
+ );
6587
+ }
6588
+ }
6589
+ const rootIsObject = schema instanceof z.ZodObject;
6590
+ const hasTools = toolDefs.length > 0;
6591
+ if (this._streamingEnabled && (!rootIsObject || hasTools)) {
6592
+ const cause = !rootIsObject ? "non-object" : "tools";
6593
+ const rootType = schema.constructor?.name ?? "unknown";
6594
+ this.emitEvent({
6595
+ type: "schema_diagnostic",
6596
+ agent: agent2._name,
6597
+ data: { kind: "streaming_disabled", rootType, cause }
6598
+ });
6599
+ if (cause === "non-object") {
6600
+ warnSchemaDiagnosticOnce(
6601
+ `${agent2._name}\0streaming_disabled\0${rootType}`,
6602
+ `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.`,
6603
+ silent
6604
+ );
6605
+ }
6606
+ }
6607
+ }
6608
+ for (const tool2 of agent2._config.tools ?? []) {
6609
+ const toolTokens = estimateInlineSchemaTokens(tool2.inputSchema);
6610
+ if (toolTokens > threshold) {
6611
+ this.emitEvent({
6612
+ type: "schema_diagnostic",
6613
+ agent: agent2._name,
6614
+ data: {
6615
+ kind: "prompt_schema_oversized",
6616
+ estimatedTokens: toolTokens,
6617
+ threshold,
6618
+ site: "tool",
6619
+ tool: tool2.name
6620
+ }
6621
+ });
6622
+ }
6623
+ const droppedTool = detectDroppedRefinements(tool2.inputSchema);
6624
+ if (droppedTool.count > 0) {
6625
+ this.emitEvent({
6626
+ type: "schema_diagnostic",
6627
+ agent: agent2._name,
6628
+ data: {
6629
+ kind: "dropped_refinements",
6630
+ count: droppedTool.count,
6631
+ paths: droppedTool.paths,
6632
+ site: "tool",
6633
+ tool: tool2.name
6634
+ }
6635
+ });
6636
+ warnSchemaDiagnosticOnce(
6637
+ `${agent2._name}\0dropped_refinements\0tool\0${tool2.name}\0${droppedTool.count}`,
6638
+ `${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.`,
6639
+ silent
6640
+ );
6641
+ }
6642
+ }
6643
+ }
5367
6644
  buildToolDefs(agent2, resolvedHandoffs) {
5368
6645
  const defs = [];
5369
6646
  if (agent2._config.tools) {
@@ -5856,24 +7133,44 @@ ${summaryResponse.content}`
5856
7133
  limit,
5857
7134
  exceeded: false,
5858
7135
  policy,
5859
- abortController: controller
7136
+ abortController: controller,
7137
+ unpriced: false,
7138
+ unpricedCount: 0,
7139
+ unpricedWarned: false
5860
7140
  };
5861
7141
  const executeBudget = async () => {
5862
7142
  try {
5863
7143
  const value = budgetSignal ? await signalStorage.run(budgetSignal, fn) : await fn();
5864
7144
  const totalCost = this.budgetContext.totalCost;
5865
7145
  const exceeded = this.budgetContext.exceeded;
5866
- return { value, budgetExceeded: exceeded, totalCost };
7146
+ const unpriced = this.budgetContext.unpriced;
7147
+ return { value, budgetExceeded: exceeded, totalCost, unpriced };
5867
7148
  } catch (err) {
5868
7149
  if (this.budgetContext.exceeded) {
5869
- return { value: null, budgetExceeded: true, totalCost: this.budgetContext.totalCost };
7150
+ return {
7151
+ value: null,
7152
+ budgetExceeded: true,
7153
+ totalCost: this.budgetContext.totalCost,
7154
+ unpriced: this.budgetContext.unpriced
7155
+ };
5870
7156
  }
5871
7157
  if (err instanceof DOMException && err.name === "AbortError" && controller) {
5872
- return { value: null, budgetExceeded: true, totalCost: this.budgetContext.totalCost };
7158
+ return {
7159
+ value: null,
7160
+ budgetExceeded: true,
7161
+ totalCost: this.budgetContext.totalCost,
7162
+ unpriced: this.budgetContext.unpriced
7163
+ };
5873
7164
  }
5874
7165
  throw err;
5875
7166
  } finally {
5876
- if (parentBudget) parentBudget.totalCost += this.budgetContext.totalCost;
7167
+ if (parentBudget) {
7168
+ parentBudget.totalCost += this.budgetContext.totalCost;
7169
+ if (this.budgetContext.unpriced) {
7170
+ parentBudget.unpriced = true;
7171
+ parentBudget.unpricedCount += this.budgetContext.unpricedCount;
7172
+ }
7173
+ }
5877
7174
  this.budgetContext = parentBudget;
5878
7175
  }
5879
7176
  };
@@ -5894,13 +7191,22 @@ ${summaryResponse.content}`
5894
7191
  }
5895
7192
  return executeBudget();
5896
7193
  }
5897
- /** Get the current budget status, or null if not inside a budget block. */
7194
+ /**
7195
+ * Get the current budget status, or null if not inside a budget block. Inside
7196
+ * nested budgets this reflects the **innermost active** block (the nested block's
7197
+ * `finally` restores the parent before control returns to it).
7198
+ *
7199
+ * `unpriced` is true when this block ran a model with no usable cost (unpriced /
7200
+ * pricing-table miss): `spent` is then a LOWER BOUND and `remaining` an upper bound —
7201
+ * the limit cannot be enforced on the unmeasured spend. See {@link BudgetResult.unpriced}.
7202
+ */
5898
7203
  getBudgetStatus() {
5899
7204
  if (!this.budgetContext) return null;
5900
7205
  return {
5901
7206
  spent: this.budgetContext.totalCost,
5902
7207
  limit: this.budgetContext.limit,
5903
- remaining: Math.max(0, this.budgetContext.limit - this.budgetContext.totalCost)
7208
+ remaining: Math.max(0, this.budgetContext.limit - this.budgetContext.totalCost),
7209
+ unpriced: this.budgetContext.unpriced
5904
7210
  };
5905
7211
  }
5906
7212
  // ── ctx.race() ────────────────────────────────────────────────────────
@@ -6482,6 +7788,8 @@ ${summaryResponse.content}`
6482
7788
  });
6483
7789
  return this.ask(agents[0], prompt, {
6484
7790
  schema: options?.schema,
7791
+ schemaPrompt: options?.schemaPrompt,
7792
+ nativeStructuredOutput: options?.nativeStructuredOutput,
6485
7793
  retries: options?.retries,
6486
7794
  metadata: options?.metadata,
6487
7795
  validate: options?.validate,
@@ -6526,6 +7834,8 @@ ${summaryResponse.content}`
6526
7834
  });
6527
7835
  return this.ask(routerAgent, prompt, {
6528
7836
  schema: options?.schema,
7837
+ schemaPrompt: options?.schemaPrompt,
7838
+ nativeStructuredOutput: options?.nativeStructuredOutput,
6529
7839
  retries: options?.retries,
6530
7840
  metadata: options?.metadata,
6531
7841
  validate: options?.validate,
@@ -6544,8 +7854,25 @@ ${summaryResponse.content}`
6544
7854
  const frame = askStorage.getStore();
6545
7855
  const step = (frame?.stepRef ?? this.stepRefRoot).value++;
6546
7856
  const cost = partial.cost;
6547
- if (frame && typeof cost === "number" && Number.isFinite(cost) && cost >= 0 && COST_BEARING_LEAF_TYPES.includes(partial.type)) {
6548
- frame.askCost.value += cost;
7857
+ if (COST_BEARING_LEAF_TYPES.includes(partial.type)) {
7858
+ const usable = isUsableCost(cost);
7859
+ const unpriced = !usable && hasPositiveTokens(
7860
+ partial
7861
+ );
7862
+ if (frame) {
7863
+ if (usable) frame.askCost.value += cost;
7864
+ else if (unpriced) frame.askUnpriced = true;
7865
+ }
7866
+ if (unpriced && this.budgetContext) {
7867
+ this.budgetContext.unpriced = true;
7868
+ this.budgetContext.unpricedCount += 1;
7869
+ if (!this.budgetContext.unpricedWarned && Number.isFinite(this.budgetContext.limit)) {
7870
+ this.budgetContext.unpricedWarned = true;
7871
+ console.warn(
7872
+ "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."
7873
+ );
7874
+ }
7875
+ }
6549
7876
  }
6550
7877
  const event = {
6551
7878
  executionId: this.executionId,
@@ -8372,7 +9699,7 @@ var StreamingFlusher = class {
8372
9699
  await Promise.allSettled(pendings);
8373
9700
  }
8374
9701
  };
8375
- function isAbortError(err) {
9702
+ function isAbortError2(err) {
8376
9703
  return typeof err === "object" && err !== null && err.name === "AbortError";
8377
9704
  }
8378
9705
  function pushEventBounded(execInfo, event, cap) {
@@ -8635,7 +9962,7 @@ var AxlRuntime = class extends EventEmitter2 {
8635
9962
  this.persistExecution(execInfo);
8636
9963
  return result;
8637
9964
  } catch (err) {
8638
- const aborted = isAbortError(err);
9965
+ const aborted = isAbortError2(err);
8639
9966
  execInfo.status = "failed";
8640
9967
  execInfo.completedAt = Date.now();
8641
9968
  execInfo.duration = execInfo.completedAt - execInfo.startedAt;
@@ -8909,7 +10236,10 @@ var AxlRuntime = class extends EventEmitter2 {
8909
10236
  totalCost: 0,
8910
10237
  limit: budgetLimit,
8911
10238
  exceeded: false,
8912
- policy: "finish_and_stop"
10239
+ policy: "finish_and_stop",
10240
+ unpriced: false,
10241
+ unpricedCount: 0,
10242
+ unpricedWarned: false
8913
10243
  }
8914
10244
  });
8915
10245
  }
@@ -8944,6 +10274,7 @@ var AxlRuntime = class extends EventEmitter2 {
8944
10274
  status: "running",
8945
10275
  events: [],
8946
10276
  totalCost: 0,
10277
+ unpriced: false,
8947
10278
  startedAt: Date.now(),
8948
10279
  duration: 0,
8949
10280
  metadata: liftPersistedMetadata(options?.metadata)
@@ -8962,6 +10293,7 @@ var AxlRuntime = class extends EventEmitter2 {
8962
10293
  onTrace: (event) => {
8963
10294
  pushEventBounded(execInfo, event, this.maxEventsPerExecution);
8964
10295
  execInfo.totalCost += eventCostContribution(event);
10296
+ if (isUnpricedLeaf(event)) execInfo.unpriced = true;
8965
10297
  if (this.streamableExecutionIds.has(executionId)) {
8966
10298
  this.streamingFlusher?.append(executionId, event);
8967
10299
  }
@@ -9008,7 +10340,10 @@ var AxlRuntime = class extends EventEmitter2 {
9008
10340
  totalCost: 0,
9009
10341
  limit: Infinity,
9010
10342
  exceeded: false,
9011
- policy: "finish_and_stop"
10343
+ policy: "finish_and_stop",
10344
+ unpriced: false,
10345
+ unpricedCount: 0,
10346
+ unpricedWarned: false
9012
10347
  }
9013
10348
  });
9014
10349
  return this.spanManager.withSpanAsync(
@@ -9052,6 +10387,7 @@ var AxlRuntime = class extends EventEmitter2 {
9052
10387
  status: "running",
9053
10388
  events: [],
9054
10389
  totalCost: 0,
10390
+ unpriced: false,
9055
10391
  startedAt: Date.now(),
9056
10392
  duration: 0,
9057
10393
  metadata: liftPersistedMetadata(options?.metadata)
@@ -9075,6 +10411,7 @@ var AxlRuntime = class extends EventEmitter2 {
9075
10411
  onTrace: (event) => {
9076
10412
  pushEventBounded(execInfo, event, this.maxEventsPerExecution);
9077
10413
  execInfo.totalCost += eventCostContribution(event);
10414
+ if (isUnpricedLeaf(event)) execInfo.unpriced = true;
9078
10415
  if (this.streamableExecutionIds.has(executionId)) {
9079
10416
  this.streamingFlusher?.append(executionId, event);
9080
10417
  }
@@ -9122,7 +10459,10 @@ var AxlRuntime = class extends EventEmitter2 {
9122
10459
  totalCost: 0,
9123
10460
  limit: Infinity,
9124
10461
  exceeded: false,
9125
- policy: "finish_and_stop"
10462
+ policy: "finish_and_stop",
10463
+ unpriced: false,
10464
+ unpricedCount: 0,
10465
+ unpricedWarned: false
9126
10466
  }
9127
10467
  });
9128
10468
  return this.spanManager.withSpanAsync(
@@ -9284,12 +10624,14 @@ var AxlRuntime = class extends EventEmitter2 {
9284
10624
  const completedAt = lastEvent.timestamp ?? startedAt;
9285
10625
  const workflowName = workflowStartEvent?.workflow ?? RECOVERED_UNKNOWN_WORKFLOW;
9286
10626
  const totalCost = boundedEvents.reduce((sum, e) => sum + eventCostContribution(e), 0);
10627
+ const unpriced = boundedEvents.some(isUnpricedLeaf);
9287
10628
  const synthesized = {
9288
10629
  executionId: id,
9289
10630
  workflow: workflowName,
9290
10631
  status: "failed",
9291
10632
  events: boundedEvents,
9292
10633
  totalCost,
10634
+ unpriced,
9293
10635
  startedAt,
9294
10636
  completedAt,
9295
10637
  duration: completedAt - startedAt,
@@ -9591,8 +10933,8 @@ var AxlRuntime = class extends EventEmitter2 {
9591
10933
  * Works with both `createContext()` and `execute()` calls inside `fn`.
9592
10934
  */
9593
10935
  async trackCost(fn) {
9594
- const { result, cost } = await this.trackExecution(fn);
9595
- return { result, cost };
10936
+ const { result, cost, unpriced } = await this.trackExecution(fn);
10937
+ return { result, cost, unpriced };
9596
10938
  }
9597
10939
  /**
9598
10940
  * Track cost and execution metadata across any runtime operations within the given function.
@@ -9637,10 +10979,12 @@ var AxlRuntime = class extends EventEmitter2 {
9637
10979
  const workflowCalls = /* @__PURE__ */ new Map();
9638
10980
  const tokens = { input: 0, output: 0, reasoning: 0 };
9639
10981
  let agentCalls = 0;
10982
+ let unpriced = false;
9640
10983
  const capturedTraces = options?.captureTraces ? [] : void 0;
9641
10984
  const listener = (event) => {
9642
10985
  if (!scope.trackedIds.has(event.executionId)) return;
9643
10986
  scope.totalCost += eventCostContribution(event);
10987
+ if (isUnpricedLeaf(event)) unpriced = true;
9644
10988
  if (event.type === "agent_call_end") {
9645
10989
  if (event.model) modelCalls.set(event.model, (modelCalls.get(event.model) ?? 0) + 1);
9646
10990
  agentCalls++;
@@ -9678,6 +11022,7 @@ var AxlRuntime = class extends EventEmitter2 {
9678
11022
  return {
9679
11023
  result,
9680
11024
  cost: scope.totalCost,
11025
+ unpriced,
9681
11026
  ...capturedTraces ? { traces: capturedTraces } : {},
9682
11027
  metadata: {
9683
11028
  models: [...modelCalls.keys()],
@@ -10572,22 +11917,40 @@ var OpenAIEmbedder = class {
10572
11917
  this.dimensions = options.dimensions ?? 1536;
10573
11918
  }
10574
11919
  async embed(texts, signal) {
10575
- const response = await fetchWithRetry(`${this.baseUrl}/v1/embeddings`, {
10576
- method: "POST",
10577
- headers: {
10578
- "Content-Type": "application/json",
10579
- Authorization: `Bearer ${this.apiKey}`
11920
+ const response = await fetchWithRetry(
11921
+ `${this.baseUrl}/v1/embeddings`,
11922
+ {
11923
+ method: "POST",
11924
+ headers: {
11925
+ "Content-Type": "application/json",
11926
+ Authorization: `Bearer ${this.apiKey}`
11927
+ },
11928
+ body: JSON.stringify({
11929
+ model: this.model,
11930
+ input: texts,
11931
+ dimensions: this.dimensions
11932
+ }),
11933
+ signal
10580
11934
  },
10581
- body: JSON.stringify({
10582
- model: this.model,
10583
- input: texts,
10584
- dimensions: this.dimensions
10585
- }),
10586
- signal
10587
- });
11935
+ // Participate in the same typed-error contract as the provider adapters: a
11936
+ // thrown network failure normalizes to ProviderError{status:0,provider:'openai'}.
11937
+ { provider: "openai" }
11938
+ );
10588
11939
  if (!response.ok) {
10589
11940
  const body = await response.text();
10590
- throw new Error(`OpenAI embeddings API error (${response.status}): ${body}`);
11941
+ let detail = body;
11942
+ try {
11943
+ const json2 = JSON.parse(body);
11944
+ detail = json2.error?.message ?? json2.message ?? body;
11945
+ } catch {
11946
+ }
11947
+ throw buildProviderError({
11948
+ provider: "openai",
11949
+ status: response.status,
11950
+ headers: response.headers,
11951
+ message: `OpenAI embeddings API error (${response.status}): ${detail}`,
11952
+ body
11953
+ });
10591
11954
  }
10592
11955
  const json = await response.json();
10593
11956
  const vectors = json.data.sort((a, b) => a.index - b.index).map((d) => d.embedding);
@@ -10728,57 +12091,88 @@ function cosineSimilarity2(a, b) {
10728
12091
  }
10729
12092
  export {
10730
12093
  AXL_EVENT_TYPES,
12094
+ AZURE_PROFILE,
10731
12095
  AnthropicProvider,
10732
12096
  AxlError,
10733
12097
  AxlEventBus,
10734
12098
  AxlRuntime,
10735
12099
  AxlStream,
12100
+ BEDROCK_PROFILE,
12101
+ BUILTIN_PROFILES,
10736
12102
  BudgetExceededError,
10737
12103
  CONFIG_CANDIDATES,
10738
12104
  COST_BEARING_LEAF_TYPES,
12105
+ DEEPSEEK_PROFILE,
10739
12106
  EventStreamOverflowError,
12107
+ GROQ_PROFILE,
10740
12108
  GeminiProvider,
10741
12109
  GuardrailError,
10742
12110
  InMemoryVectorStore,
12111
+ LLAMACPP_PROFILE,
12112
+ LMSTUDIO_PROFILE,
12113
+ MISTRAL_PROFILE,
10743
12114
  MaxTurnsError,
10744
12115
  McpManager,
10745
12116
  MemoryManager,
10746
12117
  MemoryStore,
10747
12118
  NoConsensus,
10748
12119
  NoopSpanManager,
12120
+ OLLAMA_PROFILE,
12121
+ OPENAI_PRICING,
12122
+ OPENAI_PROFILE,
12123
+ OPENROUTER_PROFILE,
12124
+ OpenAICompatibleProvider,
10749
12125
  OpenAIEmbedder,
10750
12126
  OpenAIProvider,
10751
12127
  OpenAIResponsesProvider,
12128
+ ProviderError,
10752
12129
  ProviderRegistry,
10753
12130
  QuorumNotMet,
10754
12131
  REDACTED,
10755
12132
  REDACTION_RULES,
10756
12133
  RateLimiter,
10757
12134
  RedisStore,
12135
+ SGLANG_PROFILE,
10758
12136
  SQLiteStore,
10759
12137
  Session,
10760
12138
  SqliteVectorStore,
12139
+ ThinkTagScanner,
10761
12140
  TimeoutError,
10762
12141
  ToolDenied,
12142
+ ToolModelOutputError,
12143
+ VLLM_PROFILE,
10763
12144
  ValidationError,
10764
12145
  VerifyError,
10765
12146
  WorkflowContext,
12147
+ XAI_PROFILE,
10766
12148
  agent,
10767
12149
  createSpanManager,
10768
12150
  defineConfig,
12151
+ estimateOpenAICost,
10769
12152
  eventCostContribution,
10770
12153
  expandGlob,
10771
12154
  extractJson,
12155
+ extractThinkTags,
10772
12156
  findConfig,
12157
+ hasPositiveTokens,
10773
12158
  importModule,
10774
12159
  isCostBearingLeaf,
12160
+ isRetryableStatus,
10775
12161
  isRootLevel,
12162
+ isUnpricedLeaf,
12163
+ isUsableCost,
10776
12164
  needsTsxLoader,
12165
+ openaiReasoningEmit,
10777
12166
  parsePartialJson,
10778
12167
  pickDefault,
10779
12168
  pickExport,
12169
+ priceFromTable,
12170
+ reasoningEffortEmit,
12171
+ reasoningObjectEmit,
10780
12172
  redactEvent,
10781
12173
  registerConditions,
12174
+ resolveApiKey,
12175
+ resolvePerModel,
10782
12176
  resolveRuntime,
10783
12177
  resolveThinkingOptions,
10784
12178
  stringStreamFromEvents,