@axlsdk/axl 0.18.1 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -120,11 +120,218 @@ function resolveThinkingOptions(options) {
120
120
  hasBudgetOverride
121
121
  };
122
122
  }
123
+ async function resolveApiKey(source) {
124
+ if (typeof source === "function") return await source() ?? "";
125
+ return source ?? "";
126
+ }
127
+
128
+ // src/errors.ts
129
+ var AxlError = class extends Error {
130
+ code;
131
+ constructor(code, message) {
132
+ super(message);
133
+ this.name = "AxlError";
134
+ this.code = code;
135
+ }
136
+ };
137
+ var VerifyError = class extends AxlError {
138
+ lastOutput;
139
+ zodError;
140
+ retries;
141
+ constructor(lastOutput, zodError, retries) {
142
+ super("VERIFY_ERROR", `Schema validation failed after ${retries} retries: ${zodError.message}`);
143
+ this.name = "VerifyError";
144
+ this.lastOutput = lastOutput;
145
+ this.zodError = zodError;
146
+ this.retries = retries;
147
+ }
148
+ };
149
+ var QuorumNotMet = class extends AxlError {
150
+ results;
151
+ constructor(required, actual, results) {
152
+ super("QUORUM_NOT_MET", `Quorum not met: needed ${required} successes, got ${actual}`);
153
+ this.name = "QuorumNotMet";
154
+ this.results = results;
155
+ }
156
+ };
157
+ var NoConsensus = class extends AxlError {
158
+ constructor(reason) {
159
+ super("NO_CONSENSUS", `No consensus: ${reason}`);
160
+ this.name = "NoConsensus";
161
+ }
162
+ };
163
+ var TimeoutError = class extends AxlError {
164
+ constructor(operation, timeoutMs) {
165
+ super("TIMEOUT", `${operation} exceeded timeout of ${timeoutMs}ms`);
166
+ this.name = "TimeoutError";
167
+ }
168
+ };
169
+ function formatBudgetCost(cost) {
170
+ if (Number.isNaN(cost)) return "$NaN";
171
+ if (cost === Infinity) return "$Infinity";
172
+ if (cost === -Infinity) return "-$Infinity";
173
+ if (cost === 0) return "$0.00";
174
+ const sign = cost < 0 ? "-" : "";
175
+ const abs = Math.abs(cost);
176
+ if (abs < 1e-6) return `${sign}< $0.000001`;
177
+ if (abs < 1e-4) {
178
+ const [mantissa, exponent] = abs.toExponential(2).split("e");
179
+ return `${sign}$${mantissa}e${parseInt(exponent, 10)}`;
180
+ }
181
+ if (abs < 0.01) return `${sign}$${abs.toFixed(6)}`;
182
+ return `${sign}$${abs.toFixed(2)}`;
183
+ }
184
+ var BudgetExceededError = class extends AxlError {
185
+ limit;
186
+ spent;
187
+ policy;
188
+ constructor(limit, spent, policy) {
189
+ super(
190
+ "BUDGET_EXCEEDED",
191
+ `Budget exceeded: spent ${formatBudgetCost(spent)} of ${formatBudgetCost(
192
+ limit
193
+ )} limit (policy: ${policy})`
194
+ );
195
+ this.name = "BudgetExceededError";
196
+ this.limit = limit;
197
+ this.spent = spent;
198
+ this.policy = policy;
199
+ }
200
+ };
201
+ var MaxTurnsError = class extends AxlError {
202
+ maxTurns;
203
+ constructor(operation, maxTurns) {
204
+ super("MAX_TURNS", `${operation} exceeded maximum of ${maxTurns} turns`);
205
+ this.name = "MaxTurnsError";
206
+ this.maxTurns = maxTurns;
207
+ }
208
+ };
209
+ var GuardrailError = class extends AxlError {
210
+ guardrailType;
211
+ reason;
212
+ constructor(guardrailType, reason) {
213
+ super("GUARDRAIL_BLOCKED", `${guardrailType} guardrail blocked: ${reason}`);
214
+ this.name = "GuardrailError";
215
+ this.guardrailType = guardrailType;
216
+ this.reason = reason;
217
+ }
218
+ };
219
+ var ValidationError = class extends AxlError {
220
+ lastOutput;
221
+ reason;
222
+ retries;
223
+ constructor(lastOutput, reason, retries) {
224
+ super("VALIDATION_ERROR", `Validation failed after ${retries} retries: ${reason}`);
225
+ this.name = "ValidationError";
226
+ this.lastOutput = lastOutput;
227
+ this.reason = reason;
228
+ this.retries = retries;
229
+ }
230
+ };
231
+ var ToolDenied = class extends AxlError {
232
+ toolName;
233
+ agentName;
234
+ constructor(toolName, agentName) {
235
+ super(
236
+ "TOOL_DENIED",
237
+ `Agent "${agentName}" attempted to call tool "${toolName}" which is not in its ACL`
238
+ );
239
+ this.name = "ToolDenied";
240
+ this.toolName = toolName;
241
+ this.agentName = agentName;
242
+ }
243
+ };
244
+
245
+ // src/providers/errors.ts
246
+ var ProviderError = class extends AxlError {
247
+ /** Adapter/profile name, e.g. `'openai'`, `'anthropic'`, `'google'`. */
248
+ provider;
249
+ /** HTTP status code; `0` for non-HTTP (network) failures. */
250
+ status;
251
+ /** Semantic failover hint (see module doc) — NOT the transport-retry set. */
252
+ retryable;
253
+ /** Parsed `Retry-After` delay in ms (RAW/unclamped), when the header was present. */
254
+ retryAfterMs;
255
+ /** Provider request id from response headers, when present. */
256
+ requestId;
257
+ /**
258
+ * Raw provider error body. Lives ONLY on the error object — it is
259
+ * intentionally never placed on the event stream (it can echo prompt text
260
+ * and is redaction-eligible). See `docs/security.md`.
261
+ */
262
+ body;
263
+ constructor(args) {
264
+ super("PROVIDER_ERROR", args.message);
265
+ this.name = "ProviderError";
266
+ this.provider = args.provider;
267
+ this.status = args.status;
268
+ this.retryable = args.retryable;
269
+ this.retryAfterMs = args.retryAfterMs;
270
+ this.requestId = args.requestId;
271
+ this.body = args.body;
272
+ }
273
+ };
274
+ function isRetryableStatus(status) {
275
+ switch (status) {
276
+ case 0:
277
+ return true;
278
+ case 408:
279
+ // request timeout
280
+ case 429:
281
+ // rate limit
282
+ case 500:
283
+ case 502:
284
+ case 503:
285
+ case 504:
286
+ case 529:
287
+ return true;
288
+ // Explicitly non-retryable (bad request / auth / not found / payload):
289
+ // 400, 401, 403, 404, 413, 422 fall through to the default.
290
+ default:
291
+ return false;
292
+ }
293
+ }
294
+ function parseRetryAfter(headers) {
295
+ const raw = headers.get("retry-after");
296
+ if (!raw) return void 0;
297
+ const trimmed = raw.trim();
298
+ if (/^\d+(\.\d+)?$/.test(trimmed)) {
299
+ const ms = Number(trimmed) * 1e3;
300
+ return ms > 0 ? ms : void 0;
301
+ }
302
+ const dateMs = Date.parse(trimmed);
303
+ if (Number.isNaN(dateMs)) return void 0;
304
+ const delta = dateMs - Date.now();
305
+ return delta > 0 ? delta : void 0;
306
+ }
307
+ var REQUEST_ID_HEADERS = ["x-request-id", "request-id"];
308
+ function extractRequestId(headers) {
309
+ for (const name of REQUEST_ID_HEADERS) {
310
+ const value = headers.get(name);
311
+ if (value) return value;
312
+ }
313
+ return void 0;
314
+ }
315
+ function buildProviderError(args) {
316
+ return new ProviderError({
317
+ provider: args.provider,
318
+ status: args.status,
319
+ retryable: isRetryableStatus(args.status),
320
+ message: args.message,
321
+ retryAfterMs: args.headers ? parseRetryAfter(args.headers) : void 0,
322
+ requestId: args.headers ? extractRequestId(args.headers) : void 0,
323
+ body: args.body
324
+ });
325
+ }
123
326
 
124
327
  // src/providers/retry.ts
125
328
  var RETRYABLE_STATUS_CODES = /* @__PURE__ */ new Set([429, 503, 529]);
126
329
  var MAX_RETRIES = 2;
127
330
  var BASE_DELAY_MS = 1e3;
331
+ var MAX_BACKOFF_MS = 6e4;
332
+ function jitter(ms) {
333
+ return ms * (0.75 + Math.random() * 0.5);
334
+ }
128
335
  function sleep2(ms, signal) {
129
336
  return new Promise((resolve2) => {
130
337
  if (signal?.aborted) {
@@ -142,9 +349,14 @@ function sleep2(ms, signal) {
142
349
  );
143
350
  });
144
351
  }
352
+ function isAbortError(err, signal) {
353
+ if (signal?.aborted) return true;
354
+ return typeof err === "object" && err !== null && "name" in err && err.name === "AbortError";
355
+ }
145
356
  async function fetchWithRetry(input, init, opts) {
146
357
  const maxRetries = opts?.maxRetries ?? MAX_RETRIES;
147
358
  const governor = opts?.governor;
359
+ const provider = opts?.provider ?? "unknown";
148
360
  let acquired = false;
149
361
  if (governor) {
150
362
  await governor.acquire(init?.signal ?? void 0);
@@ -152,7 +364,21 @@ async function fetchWithRetry(input, init, opts) {
152
364
  }
153
365
  try {
154
366
  for (let attempt = 0; ; attempt++) {
155
- const res = await fetch(input, init);
367
+ let res;
368
+ try {
369
+ res = await fetch(input, init);
370
+ } catch (err) {
371
+ if (isAbortError(err, init?.signal ?? void 0)) throw err;
372
+ if (attempt >= maxRetries) {
373
+ throw buildProviderError({
374
+ provider,
375
+ status: 0,
376
+ message: err instanceof Error ? err.message : String(err)
377
+ });
378
+ }
379
+ await sleep2(jitter(BASE_DELAY_MS * 2 ** attempt), init?.signal ?? void 0);
380
+ continue;
381
+ }
156
382
  governor?.observe(res);
157
383
  if (res.ok || !RETRYABLE_STATUS_CODES.has(res.status) || attempt >= maxRetries) {
158
384
  return res;
@@ -160,15 +386,9 @@ async function fetchWithRetry(input, init, opts) {
160
386
  if (init?.signal?.aborted) {
161
387
  return res;
162
388
  }
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);
389
+ const retryAfterMs = parseRetryAfter(res.headers);
390
+ const baseDelay = retryAfterMs !== void 0 ? Math.min(retryAfterMs, MAX_BACKOFF_MS) : BASE_DELAY_MS * 2 ** attempt;
391
+ await sleep2(jitter(baseDelay), init?.signal ?? void 0);
172
392
  }
173
393
  } finally {
174
394
  if (acquired) governor.release();
@@ -316,254 +536,467 @@ var RateLimiter = class {
316
536
  }
317
537
  };
318
538
 
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];
539
+ // src/providers/openai-compatible.ts
540
+ function resolvePerModel(value, model, fallback) {
541
+ if (value === void 0) return fallback;
542
+ return typeof value === "function" ? value(model) : value;
543
+ }
544
+ var SORTED_KEYS_CACHE = /* @__PURE__ */ new WeakMap();
545
+ function sortedKeys(table) {
546
+ let keys = SORTED_KEYS_CACHE.get(table);
547
+ if (!keys) {
548
+ keys = Object.keys(table).sort((a, b) => b.length - a.length);
549
+ SORTED_KEYS_CACHE.set(table, keys);
550
+ }
551
+ return keys;
552
+ }
553
+ function priceFromTable(table, model, promptTokens, completionTokens, cachedTokens) {
554
+ let pricing = table[model];
354
555
  if (!pricing) {
355
- for (const key of PRICING_KEYS_BY_LENGTH) {
556
+ for (const key of sortedKeys(table)) {
356
557
  if (model.startsWith(key)) {
357
- pricing = OPENAI_PRICING[key];
558
+ pricing = table[key];
358
559
  break;
359
560
  }
360
561
  }
361
562
  }
362
- if (!pricing) return 0;
563
+ if (!pricing) return void 0;
363
564
  const [inputRate, outputRate, cacheMultiplier] = pricing;
364
565
  const cached = cachedTokens ?? 0;
365
566
  const inputCost = (promptTokens - cached) * inputRate + cached * inputRate * cacheMultiplier;
366
567
  return inputCost + completionTokens * outputRate;
367
568
  }
368
- function isOSeriesModel(model) {
369
- return /^(o1|o3|o4-mini)/.test(model);
569
+ var THINK_OPEN = "<think>";
570
+ var THINK_CLOSE = "</think>";
571
+ function partialTagSuffixLen(s, tag) {
572
+ const max = Math.min(s.length, tag.length - 1);
573
+ for (let n = max; n > 0; n--) {
574
+ if (tag.startsWith(s.slice(s.length - n))) return n;
575
+ }
576
+ return 0;
370
577
  }
371
- function supportsReasoningEffort(model) {
372
- return isOSeriesModel(model) || /^gpt-5/.test(model);
373
- }
374
- function supportsReasoningNone(model) {
375
- return /^gpt-5\.[1-9]/.test(model);
578
+ var ThinkTagScanner = class {
579
+ buffer = "";
580
+ inside = false;
581
+ push(chunk) {
582
+ this.buffer += chunk;
583
+ let text = "";
584
+ let thinking = "";
585
+ for (; ; ) {
586
+ const tag = this.inside ? THINK_CLOSE : THINK_OPEN;
587
+ const idx = this.buffer.indexOf(tag);
588
+ if (idx === -1) {
589
+ const hold = partialTagSuffixLen(this.buffer, tag);
590
+ const emit = this.buffer.slice(0, this.buffer.length - hold);
591
+ if (this.inside) thinking += emit;
592
+ else text += emit;
593
+ this.buffer = this.buffer.slice(this.buffer.length - hold);
594
+ break;
595
+ }
596
+ const before = this.buffer.slice(0, idx);
597
+ if (this.inside) thinking += before;
598
+ else text += before;
599
+ this.buffer = this.buffer.slice(idx + tag.length);
600
+ this.inside = !this.inside;
601
+ }
602
+ return { text, thinking };
603
+ }
604
+ /** Flush any residual buffer at stream end (treat as the current channel). */
605
+ flush() {
606
+ const rest = this.buffer;
607
+ this.buffer = "";
608
+ return this.inside ? { text: "", thinking: rest } : { text: rest, thinking: "" };
609
+ }
610
+ };
611
+ function extractThinkTags(content) {
612
+ const scanner = new ThinkTagScanner();
613
+ const a = scanner.push(content);
614
+ const b = scanner.flush();
615
+ return { content: a.text + b.text, thinking: a.thinking + b.thinking };
376
616
  }
377
- function supportsXhigh(model) {
378
- return /^gpt-5\.([2-9]|\d{2,})/.test(model);
617
+ function reasoningEffortEmit(map) {
618
+ return (body, resolved, model) => {
619
+ const value = map(resolved, model);
620
+ if (value) body.reasoning_effort = value;
621
+ };
379
622
  }
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;
623
+ function reasoningObjectEmit(mapEffort) {
624
+ return (body, resolved, model) => {
625
+ if (resolved.hasBudgetOverride) {
626
+ body.reasoning = { max_tokens: resolved.thinkingBudget };
627
+ } else if (resolved.activeEffort) {
628
+ body.reasoning = { effort: mapEffort(resolved.activeEffort, model) };
629
+ } else if (resolved.thinkingDisabled) {
630
+ body.reasoning = { enabled: false };
631
+ }
632
+ };
385
633
  }
386
- function effortToReasoningEffort(effort) {
387
- return effort === "max" ? "xhigh" : effort;
634
+ var ROUND_TRIP_FIELDS = ["reasoning_content", "reasoning", "reasoning_details"];
635
+ function isRoundTripField(field) {
636
+ return typeof field === "string" && ROUND_TRIP_FIELDS.includes(field);
388
637
  }
389
- function budgetToReasoningEffort(budget) {
390
- if (budget <= 1024) return "low";
391
- if (budget <= 8192) return "medium";
392
- return "high";
638
+ function normalizeMessageContent(content) {
639
+ if (typeof content === "string") return content;
640
+ if (content == null) return "";
641
+ if (!Array.isArray(content)) return "";
642
+ return content.map((part) => {
643
+ if (typeof part === "string") return part;
644
+ if (!part || typeof part !== "object") return "";
645
+ if (typeof part.text === "string") return part.text;
646
+ if (typeof part.content === "string") return part.content;
647
+ return "";
648
+ }).join("");
393
649
  }
394
- var OpenAIProvider = class {
395
- name = "openai";
650
+ var OpenAICompatibleProvider = class {
651
+ name;
652
+ /** `json_schema` is honored natively when the profile's `supportsJsonSchema`
653
+ * capability is true for this model (the default); otherwise the engine
654
+ * downgrades the request to plain `json_object` (see `buildRequestBody`),
655
+ * which does NOT enforce the schema shape. */
656
+ nativeStructuredOutputSupport(model) {
657
+ const supportsSchema = resolvePerModel(
658
+ this.profile.capabilities?.supportsJsonSchema,
659
+ model,
660
+ true
661
+ );
662
+ return supportsSchema ? "schema" : "downgraded";
663
+ }
664
+ profile;
396
665
  baseUrl;
397
- apiKey;
666
+ /** A key string, or a resolver invoked per request (expiring tokens). */
667
+ apiKeySource;
668
+ authHeader;
398
669
  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(/\/$/, "");
670
+ constructor(options) {
671
+ const p = options.profile;
672
+ this.profile = p;
673
+ this.name = p.name;
674
+ this.apiKeySource = options.apiKey ?? (p.envApiKey ? process.env[p.envApiKey] : void 0) ?? "";
675
+ this.authHeader = options.authHeader;
676
+ const envBase = p.envBaseUrl ? process.env[p.envBaseUrl] : void 0;
677
+ const explicitBase = options.baseUrl ?? envBase;
678
+ this.baseUrl = (explicitBase ?? p.defaultBaseUrl).replace(/\/$/, "");
402
679
  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.");
680
+ const label = p.label ?? p.name;
681
+ if (p.requireExplicitBaseUrl && explicitBase === void 0) {
682
+ const env = p.envBaseUrl ? ` or ${p.envBaseUrl}` : "";
683
+ throw new Error(
684
+ `${label} requires a base URL. Set providers.${p.name}.baseUrl${env} (it is resource-specific and has no default).`
685
+ );
686
+ }
687
+ if (typeof this.apiKeySource === "string" && !this.apiKeySource && !p.allowMissingApiKey) {
688
+ const env = p.envApiKey ?? "the API key env var";
689
+ throw new Error(`${label} API key is required. Set ${env} or pass apiKey in options.`);
405
690
  }
406
691
  }
407
- // ---------------------------------------------------------------------------
408
- // chat - non-streaming completion
409
- // ---------------------------------------------------------------------------
692
+ /** Resolve the API key for one request, validating against allowMissingApiKey. */
693
+ async resolveKey() {
694
+ const key = await resolveApiKey(this.apiKeySource);
695
+ if (!key && !this.profile.allowMissingApiKey) {
696
+ const label = this.profile.label ?? this.profile.name;
697
+ const env = this.profile.envApiKey ?? "the API key env var";
698
+ throw new Error(`${label} API key is required. Set ${env} or pass apiKey in options.`);
699
+ }
700
+ return key;
701
+ }
410
702
  async chat(messages, options) {
703
+ const headers = this.buildHeaders(await this.resolveKey());
411
704
  const body = this.buildRequestBody(messages, options, false);
412
705
  const res = await fetchWithRetry(
413
706
  `${this.baseUrl}/chat/completions`,
414
707
  {
415
708
  method: "POST",
416
- headers: {
417
- "Content-Type": "application/json",
418
- Authorization: `Bearer ${this.apiKey}`
419
- },
709
+ headers,
420
710
  body: JSON.stringify(body),
421
711
  signal: options.signal
422
712
  },
423
- { governor: this.governor }
713
+ { governor: this.governor, provider: this.name }
424
714
  );
425
715
  if (!res.ok) {
426
716
  const errorBody = await res.text();
427
- const message = this.extractErrorMessage(errorBody, res.status);
428
- throw new Error(message);
717
+ throw buildProviderError({
718
+ provider: this.name,
719
+ status: res.status,
720
+ headers: res.headers,
721
+ message: this.extractErrorMessage(errorBody, res.status),
722
+ body: errorBody
723
+ });
429
724
  }
430
725
  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
- };
726
+ return this.parseResponse(json, options.model);
458
727
  }
459
- // ---------------------------------------------------------------------------
460
- // stream - SSE streaming completion
461
- // ---------------------------------------------------------------------------
462
728
  async *stream(messages, options) {
729
+ const headers = this.buildHeaders(await this.resolveKey());
463
730
  const body = this.buildRequestBody(messages, options, true);
464
731
  const res = await fetchWithRetry(
465
732
  `${this.baseUrl}/chat/completions`,
466
733
  {
467
734
  method: "POST",
468
- headers: {
469
- "Content-Type": "application/json",
470
- Authorization: `Bearer ${this.apiKey}`
471
- },
735
+ headers,
472
736
  body: JSON.stringify(body),
473
737
  signal: options.signal
474
738
  },
475
- { governor: this.governor }
739
+ { governor: this.governor, provider: this.name }
476
740
  );
477
741
  if (!res.ok) {
478
742
  const errorBody = await res.text();
479
- const message = this.extractErrorMessage(errorBody, res.status);
480
- throw new Error(message);
743
+ throw buildProviderError({
744
+ provider: this.name,
745
+ status: res.status,
746
+ headers: res.headers,
747
+ message: this.extractErrorMessage(errorBody, res.status),
748
+ body: errorBody
749
+ });
481
750
  }
482
751
  if (!res.body) {
483
- throw new Error("OpenAI stream response has no body");
752
+ throw new Error(`${this.profile.label ?? this.profile.name} stream response has no body`);
484
753
  }
485
754
  yield* this.parseSSEStream(res.body, options.model);
486
755
  }
487
756
  // ---------------------------------------------------------------------------
488
- // Internal helpers
757
+ // Request building
489
758
  // ---------------------------------------------------------------------------
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
- }
759
+ buildHeaders(apiKey) {
760
+ const headers = {
761
+ "Content-Type": "application/json",
762
+ ...this.profile.headers ?? {}
763
+ };
764
+ if (apiKey) {
765
+ const auth = this.authHeader ?? this.profile.authHeader ?? "bearer";
766
+ if (auth === "bearer") headers.Authorization = `Bearer ${apiKey}`;
767
+ else if (auth === "api-key") headers["api-key"] = apiKey;
768
+ else headers[auth.header] = auth.scheme ? `${auth.scheme} ${apiKey}` : apiKey;
503
769
  }
504
- const stripTemp = oSeries || reasoningCapable && wireEffort !== void 0;
770
+ return headers;
771
+ }
772
+ buildRequestBody(messages, options, stream) {
773
+ const profile = this.profile;
774
+ const resolved = resolveThinkingOptions(options);
505
775
  const body = {
506
776
  model: options.model,
507
- messages: messages.map((m) => this.formatMessage(m, oSeries)),
777
+ messages: messages.map((m) => this.formatMessage(m, options.model)),
508
778
  stream
509
779
  };
510
- if (options.temperature !== void 0 && !stripTemp) {
780
+ const emitResult = profile.reasoning.emit(body, resolved, options.model) ?? {};
781
+ const stripTemperature = emitResult.stripTemperature ?? false;
782
+ if (options.temperature !== void 0 && !stripTemperature) {
511
783
  body.temperature = options.temperature;
512
784
  }
513
785
  if (options.maxTokens !== void 0) {
514
- body.max_completion_tokens = options.maxTokens;
786
+ body[profile.maxTokensField ?? "max_completion_tokens"] = options.maxTokens;
515
787
  }
516
788
  if (options.stop) body.stop = options.stop;
517
789
  if (options.tools && options.tools.length > 0) {
518
790
  body.tools = options.tools;
519
- if (!oSeries) {
791
+ if (resolvePerModel(profile.parallelToolCalls, options.model, false)) {
520
792
  body.parallel_tool_calls = true;
521
793
  }
522
794
  }
523
- if (options.toolChoice !== void 0) {
524
- body.tool_choice = options.toolChoice;
525
- }
795
+ if (options.toolChoice !== void 0) body.tool_choice = options.toolChoice;
526
796
  if (options.responseFormat) {
527
- body.response_format = options.responseFormat;
797
+ const supportsSchema = resolvePerModel(
798
+ profile.capabilities?.supportsJsonSchema,
799
+ options.model,
800
+ true
801
+ );
802
+ body.response_format = options.responseFormat.type === "json_schema" && !supportsSchema ? { type: "json_object" } : options.responseFormat;
528
803
  }
529
- if (wireEffort) body.reasoning_effort = wireEffort;
530
- if (stream) {
804
+ if (stream && (profile.capabilities?.supportsStreamUsage ?? true)) {
531
805
  body.stream_options = { include_usage: true };
532
806
  }
533
- if (options.providerOptions) {
534
- Object.assign(body, options.providerOptions);
807
+ if (profile.requestDefaults) Object.assign(body, profile.requestDefaults);
808
+ const userKeys = options.providerOptions ? Object.keys(options.providerOptions) : [];
809
+ if (options.providerOptions) Object.assign(body, options.providerOptions);
810
+ const forbidden = resolvePerModel(profile.capabilities?.forbiddenParams, options.model, []);
811
+ for (const key of forbidden) {
812
+ if (!userKeys.includes(key)) delete body[key];
535
813
  }
536
814
  return body;
537
815
  }
538
- /** Extract a human-readable message from an API error response body. */
816
+ formatMessage(msg, model) {
817
+ const out = {
818
+ role: this.profile.roleFor ? this.profile.roleFor(msg.role, model) : msg.role,
819
+ content: msg.content
820
+ };
821
+ if (msg.name && (this.profile.capabilities?.emitsMessageName ?? true)) out.name = msg.name;
822
+ if (msg.tool_calls) out.tool_calls = msg.tool_calls;
823
+ if (msg.tool_call_id) out.tool_call_id = msg.tool_call_id;
824
+ if (this.profile.reasoning.roundTrip === "on-tool-call-turns" && msg.role === "assistant" && msg.tool_calls && msg.tool_calls.length > 0) {
825
+ const rt = msg.providerMetadata?.openaiCompatReasoning;
826
+ if (rt && rt.provider === this.name && isRoundTripField(rt.field) && rt.value !== void 0 && rt.value !== null) {
827
+ out[rt.field] = rt.value;
828
+ }
829
+ }
830
+ return out;
831
+ }
832
+ // ---------------------------------------------------------------------------
833
+ // Response parsing
834
+ // ---------------------------------------------------------------------------
539
835
  extractErrorMessage(body, status) {
836
+ const label = this.profile.label ?? this.profile.name;
540
837
  try {
541
838
  const json = JSON.parse(body);
542
- if (json.error?.message) {
543
- return `OpenAI API error (${status}): ${json.error.message}`;
544
- }
839
+ const message = json.error?.message ?? json.message ?? json.detail;
840
+ if (message) return `${label} API error (${status}): ${message}`;
545
841
  } catch {
546
842
  }
547
- return `OpenAI API error (${status}): ${body}`;
843
+ return `${label} API error (${status}): ${body}`;
548
844
  }
549
- formatMessage(msg, oSeries) {
550
- const out = {
551
- role: msg.role === "system" && oSeries ? "developer" : msg.role,
552
- content: msg.content
845
+ toUsage(raw) {
846
+ if (!raw) return void 0;
847
+ return {
848
+ prompt_tokens: raw.prompt_tokens,
849
+ completion_tokens: raw.completion_tokens,
850
+ total_tokens: raw.total_tokens,
851
+ reasoning_tokens: raw.completion_tokens_details?.reasoning_tokens,
852
+ cached_tokens: raw.prompt_tokens_details?.cached_tokens
553
853
  };
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
854
  }
855
+ computeCost(model, usage, reportedCost) {
856
+ const p = this.profile.pricing;
857
+ if (p.kind === "zero") return 0;
858
+ if (!usage) return void 0;
859
+ switch (p.kind) {
860
+ case "unknown":
861
+ return void 0;
862
+ case "from-response":
863
+ return typeof reportedCost === "number" && Number.isFinite(reportedCost) ? reportedCost : void 0;
864
+ case "table": {
865
+ const c = priceFromTable(
866
+ p.table,
867
+ model,
868
+ usage.prompt_tokens,
869
+ usage.completion_tokens,
870
+ usage.cached_tokens
871
+ );
872
+ return c !== void 0 && Number.isFinite(c) ? c : void 0;
873
+ }
874
+ default: {
875
+ const _exhaustive = p;
876
+ throw new Error(`Unhandled pricing source: ${JSON.stringify(_exhaustive)}`);
877
+ }
878
+ }
879
+ }
880
+ parseResponse(json, model) {
881
+ const message = json.choices?.[0]?.message ?? {};
882
+ let content = normalizeMessageContent(message.content);
883
+ let thinking;
884
+ let roundTrip;
885
+ switch (this.profile.reasoning.capture) {
886
+ case "reasoning_content":
887
+ if (typeof message.reasoning_content === "string") {
888
+ thinking = message.reasoning_content;
889
+ roundTrip = {
890
+ provider: this.name,
891
+ field: "reasoning_content",
892
+ value: message.reasoning_content
893
+ };
894
+ }
895
+ break;
896
+ case "reasoning":
897
+ if (typeof message.reasoning === "string") {
898
+ thinking = message.reasoning;
899
+ roundTrip = { provider: this.name, field: "reasoning", value: message.reasoning };
900
+ }
901
+ break;
902
+ case "reasoning_details":
903
+ if (typeof message.reasoning === "string") thinking = message.reasoning;
904
+ if (message.reasoning_details !== void 0) {
905
+ roundTrip = {
906
+ provider: this.name,
907
+ field: "reasoning_details",
908
+ value: message.reasoning_details
909
+ };
910
+ }
911
+ break;
912
+ case "think_tags": {
913
+ const ex = extractThinkTags(content);
914
+ content = ex.content;
915
+ if (ex.thinking) thinking = ex.thinking;
916
+ break;
917
+ }
918
+ case "none":
919
+ break;
920
+ default: {
921
+ const _exhaustive = this.profile.reasoning.capture;
922
+ throw new Error(`Unhandled reasoning capture: ${String(_exhaustive)}`);
923
+ }
924
+ }
925
+ const usage = this.toUsage(json.usage);
926
+ const cost = this.computeCost(model, usage, json.usage?.cost);
927
+ return {
928
+ content,
929
+ thinking_content: thinking || void 0,
930
+ tool_calls: message.tool_calls?.map((tc) => ({
931
+ id: tc.id,
932
+ type: "function",
933
+ function: { name: tc.function.name, arguments: tc.function.arguments }
934
+ })),
935
+ usage,
936
+ cost,
937
+ providerMetadata: this.roundTripMetadata(roundTrip)
938
+ };
939
+ }
940
+ roundTripMetadata(rt) {
941
+ if (!rt || this.profile.reasoning.roundTrip === void 0 || this.profile.reasoning.roundTrip === "none") {
942
+ return void 0;
943
+ }
944
+ return rt.value === void 0 || rt.value === null ? void 0 : { openaiCompatReasoning: rt };
945
+ }
946
+ // ---------------------------------------------------------------------------
947
+ // SSE streaming
948
+ // ---------------------------------------------------------------------------
559
949
  async *parseSSEStream(body, model) {
560
950
  const reader = body.getReader();
561
951
  const decoder = new TextDecoder();
562
952
  let buffer = "";
563
953
  let usageData;
954
+ const capture = this.profile.reasoning.capture;
955
+ const thinkScanner = capture === "think_tags" ? new ThinkTagScanner() : void 0;
956
+ let reasoningText = "";
957
+ const reasoningDetails = [];
564
958
  const indexToId = /* @__PURE__ */ new Map();
959
+ const pendingToolCalls = /* @__PURE__ */ new Map();
960
+ const flushPendingToolCalls = () => {
961
+ const out = [];
962
+ for (const [index, buf] of pendingToolCalls) {
963
+ out.push({
964
+ type: "tool_call_delta",
965
+ id: `__toolcall_${index}`,
966
+ name: buf.name,
967
+ arguments: buf.arguments
968
+ });
969
+ }
970
+ pendingToolCalls.clear();
971
+ return out;
972
+ };
973
+ const makeDone = () => {
974
+ const usage = this.toUsage(usageData);
975
+ let providerMetadata;
976
+ if (this.profile.reasoning.roundTrip === "on-tool-call-turns") {
977
+ if (capture === "reasoning_details" && reasoningDetails.length > 0) {
978
+ providerMetadata = {
979
+ openaiCompatReasoning: {
980
+ provider: this.name,
981
+ field: "reasoning_details",
982
+ value: reasoningDetails
983
+ }
984
+ };
985
+ } else if ((capture === "reasoning_content" || capture === "reasoning") && reasoningText) {
986
+ providerMetadata = {
987
+ openaiCompatReasoning: { provider: this.name, field: capture, value: reasoningText }
988
+ };
989
+ }
990
+ }
991
+ return {
992
+ type: "done",
993
+ usage,
994
+ cost: this.computeCost(model, usage, usageData?.cost),
995
+ providerMetadata
996
+ };
997
+ };
565
998
  try {
566
- while (true) {
999
+ for (; ; ) {
567
1000
  const { done, value } = await reader.read();
568
1001
  if (done) break;
569
1002
  buffer += decoder.decode(value, { stream: true });
@@ -573,47 +1006,76 @@ var OpenAIProvider = class {
573
1006
  const trimmed = line.trim();
574
1007
  if (!trimmed || trimmed.startsWith(":")) continue;
575
1008
  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
- };
1009
+ if (thinkScanner) {
1010
+ const flushed = thinkScanner.flush();
1011
+ if (flushed.text) yield { type: "text_delta", content: flushed.text };
1012
+ if (flushed.thinking) yield { type: "thinking_delta", content: flushed.thinking };
1013
+ }
1014
+ yield* flushPendingToolCalls();
1015
+ yield makeDone();
586
1016
  return;
587
1017
  }
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) {
1018
+ if (!trimmed.startsWith("data: ")) continue;
1019
+ let parsed;
1020
+ try {
1021
+ parsed = JSON.parse(trimmed.slice(6));
1022
+ } catch {
1023
+ continue;
1024
+ }
1025
+ if (parsed.error) {
1026
+ const message = typeof parsed.error.message === "string" ? parsed.error.message : `${this.profile.label ?? this.profile.name} stream error`;
1027
+ throw new ProviderError({
1028
+ provider: this.name,
1029
+ status: 0,
1030
+ retryable: false,
1031
+ message,
1032
+ body: JSON.stringify(parsed.error)
1033
+ });
1034
+ }
1035
+ if (parsed.usage) {
1036
+ usageData = parsed.usage;
1037
+ if (!parsed.choices || parsed.choices.length === 0) continue;
1038
+ }
1039
+ const delta = parsed.choices?.[0]?.delta;
1040
+ if (!delta) continue;
1041
+ const deltaReasoning = capture === "reasoning_content" ? delta.reasoning_content : capture === "reasoning" || capture === "reasoning_details" ? delta.reasoning : void 0;
1042
+ if (typeof deltaReasoning === "string" && deltaReasoning.length > 0) {
1043
+ reasoningText += deltaReasoning;
1044
+ yield { type: "thinking_delta", content: deltaReasoning };
1045
+ }
1046
+ if (capture === "reasoning_details" && Array.isArray(delta.reasoning_details)) {
1047
+ reasoningDetails.push(...delta.reasoning_details);
1048
+ }
1049
+ if (typeof delta.content === "string" && delta.content.length > 0) {
1050
+ if (thinkScanner) {
1051
+ const { text, thinking } = thinkScanner.push(delta.content);
1052
+ if (text) yield { type: "text_delta", content: text };
1053
+ if (thinking) yield { type: "thinking_delta", content: thinking };
1054
+ } else {
609
1055
  yield { type: "text_delta", content: delta.content };
610
1056
  }
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}`;
1057
+ }
1058
+ if (delta.tool_calls) {
1059
+ for (const tc of delta.tool_calls) {
1060
+ if (tc.id) indexToId.set(tc.index, tc.id);
1061
+ const id = indexToId.get(tc.index);
1062
+ if (id === void 0) {
1063
+ const buf2 = pendingToolCalls.get(tc.index) ?? { arguments: "" };
1064
+ if (tc.function?.name) buf2.name = tc.function.name;
1065
+ if (tc.function?.arguments) buf2.arguments += tc.function.arguments;
1066
+ pendingToolCalls.set(tc.index, buf2);
1067
+ continue;
1068
+ }
1069
+ const buf = pendingToolCalls.get(tc.index);
1070
+ if (buf) {
1071
+ pendingToolCalls.delete(tc.index);
1072
+ yield {
1073
+ type: "tool_call_delta",
1074
+ id,
1075
+ name: buf.name ?? tc.function?.name,
1076
+ arguments: buf.arguments + (tc.function?.arguments ?? "")
1077
+ };
1078
+ } else {
617
1079
  yield {
618
1080
  type: "tool_call_delta",
619
1081
  id,
@@ -625,58 +1087,176 @@ var OpenAIProvider = class {
625
1087
  }
626
1088
  }
627
1089
  }
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
- };
1090
+ if (thinkScanner) {
1091
+ const flushed = thinkScanner.flush();
1092
+ if (flushed.text) yield { type: "text_delta", content: flushed.text };
1093
+ if (flushed.thinking) yield { type: "thinking_delta", content: flushed.thinking };
1094
+ }
1095
+ yield* flushPendingToolCalls();
1096
+ throw new ProviderError({
1097
+ provider: this.name,
1098
+ status: 0,
1099
+ retryable: true,
1100
+ message: `${this.profile.label ?? this.profile.name} stream ended before [DONE]`
1101
+ });
638
1102
  } finally {
639
1103
  reader.releaseLock();
640
1104
  }
641
1105
  }
642
1106
  };
643
1107
 
1108
+ // src/providers/openai.ts
1109
+ var OPENAI_PRICING = {
1110
+ // gpt-4o era — cache reads at 50% of input rate
1111
+ "gpt-4o": [25e-7, 1e-5, 0.5],
1112
+ "gpt-4o-mini": [15e-8, 6e-7, 0.5],
1113
+ "gpt-4-turbo": [1e-5, 3e-5, 0.5],
1114
+ "gpt-4": [3e-5, 6e-5, 0.5],
1115
+ "gpt-3.5-turbo": [5e-7, 15e-7, 0.5],
1116
+ o1: [15e-6, 6e-5, 0.5],
1117
+ "o1-mini": [3e-6, 12e-6, 0.5],
1118
+ "o1-pro": [15e-5, 6e-4, 0.5],
1119
+ // gpt-4.1 / o3 / o4 era — cache reads at 25% of input rate
1120
+ "gpt-4.1": [2e-6, 8e-6, 0.25],
1121
+ "gpt-4.1-mini": [4e-7, 16e-7, 0.25],
1122
+ "gpt-4.1-nano": [1e-7, 4e-7, 0.25],
1123
+ o3: [1e-5, 4e-5, 0.25],
1124
+ "o3-mini": [11e-7, 44e-7, 0.25],
1125
+ "o3-pro": [2e-5, 8e-5, 0.25],
1126
+ "o4-mini": [11e-7, 44e-7, 0.25],
1127
+ // gpt-5 era — cache reads at 10% of input rate
1128
+ "gpt-5": [125e-8, 1e-5, 0.1],
1129
+ "gpt-5-mini": [25e-8, 2e-6, 0.1],
1130
+ "gpt-5-nano": [5e-8, 4e-7, 0.1],
1131
+ "gpt-5.1": [125e-8, 1e-5, 0.1],
1132
+ "gpt-5.2": [175e-8, 14e-6, 0.1],
1133
+ "gpt-5.3": [175e-8, 14e-6, 0.1],
1134
+ "gpt-5.4": [25e-7, 15e-6, 0.1],
1135
+ "gpt-5.4-pro": [3e-5, 18e-5, 0.1],
1136
+ // gpt-5.5 era (flagship released 2026-04) — cache reads at 10% of input rate
1137
+ "gpt-5.5": [5e-6, 3e-5, 0.1],
1138
+ "gpt-5.5-pro": [3e-5, 18e-5, 0.1]
1139
+ };
1140
+ function estimateOpenAICost(model, promptTokens, completionTokens, cachedTokens) {
1141
+ return priceFromTable(OPENAI_PRICING, model, promptTokens, completionTokens, cachedTokens);
1142
+ }
1143
+ function isOSeriesModel(model) {
1144
+ return /^(o1|o3|o4-mini)/.test(model);
1145
+ }
1146
+ function supportsReasoningEffort(model) {
1147
+ return isOSeriesModel(model) || /^gpt-5/.test(model);
1148
+ }
1149
+ function supportsReasoningNone(model) {
1150
+ return /^gpt-5\.[1-9]/.test(model);
1151
+ }
1152
+ function supportsXhigh(model) {
1153
+ return /^gpt-5\.([2-9]|\d{2,})/.test(model);
1154
+ }
1155
+ function clampReasoningEffort(model, effort) {
1156
+ if (model.startsWith("gpt-5-pro")) return "high";
1157
+ if (effort === "none" && !supportsReasoningNone(model)) return "minimal";
1158
+ if (effort === "xhigh" && !supportsXhigh(model)) return "high";
1159
+ return effort;
1160
+ }
1161
+ function effortToReasoningEffort(effort) {
1162
+ return effort === "max" ? "xhigh" : effort;
1163
+ }
1164
+ function budgetToReasoningEffort(budget) {
1165
+ if (budget <= 1024) return "low";
1166
+ if (budget <= 8192) return "medium";
1167
+ return "high";
1168
+ }
1169
+ var openaiReasoningEmit = (body, resolved, model) => {
1170
+ const oSeries = isOSeriesModel(model);
1171
+ const reasoningCapable = supportsReasoningEffort(model);
1172
+ const { thinkingBudget, thinkingDisabled, activeEffort, hasBudgetOverride } = resolved;
1173
+ let wireEffort;
1174
+ if (reasoningCapable) {
1175
+ if (hasBudgetOverride) {
1176
+ wireEffort = clampReasoningEffort(model, budgetToReasoningEffort(thinkingBudget));
1177
+ } else if (!thinkingDisabled && activeEffort) {
1178
+ wireEffort = clampReasoningEffort(model, effortToReasoningEffort(activeEffort));
1179
+ } else if (thinkingDisabled) {
1180
+ wireEffort = clampReasoningEffort(model, "none");
1181
+ }
1182
+ }
1183
+ if (wireEffort) body.reasoning_effort = wireEffort;
1184
+ return { stripTemperature: oSeries || reasoningCapable && wireEffort !== void 0 };
1185
+ };
1186
+ var OPENAI_PROFILE = {
1187
+ name: "openai",
1188
+ label: "OpenAI",
1189
+ defaultBaseUrl: "https://api.openai.com/v1",
1190
+ envApiKey: "OPENAI_API_KEY",
1191
+ envBaseUrl: "OPENAI_BASE_URL",
1192
+ pricing: { kind: "table", table: OPENAI_PRICING },
1193
+ reasoning: { emit: openaiReasoningEmit, capture: "none" },
1194
+ roleFor: (role, model) => role === "system" && isOSeriesModel(model) ? "developer" : role,
1195
+ maxTokensField: "max_completion_tokens",
1196
+ parallelToolCalls: (model) => !isOSeriesModel(model)
1197
+ };
1198
+ var OpenAIProvider = class extends OpenAICompatibleProvider {
1199
+ constructor(options = {}) {
1200
+ super({ profile: OPENAI_PROFILE, ...options });
1201
+ }
1202
+ };
1203
+
644
1204
  // src/providers/openai-responses.ts
645
1205
  var OpenAIResponsesProvider = class {
646
1206
  name = "openai-responses";
1207
+ /** The Responses API honors `json_schema` (structured outputs) natively. */
1208
+ nativeStructuredOutputSupport() {
1209
+ return "schema";
1210
+ }
647
1211
  baseUrl;
648
- apiKey;
1212
+ apiKeySource;
649
1213
  governor;
650
1214
  constructor(options = {}) {
651
- this.apiKey = options.apiKey ?? process.env.OPENAI_API_KEY ?? "";
1215
+ this.apiKeySource = options.apiKey ?? process.env.OPENAI_API_KEY ?? "";
652
1216
  this.baseUrl = (options.baseUrl ?? process.env.OPENAI_BASE_URL ?? "https://api.openai.com/v1").replace(/\/$/, "");
653
1217
  this.governor = options.rateLimit ? new RateLimiter(options.rateLimit) : void 0;
654
- if (!this.apiKey) {
1218
+ if (typeof this.apiKeySource === "string" && !this.apiKeySource) {
655
1219
  throw new Error("OpenAI API key is required. Set OPENAI_API_KEY or pass apiKey in options.");
656
1220
  }
657
1221
  }
1222
+ /** Resolve the API key for one request (supports an expiring-token callback). */
1223
+ async resolveKey() {
1224
+ const key = await resolveApiKey(this.apiKeySource);
1225
+ if (!key) {
1226
+ throw new Error("OpenAI API key is required. Set OPENAI_API_KEY or pass apiKey in options.");
1227
+ }
1228
+ return key;
1229
+ }
1230
+ /** Build request headers from a resolved key (used by both chat and stream). */
1231
+ buildHeaders(apiKey) {
1232
+ return { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` };
1233
+ }
658
1234
  // ---------------------------------------------------------------------------
659
1235
  // chat - non-streaming
660
1236
  // ---------------------------------------------------------------------------
661
1237
  async chat(messages, options) {
1238
+ const headers = this.buildHeaders(await this.resolveKey());
662
1239
  const body = this.buildRequestBody(messages, options, false);
663
1240
  const res = await fetchWithRetry(
664
1241
  `${this.baseUrl}/responses`,
665
1242
  {
666
1243
  method: "POST",
667
- headers: {
668
- "Content-Type": "application/json",
669
- Authorization: `Bearer ${this.apiKey}`
670
- },
1244
+ headers,
671
1245
  body: JSON.stringify(body),
672
1246
  signal: options.signal
673
1247
  },
674
- { governor: this.governor }
1248
+ { governor: this.governor, provider: this.name }
675
1249
  );
676
1250
  if (!res.ok) {
677
1251
  const errorBody = await res.text();
678
1252
  const message = this.extractErrorMessage(errorBody, res.status);
679
- throw new Error(message);
1253
+ throw buildProviderError({
1254
+ provider: this.name,
1255
+ status: res.status,
1256
+ headers: res.headers,
1257
+ message,
1258
+ body: errorBody
1259
+ });
680
1260
  }
681
1261
  const json = await res.json();
682
1262
  return this.parseResponse(json, options.model);
@@ -685,24 +1265,28 @@ var OpenAIResponsesProvider = class {
685
1265
  // stream - SSE streaming
686
1266
  // ---------------------------------------------------------------------------
687
1267
  async *stream(messages, options) {
1268
+ const headers = this.buildHeaders(await this.resolveKey());
688
1269
  const body = this.buildRequestBody(messages, options, true);
689
1270
  const res = await fetchWithRetry(
690
1271
  `${this.baseUrl}/responses`,
691
1272
  {
692
1273
  method: "POST",
693
- headers: {
694
- "Content-Type": "application/json",
695
- Authorization: `Bearer ${this.apiKey}`
696
- },
1274
+ headers,
697
1275
  body: JSON.stringify(body),
698
1276
  signal: options.signal
699
1277
  },
700
- { governor: this.governor }
1278
+ { governor: this.governor, provider: this.name }
701
1279
  );
702
1280
  if (!res.ok) {
703
1281
  const errorBody = await res.text();
704
1282
  const message = this.extractErrorMessage(errorBody, res.status);
705
- throw new Error(message);
1283
+ throw buildProviderError({
1284
+ provider: this.name,
1285
+ status: res.status,
1286
+ headers: res.headers,
1287
+ message,
1288
+ body: errorBody
1289
+ });
706
1290
  }
707
1291
  if (!res.body) {
708
1292
  throw new Error("OpenAI Responses stream has no body");
@@ -991,7 +1575,12 @@ var OpenAIResponsesProvider = class {
991
1575
  }
992
1576
  case "response.failed": {
993
1577
  const errorMsg = data.response?.error?.message ?? data.response?.status_details?.error?.message ?? "Unknown error";
994
- throw new Error(`OpenAI Responses API error: ${errorMsg}`);
1578
+ throw new ProviderError({
1579
+ provider: this.name,
1580
+ status: 0,
1581
+ retryable: false,
1582
+ message: `OpenAI Responses API error: ${errorMsg}`
1583
+ });
995
1584
  }
996
1585
  default:
997
1586
  return null;
@@ -1045,7 +1634,7 @@ function estimateAnthropicCost(model, inputTokens, outputTokens, cacheReadTokens
1045
1634
  }
1046
1635
  }
1047
1636
  }
1048
- if (!pricing) return 0;
1637
+ if (!pricing) return void 0;
1049
1638
  const [inputRate, outputRate] = pricing;
1050
1639
  const cacheRead = cacheReadTokens ?? 0;
1051
1640
  const cacheWrite = cacheWriteTokens ?? 0;
@@ -1079,63 +1668,90 @@ function clampAnthropicEffort(model, effort) {
1079
1668
  }
1080
1669
  var AnthropicProvider = class {
1081
1670
  name = "anthropic";
1671
+ /** Anthropic ignores native `json_schema` structurally — Axl uses a system
1672
+ * prompt JSON instruction + client-side Zod validation (see buildRequest). */
1673
+ nativeStructuredOutputSupport() {
1674
+ return "unsupported";
1675
+ }
1082
1676
  baseUrl;
1083
- apiKey;
1084
- currentModel;
1677
+ apiKeySource;
1085
1678
  governor;
1086
1679
  constructor(options = {}) {
1087
- this.apiKey = options.apiKey ?? process.env.ANTHROPIC_API_KEY ?? "";
1680
+ this.apiKeySource = options.apiKey ?? process.env.ANTHROPIC_API_KEY ?? "";
1088
1681
  this.baseUrl = (options.baseUrl ?? "https://api.anthropic.com/v1").replace(/\/$/, "");
1089
1682
  this.governor = options.rateLimit ? new RateLimiter(options.rateLimit) : void 0;
1090
- if (!this.apiKey) {
1683
+ if (typeof this.apiKeySource === "string" && !this.apiKeySource) {
1684
+ throw new Error(
1685
+ "Anthropic API key is required. Set ANTHROPIC_API_KEY or pass apiKey in options."
1686
+ );
1687
+ }
1688
+ }
1689
+ /** Resolve the API key for one request (supports an expiring-token callback). */
1690
+ async resolveKey() {
1691
+ const key = await resolveApiKey(this.apiKeySource);
1692
+ if (!key) {
1091
1693
  throw new Error(
1092
1694
  "Anthropic API key is required. Set ANTHROPIC_API_KEY or pass apiKey in options."
1093
1695
  );
1094
1696
  }
1697
+ return key;
1095
1698
  }
1096
1699
  // ---------------------------------------------------------------------------
1097
1700
  // chat - non-streaming completion
1098
1701
  // ---------------------------------------------------------------------------
1099
1702
  async chat(messages, options) {
1100
- this.currentModel = options.model;
1703
+ const headers = this.buildHeaders(await this.resolveKey());
1101
1704
  const body = this.buildRequestBody(messages, options, false);
1102
1705
  const res = await fetchWithRetry(
1103
1706
  `${this.baseUrl}/messages`,
1104
1707
  {
1105
1708
  method: "POST",
1106
- headers: this.buildHeaders(),
1709
+ headers,
1107
1710
  body: JSON.stringify(body),
1108
1711
  signal: options.signal
1109
1712
  },
1110
- { governor: this.governor }
1713
+ { governor: this.governor, provider: this.name }
1111
1714
  );
1112
1715
  if (!res.ok) {
1113
1716
  const errorBody = await res.text();
1114
1717
  const message = this.extractErrorMessage(errorBody, res.status);
1115
- throw new Error(message);
1718
+ throw buildProviderError({
1719
+ provider: this.name,
1720
+ status: res.status,
1721
+ headers: res.headers,
1722
+ message,
1723
+ body: errorBody
1724
+ });
1116
1725
  }
1117
1726
  const json = await res.json();
1118
- return this.parseResponse(json);
1727
+ return this.parseResponse(json, options.model);
1119
1728
  }
1120
1729
  // ---------------------------------------------------------------------------
1121
1730
  // stream - SSE streaming completion
1122
1731
  // ---------------------------------------------------------------------------
1123
1732
  async *stream(messages, options) {
1733
+ const headers = this.buildHeaders(await this.resolveKey());
1124
1734
  const body = this.buildRequestBody(messages, options, true);
1125
1735
  const res = await fetchWithRetry(
1126
1736
  `${this.baseUrl}/messages`,
1127
1737
  {
1128
1738
  method: "POST",
1129
- headers: this.buildHeaders(),
1739
+ headers,
1130
1740
  body: JSON.stringify(body),
1131
1741
  signal: options.signal
1132
1742
  },
1133
- { governor: this.governor }
1743
+ { governor: this.governor, provider: this.name }
1134
1744
  );
1135
1745
  if (!res.ok) {
1136
1746
  const errorBody = await res.text();
1137
1747
  const message = this.extractErrorMessage(errorBody, res.status);
1138
- throw new Error(message);
1748
+ throw buildProviderError({
1749
+ provider: this.name,
1750
+ status: res.status,
1751
+ headers: res.headers,
1752
+ message,
1753
+ body: errorBody
1754
+ });
1139
1755
  }
1140
1756
  if (!res.body) {
1141
1757
  throw new Error("Anthropic stream response has no body");
@@ -1145,10 +1761,10 @@ var AnthropicProvider = class {
1145
1761
  // ---------------------------------------------------------------------------
1146
1762
  // Internal: request building
1147
1763
  // ---------------------------------------------------------------------------
1148
- buildHeaders() {
1764
+ buildHeaders(apiKey) {
1149
1765
  return {
1150
1766
  "Content-Type": "application/json",
1151
- "x-api-key": this.apiKey,
1767
+ "x-api-key": apiKey,
1152
1768
  "anthropic-version": ANTHROPIC_API_VERSION
1153
1769
  };
1154
1770
  }
@@ -1333,7 +1949,7 @@ ${jsonInstruction}` : jsonInstruction;
1333
1949
  // ---------------------------------------------------------------------------
1334
1950
  // Internal: response parsing
1335
1951
  // ---------------------------------------------------------------------------
1336
- parseResponse(json) {
1952
+ parseResponse(json, model) {
1337
1953
  let content = "";
1338
1954
  let thinkingContent = "";
1339
1955
  const toolCalls = [];
@@ -1362,13 +1978,7 @@ ${jsonInstruction}` : jsonInstruction;
1362
1978
  total_tokens: totalInput + json.usage.output_tokens,
1363
1979
  cached_tokens: cacheRead > 0 ? cacheRead : void 0
1364
1980
  } : 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;
1981
+ const cost = json.usage ? estimateAnthropicCost(model, totalInput, json.usage.output_tokens, cacheRead, cacheWrite) : void 0;
1372
1982
  return {
1373
1983
  content,
1374
1984
  thinking_content: thinkingContent || void 0,
@@ -1570,7 +2180,7 @@ function estimateGeminiCost(model, inputTokens, outputTokens, cachedTokens) {
1570
2180
  }
1571
2181
  }
1572
2182
  }
1573
- if (!pricing) return 0;
2183
+ if (!pricing) return void 0;
1574
2184
  const [inputRate, outputRate] = pricing;
1575
2185
  const cached = cachedTokens ?? 0;
1576
2186
  const inputCost = (inputTokens - cached) * inputRate + cached * inputRate * 0.1;
@@ -1615,40 +2225,61 @@ function warnGemini3xEffortNone(model) {
1615
2225
  }
1616
2226
  var GeminiProvider = class {
1617
2227
  name = "google";
2228
+ /** Gemini accepts a `responseSchema` but `sanitizeSchemaForGemini` strips
2229
+ * keywords it doesn't support (`$ref`/`$defs`/`additionalProperties`/…), so a
2230
+ * derived schema can lose constraints — lossy, not faithful. */
2231
+ nativeStructuredOutputSupport() {
2232
+ return "lossy";
2233
+ }
1618
2234
  baseUrl;
1619
- apiKey;
2235
+ apiKeySource;
1620
2236
  callCounter = 0;
1621
2237
  governor;
1622
2238
  constructor(options = {}) {
1623
- this.apiKey = options.apiKey ?? process.env.GOOGLE_API_KEY ?? process.env.GEMINI_API_KEY ?? "";
2239
+ this.apiKeySource = options.apiKey ?? process.env.GOOGLE_API_KEY ?? process.env.GEMINI_API_KEY ?? "";
1624
2240
  this.baseUrl = (options.baseUrl ?? "https://generativelanguage.googleapis.com/v1beta").replace(
1625
2241
  /\/$/,
1626
2242
  ""
1627
2243
  );
1628
2244
  this.governor = options.rateLimit ? new RateLimiter(options.rateLimit) : void 0;
1629
- if (!this.apiKey) {
2245
+ if (typeof this.apiKeySource === "string" && !this.apiKeySource) {
1630
2246
  throw new Error("Google API key is required. Set GOOGLE_API_KEY or pass apiKey in options.");
1631
2247
  }
1632
2248
  }
2249
+ /** Resolve the API key for one request (supports an expiring-token callback). */
2250
+ async resolveKey() {
2251
+ const key = await resolveApiKey(this.apiKeySource);
2252
+ if (!key) {
2253
+ throw new Error("Google API key is required. Set GOOGLE_API_KEY or pass apiKey in options.");
2254
+ }
2255
+ return key;
2256
+ }
1633
2257
  // ---------------------------------------------------------------------------
1634
2258
  // chat - non-streaming completion
1635
2259
  // ---------------------------------------------------------------------------
1636
2260
  async chat(messages, options) {
2261
+ const headers = this.buildHeaders(await this.resolveKey());
1637
2262
  const body = this.buildRequestBody(messages, options);
1638
2263
  const res = await fetchWithRetry(
1639
2264
  `${this.baseUrl}/models/${options.model}:generateContent`,
1640
2265
  {
1641
2266
  method: "POST",
1642
- headers: this.buildHeaders(),
2267
+ headers,
1643
2268
  body: JSON.stringify(body),
1644
2269
  signal: options.signal
1645
2270
  },
1646
- { governor: this.governor }
2271
+ { governor: this.governor, provider: this.name }
1647
2272
  );
1648
2273
  if (!res.ok) {
1649
2274
  const errorBody = await res.text();
1650
2275
  const message = this.extractErrorMessage(errorBody, res.status);
1651
- throw new Error(message);
2276
+ throw buildProviderError({
2277
+ provider: this.name,
2278
+ status: res.status,
2279
+ headers: res.headers,
2280
+ message,
2281
+ body: errorBody
2282
+ });
1652
2283
  }
1653
2284
  const json = await res.json();
1654
2285
  return this.parseResponse(json, options.model);
@@ -1657,21 +2288,28 @@ var GeminiProvider = class {
1657
2288
  // stream - SSE streaming completion
1658
2289
  // ---------------------------------------------------------------------------
1659
2290
  async *stream(messages, options) {
2291
+ const headers = this.buildHeaders(await this.resolveKey());
1660
2292
  const body = this.buildRequestBody(messages, options);
1661
2293
  const res = await fetchWithRetry(
1662
2294
  `${this.baseUrl}/models/${options.model}:streamGenerateContent?alt=sse`,
1663
2295
  {
1664
2296
  method: "POST",
1665
- headers: this.buildHeaders(),
2297
+ headers,
1666
2298
  body: JSON.stringify(body),
1667
2299
  signal: options.signal
1668
2300
  },
1669
- { governor: this.governor }
2301
+ { governor: this.governor, provider: this.name }
1670
2302
  );
1671
2303
  if (!res.ok) {
1672
2304
  const errorBody = await res.text();
1673
2305
  const message = this.extractErrorMessage(errorBody, res.status);
1674
- throw new Error(message);
2306
+ throw buildProviderError({
2307
+ provider: this.name,
2308
+ status: res.status,
2309
+ headers: res.headers,
2310
+ message,
2311
+ body: errorBody
2312
+ });
1675
2313
  }
1676
2314
  if (!res.body) {
1677
2315
  throw new Error("Gemini stream response has no body");
@@ -1681,10 +2319,10 @@ var GeminiProvider = class {
1681
2319
  // ---------------------------------------------------------------------------
1682
2320
  // Internal: request building
1683
2321
  // ---------------------------------------------------------------------------
1684
- buildHeaders() {
2322
+ buildHeaders(apiKey) {
1685
2323
  return {
1686
2324
  "Content-Type": "application/json",
1687
- "x-goog-api-key": this.apiKey
2325
+ "x-goog-api-key": apiKey
1688
2326
  };
1689
2327
  }
1690
2328
  extractErrorMessage(body, status) {
@@ -2041,6 +2679,203 @@ var GeminiProvider = class {
2041
2679
  }
2042
2680
  };
2043
2681
 
2682
+ // src/providers/profiles/openrouter.ts
2683
+ var OPENROUTER_PROFILE = {
2684
+ name: "openrouter",
2685
+ label: "OpenRouter",
2686
+ defaultBaseUrl: "https://openrouter.ai/api/v1",
2687
+ envApiKey: "OPENROUTER_API_KEY",
2688
+ envBaseUrl: "OPENROUTER_BASE_URL",
2689
+ pricing: { kind: "from-response" },
2690
+ // Ask OpenRouter to report per-call cost in usage.cost.
2691
+ requestDefaults: { usage: { include: true } },
2692
+ reasoning: {
2693
+ // OpenRouter's effort vocabulary is low|medium|high; xhigh/max clamp to high.
2694
+ emit: reasoningObjectEmit(
2695
+ (effort) => effort === "xhigh" || effort === "max" ? "high" : effort
2696
+ ),
2697
+ capture: "reasoning_details",
2698
+ roundTrip: "on-tool-call-turns"
2699
+ },
2700
+ // OpenRouter normalizes to max_tokens across its backends.
2701
+ maxTokensField: "max_tokens"
2702
+ };
2703
+
2704
+ // src/providers/profiles/azure.ts
2705
+ var AZURE_PROFILE = {
2706
+ name: "azure",
2707
+ label: "Azure OpenAI",
2708
+ // Never actually used — requireExplicitBaseUrl forces a real value first.
2709
+ defaultBaseUrl: "https://YOUR-RESOURCE.openai.azure.com/openai/v1",
2710
+ requireExplicitBaseUrl: true,
2711
+ envApiKey: "AZURE_OPENAI_API_KEY",
2712
+ envBaseUrl: "AZURE_OPENAI_BASE_URL",
2713
+ authHeader: "api-key",
2714
+ pricing: { kind: "table", table: OPENAI_PRICING },
2715
+ reasoning: { emit: openaiReasoningEmit, capture: "none" },
2716
+ roleFor: (role, model) => role === "system" && isOSeriesModel(model) ? "developer" : role,
2717
+ maxTokensField: "max_completion_tokens",
2718
+ parallelToolCalls: (model) => !isOSeriesModel(model)
2719
+ };
2720
+
2721
+ // src/providers/profiles/xai.ts
2722
+ var acceptsReasoningEffort = (model) => model.startsWith("grok-3-mini");
2723
+ var isReasoningModel = (model) => /^grok-(3-mini|4)/.test(model);
2724
+ var XAI_PROFILE = {
2725
+ name: "xai",
2726
+ label: "xAI",
2727
+ defaultBaseUrl: "https://api.x.ai/v1",
2728
+ envApiKey: "XAI_API_KEY",
2729
+ envBaseUrl: "XAI_BASE_URL",
2730
+ pricing: { kind: "unknown" },
2731
+ reasoning: {
2732
+ emit: reasoningEffortEmit((resolved, model) => {
2733
+ if (!acceptsReasoningEffort(model)) return void 0;
2734
+ if (resolved.thinkingDisabled || !resolved.activeEffort) return void 0;
2735
+ return resolved.activeEffort === "low" ? "low" : "high";
2736
+ }),
2737
+ capture: "reasoning"
2738
+ },
2739
+ capabilities: {
2740
+ forbiddenParams: (model) => isReasoningModel(model) ? ["stop", "presence_penalty", "frequency_penalty"] : []
2741
+ }
2742
+ };
2743
+
2744
+ // src/providers/profiles/deepseek.ts
2745
+ var isReasoner = (model) => /reasoner|v4/i.test(model);
2746
+ var deepseekEmit = (_body, _resolved, model) => ({
2747
+ stripTemperature: isReasoner(model)
2748
+ });
2749
+ var DEEPSEEK_PROFILE = {
2750
+ name: "deepseek",
2751
+ label: "DeepSeek",
2752
+ defaultBaseUrl: "https://api.deepseek.com/v1",
2753
+ envApiKey: "DEEPSEEK_API_KEY",
2754
+ envBaseUrl: "DEEPSEEK_BASE_URL",
2755
+ pricing: { kind: "unknown" },
2756
+ reasoning: {
2757
+ emit: deepseekEmit,
2758
+ capture: "reasoning_content",
2759
+ roundTrip: "on-tool-call-turns"
2760
+ },
2761
+ capabilities: {
2762
+ supportsJsonSchema: false,
2763
+ forbiddenParams: (model) => isReasoner(model) ? ["top_p", "presence_penalty", "frequency_penalty"] : []
2764
+ }
2765
+ };
2766
+
2767
+ // src/providers/profiles/mistral.ts
2768
+ var acceptsReasoningEffort2 = (model) => /^mistral-(small|medium)/i.test(model);
2769
+ var MISTRAL_PROFILE = {
2770
+ name: "mistral",
2771
+ label: "Mistral",
2772
+ defaultBaseUrl: "https://api.mistral.ai/v1",
2773
+ envApiKey: "MISTRAL_API_KEY",
2774
+ envBaseUrl: "MISTRAL_BASE_URL",
2775
+ pricing: { kind: "unknown" },
2776
+ reasoning: {
2777
+ emit: reasoningEffortEmit((resolved, model) => {
2778
+ if (!acceptsReasoningEffort2(model)) return void 0;
2779
+ if (resolved.thinkingDisabled || !resolved.activeEffort) return void 0;
2780
+ return "high";
2781
+ }),
2782
+ capture: "reasoning_content"
2783
+ }
2784
+ };
2785
+
2786
+ // src/providers/profiles/groq.ts
2787
+ var isGptOss = (model) => /gpt-oss/i.test(model);
2788
+ var GROQ_PROFILE = {
2789
+ name: "groq",
2790
+ label: "Groq",
2791
+ defaultBaseUrl: "https://api.groq.com/openai/v1",
2792
+ envApiKey: "GROQ_API_KEY",
2793
+ envBaseUrl: "GROQ_BASE_URL",
2794
+ pricing: { kind: "unknown" },
2795
+ reasoning: {
2796
+ emit: reasoningEffortEmit((resolved, model) => {
2797
+ if (!isGptOss(model) || resolved.thinkingDisabled || !resolved.activeEffort) {
2798
+ return void 0;
2799
+ }
2800
+ const e = resolved.activeEffort;
2801
+ return e === "xhigh" || e === "max" ? "high" : e;
2802
+ }),
2803
+ capture: "reasoning"
2804
+ },
2805
+ capabilities: {
2806
+ emitsMessageName: false,
2807
+ // Groq's `response_format: json_schema` is supported only on a subset of
2808
+ // models — the `openai/gpt-oss-*` family accepts it; llama/gemma/etc 400
2809
+ // with "This model does not support response format `json_schema`".
2810
+ // Per-model so `nativeStructuredOutput` engages the native path only where
2811
+ // it works and otherwise downgrades cleanly to `json_object` (verified live).
2812
+ supportsJsonSchema: isGptOss
2813
+ }
2814
+ };
2815
+
2816
+ // src/providers/profiles/bedrock.ts
2817
+ var isGptOss2 = (model) => /gpt-oss/i.test(model);
2818
+ var BEDROCK_PROFILE = {
2819
+ name: "bedrock",
2820
+ label: "AWS Bedrock",
2821
+ // Never used — requireExplicitBaseUrl forces a real (region-specific) value first.
2822
+ defaultBaseUrl: "https://bedrock-mantle.us-east-1.api.aws/v1",
2823
+ requireExplicitBaseUrl: true,
2824
+ envApiKey: "AWS_BEARER_TOKEN_BEDROCK",
2825
+ envBaseUrl: "BEDROCK_BASE_URL",
2826
+ pricing: { kind: "unknown" },
2827
+ reasoning: {
2828
+ emit: reasoningEffortEmit((resolved, model) => {
2829
+ if (!isGptOss2(model) || resolved.thinkingDisabled || !resolved.activeEffort) {
2830
+ return void 0;
2831
+ }
2832
+ const e = resolved.activeEffort;
2833
+ return e === "xhigh" || e === "max" ? "high" : e;
2834
+ }),
2835
+ capture: "reasoning"
2836
+ }
2837
+ };
2838
+
2839
+ // src/providers/profiles/local.ts
2840
+ function localProfile(name, label, port, envApiKey) {
2841
+ return {
2842
+ name,
2843
+ label,
2844
+ defaultBaseUrl: `http://localhost:${port}/v1`,
2845
+ envApiKey,
2846
+ envBaseUrl: `${envApiKey.replace(/_API_KEY$/, "")}_BASE_URL`,
2847
+ allowMissingApiKey: true,
2848
+ pricing: { kind: "zero" },
2849
+ reasoning: { emit: () => {
2850
+ }, capture: "think_tags" },
2851
+ maxTokensField: "max_tokens"
2852
+ };
2853
+ }
2854
+ var OLLAMA_PROFILE = localProfile("ollama", "Ollama", 11434, "OLLAMA_API_KEY");
2855
+ var VLLM_PROFILE = localProfile("vllm", "vLLM", 8e3, "VLLM_API_KEY");
2856
+ var LMSTUDIO_PROFILE = localProfile("lmstudio", "LM Studio", 1234, "LMSTUDIO_API_KEY");
2857
+ var LLAMACPP_PROFILE = localProfile("llamacpp", "llama.cpp", 8080, "LLAMACPP_API_KEY");
2858
+ var SGLANG_PROFILE = localProfile("sglang", "SGLang", 3e4, "SGLANG_API_KEY");
2859
+ var LOCAL_PROFILES = [
2860
+ OLLAMA_PROFILE,
2861
+ VLLM_PROFILE,
2862
+ LMSTUDIO_PROFILE,
2863
+ LLAMACPP_PROFILE,
2864
+ SGLANG_PROFILE
2865
+ ];
2866
+
2867
+ // src/providers/profiles/index.ts
2868
+ var BUILTIN_PROFILES = [
2869
+ OPENROUTER_PROFILE,
2870
+ AZURE_PROFILE,
2871
+ XAI_PROFILE,
2872
+ DEEPSEEK_PROFILE,
2873
+ MISTRAL_PROFILE,
2874
+ GROQ_PROFILE,
2875
+ BEDROCK_PROFILE,
2876
+ ...LOCAL_PROFILES
2877
+ ];
2878
+
2044
2879
  // src/providers/registry.ts
2045
2880
  var builtinFactories = {
2046
2881
  openai: (config) => {
@@ -2080,6 +2915,23 @@ var builtinFactories = {
2080
2915
  });
2081
2916
  }
2082
2917
  };
2918
+ function presetFactory(profile) {
2919
+ return (config) => {
2920
+ const opts = config.providers?.[profile.name] ?? {};
2921
+ return new OpenAICompatibleProvider({
2922
+ profile,
2923
+ apiKey: opts.apiKey,
2924
+ baseUrl: opts.baseUrl,
2925
+ authHeader: opts.authHeader,
2926
+ rateLimit: opts.rateLimit
2927
+ });
2928
+ };
2929
+ }
2930
+ for (const profile of BUILTIN_PROFILES) {
2931
+ if (!(profile.name in builtinFactories)) {
2932
+ builtinFactories[profile.name] = presetFactory(profile);
2933
+ }
2934
+ }
2083
2935
  var ProviderRegistry = class {
2084
2936
  /** Cached provider instances, keyed by provider name */
2085
2937
  instances = /* @__PURE__ */ new Map();
@@ -2197,123 +3049,6 @@ import { AsyncLocalStorage } from "async_hooks";
2197
3049
  import { randomUUID } from "crypto";
2198
3050
  import { z, ZodError } from "zod";
2199
3051
 
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;
2289
- }
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;
2301
- }
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;
2314
- }
2315
- };
2316
-
2317
3052
  // src/partial-json.ts
2318
3053
  var MAX_DEPTH = 256;
2319
3054
  function parsePartialJson(input) {
@@ -2888,6 +3623,14 @@ var COST_BEARING_LEAF_TYPES = [
2888
3623
  "memory_recall"
2889
3624
  ];
2890
3625
  var COST_LEAF_SET = new Set(COST_BEARING_LEAF_TYPES);
3626
+ function hasPositiveTokens(event) {
3627
+ const t = event.tokens;
3628
+ if (!t) return false;
3629
+ return (t.input ?? 0) + (t.output ?? 0) + (t.reasoning ?? 0) > 0;
3630
+ }
3631
+ function isUsableCost(cost) {
3632
+ return typeof cost === "number" && Number.isFinite(cost) && cost >= 0;
3633
+ }
2891
3634
  function eventCostContribution(event) {
2892
3635
  if (event.type === "ask_end") return 0;
2893
3636
  const c = event.cost;
@@ -2896,6 +3639,9 @@ function eventCostContribution(event) {
2896
3639
  function isCostBearingLeaf(event) {
2897
3640
  return COST_LEAF_SET.has(event.type);
2898
3641
  }
3642
+ function isUnpricedLeaf(event) {
3643
+ return event.type !== void 0 && COST_LEAF_SET.has(event.type) && !isUsableCost(event.cost) && hasPositiveTokens(event);
3644
+ }
2899
3645
  function isRootLevel(event) {
2900
3646
  const d = event.depth;
2901
3647
  return (d ?? 0) === 0;
@@ -2944,6 +3690,10 @@ var AXL_EVENT_TYPES = [
2944
3690
  "string_delta",
2945
3691
  // Verification
2946
3692
  "verify",
3693
+ // Schema capability diagnostics (spec 22, Problem E) — surface the silent
3694
+ // structured-output cliffs (oversized appended schema, dropped refinements,
3695
+ // streaming disabled, zero-guidance `schemaPrompt:'none'`).
3696
+ "schema_diagnostic",
2947
3697
  // Observability
2948
3698
  "log",
2949
3699
  "memory_remember",
@@ -2986,6 +3736,7 @@ var LIFECYCLE_TYPES = /* @__PURE__ */ new Set([
2986
3736
  "delegate",
2987
3737
  "pipeline",
2988
3738
  "verify",
3739
+ "schema_diagnostic",
2989
3740
  "workflow_start",
2990
3741
  "workflow_end",
2991
3742
  "checkpoint_save",
@@ -3790,6 +4541,11 @@ var REDACTION_RULES = {
3790
4541
  // Scrub only the delta — preserving path keeps "which fields stream" telemetry.
3791
4542
  string_delta: (e) => ({ ...e, data: { ...e.data, delta: REDACTED } }),
3792
4543
  verify: (e) => e.data.lastError !== void 0 ? { ...e, data: { ...e.data, lastError: REDACTED } } : e,
4544
+ // `schema_diagnostic` carries only structural schema metadata — token counts,
4545
+ // thresholds, JSON-Schema field paths, a root type name, a tool name — none of
4546
+ // which is user content or PII (field paths are schema-shape, preserved like
4547
+ // `string_delta.path`). Passthrough, as an explicit reviewed decision.
4548
+ schema_diagnostic: passthrough,
3793
4549
  log: (e) => {
3794
4550
  if (!e.data || typeof e.data !== "object" || Array.isArray(e.data)) return e;
3795
4551
  return { ...e, data: walkObjectOneLevel(e.data, LOG_PRESERVE_KEYS) };
@@ -3822,6 +4578,80 @@ function redactEvent(event) {
3822
4578
  return rule(event);
3823
4579
  }
3824
4580
 
4581
+ // src/schema-diagnostics.ts
4582
+ var DEFAULT_SCHEMA_OVERSIZED_TOKENS = 4e3;
4583
+ var refinementCache = /* @__PURE__ */ new WeakMap();
4584
+ function detectDroppedRefinements(schema) {
4585
+ const cached = refinementCache.get(schema);
4586
+ if (cached) return cached;
4587
+ const paths = /* @__PURE__ */ new Set();
4588
+ let count = 0;
4589
+ const visited = /* @__PURE__ */ new WeakSet();
4590
+ const walk = (node, path) => {
4591
+ if (node === null || typeof node !== "object") return;
4592
+ if (visited.has(node)) return;
4593
+ visited.add(node);
4594
+ const def = node._zod?.def;
4595
+ if (!def) return;
4596
+ const here = () => paths.add(path === "" ? "<root>" : path);
4597
+ const checks = def.checks;
4598
+ if (Array.isArray(checks)) {
4599
+ for (const check of checks) {
4600
+ const kind = check?._zod?.def?.check;
4601
+ if (kind === "custom") {
4602
+ count += 1;
4603
+ here();
4604
+ }
4605
+ }
4606
+ }
4607
+ if (def.check === "custom") {
4608
+ count += 1;
4609
+ here();
4610
+ }
4611
+ const join2 = (child) => path === "" ? child : `${path}.${child}`;
4612
+ const shape = def.shape;
4613
+ if (shape && typeof shape === "object") {
4614
+ for (const [key, child] of Object.entries(shape)) walk(child, join2(key));
4615
+ }
4616
+ if ("element" in def) walk(def.element, `${path}[]`);
4617
+ if (Array.isArray(def.options)) {
4618
+ for (const opt of def.options) walk(opt, path);
4619
+ }
4620
+ if ("innerType" in def) walk(def.innerType, path);
4621
+ if (Array.isArray(def.items)) {
4622
+ def.items.forEach((item, i) => walk(item, `${path}[${i}]`));
4623
+ }
4624
+ if ("rest" in def && def.rest) walk(def.rest, `${path}[]`);
4625
+ if ("catchall" in def && def.catchall) walk(def.catchall, `${path}[*]`);
4626
+ if ("keyType" in def) walk(def.keyType, `${path}[key]`);
4627
+ if ("valueType" in def) walk(def.valueType, `${path}[*]`);
4628
+ if ("in" in def) walk(def.in, path);
4629
+ if ("out" in def) walk(def.out, path);
4630
+ if ("left" in def) walk(def.left, path);
4631
+ if ("right" in def) walk(def.right, path);
4632
+ if (typeof def.getter === "function") {
4633
+ try {
4634
+ walk(def.getter(), path);
4635
+ } catch {
4636
+ }
4637
+ }
4638
+ };
4639
+ walk(schema, "");
4640
+ const result = { count, paths: [...paths] };
4641
+ refinementCache.set(schema, result);
4642
+ return result;
4643
+ }
4644
+ var DOCS_URL = "docs/observability.md#schema-diagnostics";
4645
+ var MAX_WARNED_KEYS = 2048;
4646
+ var warnedDiagnosticKeys = /* @__PURE__ */ new Set();
4647
+ function warnSchemaDiagnosticOnce(key, message, silent) {
4648
+ if (silent || process.env.AXL_DIAGNOSTICS_SILENT === "true") return;
4649
+ if (warnedDiagnosticKeys.has(key)) return;
4650
+ if (warnedDiagnosticKeys.size >= MAX_WARNED_KEYS) warnedDiagnosticKeys.clear();
4651
+ warnedDiagnosticKeys.add(key);
4652
+ console.warn(`[axl] ${message} See ${DOCS_URL}.`);
4653
+ }
4654
+
3825
4655
  // src/config.ts
3826
4656
  function defineConfig(config) {
3827
4657
  return config;
@@ -3900,11 +4730,47 @@ function resolveConfig(config) {
3900
4730
  // src/context.ts
3901
4731
  var signalStorage = new AsyncLocalStorage();
3902
4732
  var askStorage = new AsyncLocalStorage();
4733
+ var inlineSchemaCache = /* @__PURE__ */ new WeakMap();
3903
4734
  function zodToJsonSchema(schema) {
4735
+ const cached = inlineSchemaCache.get(schema);
4736
+ if (cached !== void 0) return cached;
3904
4737
  const result = z.toJSONSchema(schema, { unrepresentable: "any" });
3905
4738
  delete result.$schema;
4739
+ Object.freeze(result);
4740
+ inlineSchemaCache.set(schema, result);
3906
4741
  return result;
3907
4742
  }
4743
+ var promptSchemaCache = /* @__PURE__ */ new WeakMap();
4744
+ var inlineSchemaTokenCache = /* @__PURE__ */ new WeakMap();
4745
+ function estimateInlineSchemaTokens(schema) {
4746
+ const cached = inlineSchemaTokenCache.get(schema);
4747
+ if (cached !== void 0) return cached;
4748
+ const tokens = estimateTokens(JSON.stringify(zodToJsonSchema(schema)));
4749
+ inlineSchemaTokenCache.set(schema, tokens);
4750
+ return tokens;
4751
+ }
4752
+ var nativeSchemaCache = /* @__PURE__ */ new WeakMap();
4753
+ function deriveNativeSchema(schema) {
4754
+ const cached = nativeSchemaCache.get(schema);
4755
+ if (cached !== void 0) return cached;
4756
+ const json = z.toJSONSchema(schema, { unrepresentable: "any", io: "input" });
4757
+ delete json.$schema;
4758
+ nativeSchemaCache.set(schema, json);
4759
+ return json;
4760
+ }
4761
+ function renderSchemaForPrompt(schema) {
4762
+ const cached = promptSchemaCache.get(schema);
4763
+ if (cached !== void 0) return cached;
4764
+ const json = z.toJSONSchema(schema, {
4765
+ unrepresentable: "any",
4766
+ io: "input",
4767
+ reused: "ref"
4768
+ });
4769
+ delete json.$schema;
4770
+ const rendered = JSON.stringify(json);
4771
+ promptSchemaCache.set(schema, rendered);
4772
+ return rendered;
4773
+ }
3908
4774
  function estimateTokens(text) {
3909
4775
  return Math.ceil(text.length / 4);
3910
4776
  }
@@ -4285,12 +5151,14 @@ var WorkflowContext = class _WorkflowContext {
4285
5151
  depth,
4286
5152
  agent: agent2._name,
4287
5153
  stepRef,
4288
- askCost: { value: 0 }
5154
+ askCost: { value: 0 },
5155
+ askUnpriced: false
4289
5156
  };
4290
5157
  return askStorage.run(frame, async () => {
4291
5158
  const askStart = Date.now();
4292
5159
  this.emitEvent({ type: "ask_start", prompt });
4293
5160
  const costBefore = this.budgetContext?.totalCost ?? 0;
5161
+ const unpricedCountBefore = this.budgetContext?.unpricedCount ?? 0;
4294
5162
  const resolveCtx = options?.metadata ? { metadata: { ...this.metadata, ...options.metadata } } : { metadata: this.metadata };
4295
5163
  const usageCapture = {};
4296
5164
  const doCall = async () => {
@@ -4336,6 +5204,7 @@ var WorkflowContext = class _WorkflowContext {
4336
5204
  ) : await doCall();
4337
5205
  outcome = { ok: true, result };
4338
5206
  const costAfter = this.budgetContext?.totalCost ?? 0;
5207
+ const unpricedCountAfter = this.budgetContext?.unpricedCount ?? 0;
4339
5208
  if (this.onAgentCallComplete) {
4340
5209
  try {
4341
5210
  this.onAgentCallComplete({
@@ -4344,6 +5213,7 @@ var WorkflowContext = class _WorkflowContext {
4344
5213
  response: typeof result === "string" ? result : JSON.stringify(result),
4345
5214
  model: agent2.resolveModel(resolveCtx),
4346
5215
  cost: costAfter - costBefore,
5216
+ unpriced: frame.askUnpriced || unpricedCountAfter > unpricedCountBefore,
4347
5217
  duration: Date.now() - askStart,
4348
5218
  promptVersion: agent2._config.version,
4349
5219
  temperature: options?.temperature ?? agent2._config.temperature,
@@ -4378,6 +5248,7 @@ var WorkflowContext = class _WorkflowContext {
4378
5248
  error: "ask_end emitted without outcome \u2014 internal bug"
4379
5249
  },
4380
5250
  cost: frame.askCost.value,
5251
+ ...frame.askUnpriced ? { unpriced: true } : {},
4381
5252
  duration: Date.now() - askStart
4382
5253
  });
4383
5254
  } catch (emitErr) {
@@ -4461,15 +5332,32 @@ var WorkflowContext = class _WorkflowContext {
4461
5332
  messages.push(msg);
4462
5333
  }
4463
5334
  }
5335
+ const schemaPromptMode = options?.schemaPrompt ?? agent2._config.schemaPrompt ?? "json-schema";
4464
5336
  let userContent = prompt;
5337
+ let appendedSchemaText;
4465
5338
  if (options?.schema) {
4466
- const jsonSchema = zodToJsonSchema(options.schema);
4467
- userContent += `
5339
+ const schema = options.schema;
5340
+ if (schemaPromptMode === "none") {
5341
+ } else if (schemaPromptMode === "json-schema") {
5342
+ appendedSchemaText = renderSchemaForPrompt(schema);
5343
+ userContent += `
4468
5344
 
4469
5345
  Respond with valid JSON matching this schema:
4470
- ${JSON.stringify(jsonSchema, null, 2)}`;
5346
+ ${appendedSchemaText}`;
5347
+ } else {
5348
+ appendedSchemaText = typeof schemaPromptMode.render === "function" ? schemaPromptMode.render(schema) : schemaPromptMode.render;
5349
+ userContent += `
5350
+
5351
+ ${appendedSchemaText}`;
5352
+ }
4471
5353
  }
4472
5354
  messages.push({ role: "user", content: userContent });
5355
+ this.emitSchemaDiagnostics(agent2, options, toolDefs, {
5356
+ mode: schemaPromptMode,
5357
+ appendedSchemaText,
5358
+ provider,
5359
+ model
5360
+ });
4473
5361
  if (handoffMessages && handoffMessages.length > 0) {
4474
5362
  const handoffContext = handoffMessages.filter(
4475
5363
  (m) => m.role === "assistant" || m.role === "tool"
@@ -4582,7 +5470,17 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
4582
5470
  signal: this.currentSignal
4583
5471
  };
4584
5472
  if (options?.schema && toolDefs.length === 0) {
4585
- chatOptions.responseFormat = { type: "json_object" };
5473
+ const nativeOut = options?.nativeStructuredOutput ?? agent2._config.nativeStructuredOutput ?? false;
5474
+ chatOptions.responseFormat = nativeOut ? {
5475
+ type: "json_schema",
5476
+ json_schema: {
5477
+ name: "response",
5478
+ // Input-side rendering — see `deriveNativeSchema`. Keeps the
5479
+ // native schema consistent with the prompt and non-empty for
5480
+ // `.transform()` schemas.
5481
+ schema: deriveNativeSchema(options.schema)
5482
+ }
5483
+ } : { type: "json_object" };
4586
5484
  }
4587
5485
  const callbackMeta = this.currentCallbackMeta(agent2._name);
4588
5486
  const callParams = {
@@ -4700,10 +5598,7 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
4700
5598
  }
4701
5599
  });
4702
5600
  }
4703
- response ??= {
4704
- content: content2,
4705
- usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 }
4706
- };
5601
+ response ??= { content: content2 };
4707
5602
  if (toolCalls.length > 0) {
4708
5603
  response.tool_calls = toolCalls;
4709
5604
  }
@@ -4727,7 +5622,11 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
4727
5622
  response: "",
4728
5623
  turn: turns,
4729
5624
  ...retryReason ? { retryReason } : {},
4730
- error: err instanceof Error ? err.message : String(err)
5625
+ error: err instanceof Error ? err.message : String(err),
5626
+ // Enrich with typed-error metadata when available. `body` is
5627
+ // deliberately NOT emitted — it's redaction-eligible (see
5628
+ // docs/security.md). `data.error` already carries the message.
5629
+ ...err instanceof ProviderError ? { status: err.status, retryable: err.retryable } : {}
4731
5630
  }
4732
5631
  });
4733
5632
  throw err;
@@ -4791,10 +5690,13 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
4791
5690
  depth: handoffTargetDepth,
4792
5691
  agent: descriptor.agent._name,
4793
5692
  stepRef: sourceFrame?.stepRef ?? this.stepRefRoot,
4794
- askCost: { value: 0 }
5693
+ askCost: { value: 0 },
5694
+ askUnpriced: false
4795
5695
  };
4796
5696
  const handoffOptions = options ? {
4797
5697
  schema: options.schema,
5698
+ schemaPrompt: options.schemaPrompt,
5699
+ nativeStructuredOutput: options.nativeStructuredOutput,
4798
5700
  retries: options.retries,
4799
5701
  metadata: options.metadata,
4800
5702
  validate: options.validate,
@@ -4815,6 +5717,7 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
4815
5717
  type: "ask_end",
4816
5718
  outcome: { ok: true, result },
4817
5719
  cost: targetFrame.askCost.value,
5720
+ ...targetFrame.askUnpriced ? { unpriced: true } : {},
4818
5721
  duration: Date.now() - targetAskStart
4819
5722
  });
4820
5723
  return result;
@@ -4826,6 +5729,7 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
4826
5729
  error: err instanceof Error ? err.message : String(err)
4827
5730
  },
4828
5731
  cost: targetFrame.askCost.value,
5732
+ ...targetFrame.askUnpriced ? { unpriced: true } : {},
4829
5733
  duration: Date.now() - targetAskStart
4830
5734
  });
4831
5735
  throw err;
@@ -5364,6 +6268,148 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
5364
6268
  ...providerMetadata ? { providerMetadata } : {}
5365
6269
  });
5366
6270
  }
6271
+ /**
6272
+ * Emit `schema_diagnostic` events for the silent structured-output cliffs
6273
+ * (spec 22, Problems B + E). Called once per ask. Covers:
6274
+ * - the appended **prompt** schema (oversized; dropped refinements; or the
6275
+ * zero-guidance `schemaPrompt:'none'` footgun),
6276
+ * - each user **tool-def** schema (oversized, dropped refinements),
6277
+ * - the **streaming** gate (disabled by a non-object root or by tools),
6278
+ * - **native structured output** the resolved provider can't honor (R10).
6279
+ *
6280
+ * The structured event always fires (persisted + streamed). For the genuinely
6281
+ * surprising, low-frequency cliffs it ALSO fires a one-time deduped
6282
+ * `console.warn` so the median consumer — who never wires up `ctx.events` and
6283
+ * runs with the trace console off — still sees it. `oversized` is event-only
6284
+ * (O3: it's tunable/noisy); the `tools` streaming cause is event-only too
6285
+ * (expected behavior for any tool-using agent, not a surprise).
6286
+ */
6287
+ emitSchemaDiagnostics(agent2, options, toolDefs, ctx) {
6288
+ const silent = this.config.diagnostics?.silent;
6289
+ const threshold = this.config.diagnostics?.schemaOversizedTokens ?? DEFAULT_SCHEMA_OVERSIZED_TOKENS;
6290
+ const schema = options?.schema;
6291
+ if (schema) {
6292
+ if (ctx.mode === "none") {
6293
+ this.emitEvent({
6294
+ type: "schema_diagnostic",
6295
+ agent: agent2._name,
6296
+ data: { kind: "schema_prompt_none_no_guidance" }
6297
+ });
6298
+ warnSchemaDiagnosticOnce(
6299
+ `${agent2._name}\0schema_prompt_none`,
6300
+ `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'.`,
6301
+ silent
6302
+ );
6303
+ } else {
6304
+ const promptTokens = estimateTokens(ctx.appendedSchemaText ?? "");
6305
+ if (promptTokens > threshold) {
6306
+ this.emitEvent({
6307
+ type: "schema_diagnostic",
6308
+ agent: agent2._name,
6309
+ data: {
6310
+ kind: "prompt_schema_oversized",
6311
+ estimatedTokens: promptTokens,
6312
+ threshold,
6313
+ site: "prompt"
6314
+ }
6315
+ });
6316
+ }
6317
+ if (ctx.mode === "json-schema") {
6318
+ const dropped = detectDroppedRefinements(schema);
6319
+ if (dropped.count > 0) {
6320
+ this.emitEvent({
6321
+ type: "schema_diagnostic",
6322
+ agent: agent2._name,
6323
+ data: {
6324
+ kind: "dropped_refinements",
6325
+ count: dropped.count,
6326
+ paths: dropped.paths,
6327
+ site: "prompt"
6328
+ }
6329
+ });
6330
+ warnSchemaDiagnosticOnce(
6331
+ `${agent2._name}\0dropped_refinements\0prompt\0${dropped.count}\0${dropped.paths.join(",")}`,
6332
+ `${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.`,
6333
+ silent
6334
+ );
6335
+ }
6336
+ }
6337
+ }
6338
+ const nativeOut = options?.nativeStructuredOutput ?? agent2._config.nativeStructuredOutput;
6339
+ if (nativeOut && toolDefs.length === 0) {
6340
+ const support = ctx.provider.nativeStructuredOutputSupport?.(ctx.model) ?? "schema";
6341
+ if (support !== "schema") {
6342
+ this.emitEvent({
6343
+ type: "schema_diagnostic",
6344
+ agent: agent2._name,
6345
+ data: {
6346
+ kind: "native_output_unsupported",
6347
+ ...ctx.provider.name ? { provider: ctx.provider.name } : {},
6348
+ support
6349
+ }
6350
+ });
6351
+ warnSchemaDiagnosticOnce(
6352
+ `${agent2._name}\0native_output_unsupported\0${ctx.provider.name ?? ctx.model}`,
6353
+ `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.`,
6354
+ silent
6355
+ );
6356
+ }
6357
+ }
6358
+ const rootIsObject = schema instanceof z.ZodObject;
6359
+ const hasTools = toolDefs.length > 0;
6360
+ if (this._streamingEnabled && (!rootIsObject || hasTools)) {
6361
+ const cause = !rootIsObject ? "non-object" : "tools";
6362
+ const rootType = schema.constructor?.name ?? "unknown";
6363
+ this.emitEvent({
6364
+ type: "schema_diagnostic",
6365
+ agent: agent2._name,
6366
+ data: { kind: "streaming_disabled", rootType, cause }
6367
+ });
6368
+ if (cause === "non-object") {
6369
+ warnSchemaDiagnosticOnce(
6370
+ `${agent2._name}\0streaming_disabled\0${rootType}`,
6371
+ `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.`,
6372
+ silent
6373
+ );
6374
+ }
6375
+ }
6376
+ }
6377
+ for (const tool2 of agent2._config.tools ?? []) {
6378
+ const toolTokens = estimateInlineSchemaTokens(tool2.inputSchema);
6379
+ if (toolTokens > threshold) {
6380
+ this.emitEvent({
6381
+ type: "schema_diagnostic",
6382
+ agent: agent2._name,
6383
+ data: {
6384
+ kind: "prompt_schema_oversized",
6385
+ estimatedTokens: toolTokens,
6386
+ threshold,
6387
+ site: "tool",
6388
+ tool: tool2.name
6389
+ }
6390
+ });
6391
+ }
6392
+ const droppedTool = detectDroppedRefinements(tool2.inputSchema);
6393
+ if (droppedTool.count > 0) {
6394
+ this.emitEvent({
6395
+ type: "schema_diagnostic",
6396
+ agent: agent2._name,
6397
+ data: {
6398
+ kind: "dropped_refinements",
6399
+ count: droppedTool.count,
6400
+ paths: droppedTool.paths,
6401
+ site: "tool",
6402
+ tool: tool2.name
6403
+ }
6404
+ });
6405
+ warnSchemaDiagnosticOnce(
6406
+ `${agent2._name}\0dropped_refinements\0tool\0${tool2.name}\0${droppedTool.count}`,
6407
+ `${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.`,
6408
+ silent
6409
+ );
6410
+ }
6411
+ }
6412
+ }
5367
6413
  buildToolDefs(agent2, resolvedHandoffs) {
5368
6414
  const defs = [];
5369
6415
  if (agent2._config.tools) {
@@ -5856,24 +6902,44 @@ ${summaryResponse.content}`
5856
6902
  limit,
5857
6903
  exceeded: false,
5858
6904
  policy,
5859
- abortController: controller
6905
+ abortController: controller,
6906
+ unpriced: false,
6907
+ unpricedCount: 0,
6908
+ unpricedWarned: false
5860
6909
  };
5861
6910
  const executeBudget = async () => {
5862
6911
  try {
5863
6912
  const value = budgetSignal ? await signalStorage.run(budgetSignal, fn) : await fn();
5864
6913
  const totalCost = this.budgetContext.totalCost;
5865
6914
  const exceeded = this.budgetContext.exceeded;
5866
- return { value, budgetExceeded: exceeded, totalCost };
6915
+ const unpriced = this.budgetContext.unpriced;
6916
+ return { value, budgetExceeded: exceeded, totalCost, unpriced };
5867
6917
  } catch (err) {
5868
6918
  if (this.budgetContext.exceeded) {
5869
- return { value: null, budgetExceeded: true, totalCost: this.budgetContext.totalCost };
6919
+ return {
6920
+ value: null,
6921
+ budgetExceeded: true,
6922
+ totalCost: this.budgetContext.totalCost,
6923
+ unpriced: this.budgetContext.unpriced
6924
+ };
5870
6925
  }
5871
6926
  if (err instanceof DOMException && err.name === "AbortError" && controller) {
5872
- return { value: null, budgetExceeded: true, totalCost: this.budgetContext.totalCost };
6927
+ return {
6928
+ value: null,
6929
+ budgetExceeded: true,
6930
+ totalCost: this.budgetContext.totalCost,
6931
+ unpriced: this.budgetContext.unpriced
6932
+ };
5873
6933
  }
5874
6934
  throw err;
5875
6935
  } finally {
5876
- if (parentBudget) parentBudget.totalCost += this.budgetContext.totalCost;
6936
+ if (parentBudget) {
6937
+ parentBudget.totalCost += this.budgetContext.totalCost;
6938
+ if (this.budgetContext.unpriced) {
6939
+ parentBudget.unpriced = true;
6940
+ parentBudget.unpricedCount += this.budgetContext.unpricedCount;
6941
+ }
6942
+ }
5877
6943
  this.budgetContext = parentBudget;
5878
6944
  }
5879
6945
  };
@@ -5894,13 +6960,22 @@ ${summaryResponse.content}`
5894
6960
  }
5895
6961
  return executeBudget();
5896
6962
  }
5897
- /** Get the current budget status, or null if not inside a budget block. */
6963
+ /**
6964
+ * Get the current budget status, or null if not inside a budget block. Inside
6965
+ * nested budgets this reflects the **innermost active** block (the nested block's
6966
+ * `finally` restores the parent before control returns to it).
6967
+ *
6968
+ * `unpriced` is true when this block ran a model with no usable cost (unpriced /
6969
+ * pricing-table miss): `spent` is then a LOWER BOUND and `remaining` an upper bound —
6970
+ * the limit cannot be enforced on the unmeasured spend. See {@link BudgetResult.unpriced}.
6971
+ */
5898
6972
  getBudgetStatus() {
5899
6973
  if (!this.budgetContext) return null;
5900
6974
  return {
5901
6975
  spent: this.budgetContext.totalCost,
5902
6976
  limit: this.budgetContext.limit,
5903
- remaining: Math.max(0, this.budgetContext.limit - this.budgetContext.totalCost)
6977
+ remaining: Math.max(0, this.budgetContext.limit - this.budgetContext.totalCost),
6978
+ unpriced: this.budgetContext.unpriced
5904
6979
  };
5905
6980
  }
5906
6981
  // ── ctx.race() ────────────────────────────────────────────────────────
@@ -6482,6 +7557,8 @@ ${summaryResponse.content}`
6482
7557
  });
6483
7558
  return this.ask(agents[0], prompt, {
6484
7559
  schema: options?.schema,
7560
+ schemaPrompt: options?.schemaPrompt,
7561
+ nativeStructuredOutput: options?.nativeStructuredOutput,
6485
7562
  retries: options?.retries,
6486
7563
  metadata: options?.metadata,
6487
7564
  validate: options?.validate,
@@ -6526,6 +7603,8 @@ ${summaryResponse.content}`
6526
7603
  });
6527
7604
  return this.ask(routerAgent, prompt, {
6528
7605
  schema: options?.schema,
7606
+ schemaPrompt: options?.schemaPrompt,
7607
+ nativeStructuredOutput: options?.nativeStructuredOutput,
6529
7608
  retries: options?.retries,
6530
7609
  metadata: options?.metadata,
6531
7610
  validate: options?.validate,
@@ -6544,8 +7623,25 @@ ${summaryResponse.content}`
6544
7623
  const frame = askStorage.getStore();
6545
7624
  const step = (frame?.stepRef ?? this.stepRefRoot).value++;
6546
7625
  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;
7626
+ if (COST_BEARING_LEAF_TYPES.includes(partial.type)) {
7627
+ const usable = isUsableCost(cost);
7628
+ const unpriced = !usable && hasPositiveTokens(
7629
+ partial
7630
+ );
7631
+ if (frame) {
7632
+ if (usable) frame.askCost.value += cost;
7633
+ else if (unpriced) frame.askUnpriced = true;
7634
+ }
7635
+ if (unpriced && this.budgetContext) {
7636
+ this.budgetContext.unpriced = true;
7637
+ this.budgetContext.unpricedCount += 1;
7638
+ if (!this.budgetContext.unpricedWarned && Number.isFinite(this.budgetContext.limit)) {
7639
+ this.budgetContext.unpricedWarned = true;
7640
+ console.warn(
7641
+ "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."
7642
+ );
7643
+ }
7644
+ }
6549
7645
  }
6550
7646
  const event = {
6551
7647
  executionId: this.executionId,
@@ -8372,7 +9468,7 @@ var StreamingFlusher = class {
8372
9468
  await Promise.allSettled(pendings);
8373
9469
  }
8374
9470
  };
8375
- function isAbortError(err) {
9471
+ function isAbortError2(err) {
8376
9472
  return typeof err === "object" && err !== null && err.name === "AbortError";
8377
9473
  }
8378
9474
  function pushEventBounded(execInfo, event, cap) {
@@ -8635,7 +9731,7 @@ var AxlRuntime = class extends EventEmitter2 {
8635
9731
  this.persistExecution(execInfo);
8636
9732
  return result;
8637
9733
  } catch (err) {
8638
- const aborted = isAbortError(err);
9734
+ const aborted = isAbortError2(err);
8639
9735
  execInfo.status = "failed";
8640
9736
  execInfo.completedAt = Date.now();
8641
9737
  execInfo.duration = execInfo.completedAt - execInfo.startedAt;
@@ -8909,7 +10005,10 @@ var AxlRuntime = class extends EventEmitter2 {
8909
10005
  totalCost: 0,
8910
10006
  limit: budgetLimit,
8911
10007
  exceeded: false,
8912
- policy: "finish_and_stop"
10008
+ policy: "finish_and_stop",
10009
+ unpriced: false,
10010
+ unpricedCount: 0,
10011
+ unpricedWarned: false
8913
10012
  }
8914
10013
  });
8915
10014
  }
@@ -8944,6 +10043,7 @@ var AxlRuntime = class extends EventEmitter2 {
8944
10043
  status: "running",
8945
10044
  events: [],
8946
10045
  totalCost: 0,
10046
+ unpriced: false,
8947
10047
  startedAt: Date.now(),
8948
10048
  duration: 0,
8949
10049
  metadata: liftPersistedMetadata(options?.metadata)
@@ -8962,6 +10062,7 @@ var AxlRuntime = class extends EventEmitter2 {
8962
10062
  onTrace: (event) => {
8963
10063
  pushEventBounded(execInfo, event, this.maxEventsPerExecution);
8964
10064
  execInfo.totalCost += eventCostContribution(event);
10065
+ if (isUnpricedLeaf(event)) execInfo.unpriced = true;
8965
10066
  if (this.streamableExecutionIds.has(executionId)) {
8966
10067
  this.streamingFlusher?.append(executionId, event);
8967
10068
  }
@@ -9008,7 +10109,10 @@ var AxlRuntime = class extends EventEmitter2 {
9008
10109
  totalCost: 0,
9009
10110
  limit: Infinity,
9010
10111
  exceeded: false,
9011
- policy: "finish_and_stop"
10112
+ policy: "finish_and_stop",
10113
+ unpriced: false,
10114
+ unpricedCount: 0,
10115
+ unpricedWarned: false
9012
10116
  }
9013
10117
  });
9014
10118
  return this.spanManager.withSpanAsync(
@@ -9052,6 +10156,7 @@ var AxlRuntime = class extends EventEmitter2 {
9052
10156
  status: "running",
9053
10157
  events: [],
9054
10158
  totalCost: 0,
10159
+ unpriced: false,
9055
10160
  startedAt: Date.now(),
9056
10161
  duration: 0,
9057
10162
  metadata: liftPersistedMetadata(options?.metadata)
@@ -9075,6 +10180,7 @@ var AxlRuntime = class extends EventEmitter2 {
9075
10180
  onTrace: (event) => {
9076
10181
  pushEventBounded(execInfo, event, this.maxEventsPerExecution);
9077
10182
  execInfo.totalCost += eventCostContribution(event);
10183
+ if (isUnpricedLeaf(event)) execInfo.unpriced = true;
9078
10184
  if (this.streamableExecutionIds.has(executionId)) {
9079
10185
  this.streamingFlusher?.append(executionId, event);
9080
10186
  }
@@ -9122,7 +10228,10 @@ var AxlRuntime = class extends EventEmitter2 {
9122
10228
  totalCost: 0,
9123
10229
  limit: Infinity,
9124
10230
  exceeded: false,
9125
- policy: "finish_and_stop"
10231
+ policy: "finish_and_stop",
10232
+ unpriced: false,
10233
+ unpricedCount: 0,
10234
+ unpricedWarned: false
9126
10235
  }
9127
10236
  });
9128
10237
  return this.spanManager.withSpanAsync(
@@ -9284,12 +10393,14 @@ var AxlRuntime = class extends EventEmitter2 {
9284
10393
  const completedAt = lastEvent.timestamp ?? startedAt;
9285
10394
  const workflowName = workflowStartEvent?.workflow ?? RECOVERED_UNKNOWN_WORKFLOW;
9286
10395
  const totalCost = boundedEvents.reduce((sum, e) => sum + eventCostContribution(e), 0);
10396
+ const unpriced = boundedEvents.some(isUnpricedLeaf);
9287
10397
  const synthesized = {
9288
10398
  executionId: id,
9289
10399
  workflow: workflowName,
9290
10400
  status: "failed",
9291
10401
  events: boundedEvents,
9292
10402
  totalCost,
10403
+ unpriced,
9293
10404
  startedAt,
9294
10405
  completedAt,
9295
10406
  duration: completedAt - startedAt,
@@ -9591,8 +10702,8 @@ var AxlRuntime = class extends EventEmitter2 {
9591
10702
  * Works with both `createContext()` and `execute()` calls inside `fn`.
9592
10703
  */
9593
10704
  async trackCost(fn) {
9594
- const { result, cost } = await this.trackExecution(fn);
9595
- return { result, cost };
10705
+ const { result, cost, unpriced } = await this.trackExecution(fn);
10706
+ return { result, cost, unpriced };
9596
10707
  }
9597
10708
  /**
9598
10709
  * Track cost and execution metadata across any runtime operations within the given function.
@@ -9637,10 +10748,12 @@ var AxlRuntime = class extends EventEmitter2 {
9637
10748
  const workflowCalls = /* @__PURE__ */ new Map();
9638
10749
  const tokens = { input: 0, output: 0, reasoning: 0 };
9639
10750
  let agentCalls = 0;
10751
+ let unpriced = false;
9640
10752
  const capturedTraces = options?.captureTraces ? [] : void 0;
9641
10753
  const listener = (event) => {
9642
10754
  if (!scope.trackedIds.has(event.executionId)) return;
9643
10755
  scope.totalCost += eventCostContribution(event);
10756
+ if (isUnpricedLeaf(event)) unpriced = true;
9644
10757
  if (event.type === "agent_call_end") {
9645
10758
  if (event.model) modelCalls.set(event.model, (modelCalls.get(event.model) ?? 0) + 1);
9646
10759
  agentCalls++;
@@ -9678,6 +10791,7 @@ var AxlRuntime = class extends EventEmitter2 {
9678
10791
  return {
9679
10792
  result,
9680
10793
  cost: scope.totalCost,
10794
+ unpriced,
9681
10795
  ...capturedTraces ? { traces: capturedTraces } : {},
9682
10796
  metadata: {
9683
10797
  models: [...modelCalls.keys()],
@@ -10572,22 +11686,40 @@ var OpenAIEmbedder = class {
10572
11686
  this.dimensions = options.dimensions ?? 1536;
10573
11687
  }
10574
11688
  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}`
11689
+ const response = await fetchWithRetry(
11690
+ `${this.baseUrl}/v1/embeddings`,
11691
+ {
11692
+ method: "POST",
11693
+ headers: {
11694
+ "Content-Type": "application/json",
11695
+ Authorization: `Bearer ${this.apiKey}`
11696
+ },
11697
+ body: JSON.stringify({
11698
+ model: this.model,
11699
+ input: texts,
11700
+ dimensions: this.dimensions
11701
+ }),
11702
+ signal
10580
11703
  },
10581
- body: JSON.stringify({
10582
- model: this.model,
10583
- input: texts,
10584
- dimensions: this.dimensions
10585
- }),
10586
- signal
10587
- });
11704
+ // Participate in the same typed-error contract as the provider adapters: a
11705
+ // thrown network failure normalizes to ProviderError{status:0,provider:'openai'}.
11706
+ { provider: "openai" }
11707
+ );
10588
11708
  if (!response.ok) {
10589
11709
  const body = await response.text();
10590
- throw new Error(`OpenAI embeddings API error (${response.status}): ${body}`);
11710
+ let detail = body;
11711
+ try {
11712
+ const json2 = JSON.parse(body);
11713
+ detail = json2.error?.message ?? json2.message ?? body;
11714
+ } catch {
11715
+ }
11716
+ throw buildProviderError({
11717
+ provider: "openai",
11718
+ status: response.status,
11719
+ headers: response.headers,
11720
+ message: `OpenAI embeddings API error (${response.status}): ${detail}`,
11721
+ body
11722
+ });
10591
11723
  }
10592
11724
  const json = await response.json();
10593
11725
  const vectors = json.data.sort((a, b) => a.index - b.index).map((d) => d.embedding);
@@ -10728,57 +11860,87 @@ function cosineSimilarity2(a, b) {
10728
11860
  }
10729
11861
  export {
10730
11862
  AXL_EVENT_TYPES,
11863
+ AZURE_PROFILE,
10731
11864
  AnthropicProvider,
10732
11865
  AxlError,
10733
11866
  AxlEventBus,
10734
11867
  AxlRuntime,
10735
11868
  AxlStream,
11869
+ BEDROCK_PROFILE,
11870
+ BUILTIN_PROFILES,
10736
11871
  BudgetExceededError,
10737
11872
  CONFIG_CANDIDATES,
10738
11873
  COST_BEARING_LEAF_TYPES,
11874
+ DEEPSEEK_PROFILE,
10739
11875
  EventStreamOverflowError,
11876
+ GROQ_PROFILE,
10740
11877
  GeminiProvider,
10741
11878
  GuardrailError,
10742
11879
  InMemoryVectorStore,
11880
+ LLAMACPP_PROFILE,
11881
+ LMSTUDIO_PROFILE,
11882
+ MISTRAL_PROFILE,
10743
11883
  MaxTurnsError,
10744
11884
  McpManager,
10745
11885
  MemoryManager,
10746
11886
  MemoryStore,
10747
11887
  NoConsensus,
10748
11888
  NoopSpanManager,
11889
+ OLLAMA_PROFILE,
11890
+ OPENAI_PRICING,
11891
+ OPENAI_PROFILE,
11892
+ OPENROUTER_PROFILE,
11893
+ OpenAICompatibleProvider,
10749
11894
  OpenAIEmbedder,
10750
11895
  OpenAIProvider,
10751
11896
  OpenAIResponsesProvider,
11897
+ ProviderError,
10752
11898
  ProviderRegistry,
10753
11899
  QuorumNotMet,
10754
11900
  REDACTED,
10755
11901
  REDACTION_RULES,
10756
11902
  RateLimiter,
10757
11903
  RedisStore,
11904
+ SGLANG_PROFILE,
10758
11905
  SQLiteStore,
10759
11906
  Session,
10760
11907
  SqliteVectorStore,
11908
+ ThinkTagScanner,
10761
11909
  TimeoutError,
10762
11910
  ToolDenied,
11911
+ VLLM_PROFILE,
10763
11912
  ValidationError,
10764
11913
  VerifyError,
10765
11914
  WorkflowContext,
11915
+ XAI_PROFILE,
10766
11916
  agent,
10767
11917
  createSpanManager,
10768
11918
  defineConfig,
11919
+ estimateOpenAICost,
10769
11920
  eventCostContribution,
10770
11921
  expandGlob,
10771
11922
  extractJson,
11923
+ extractThinkTags,
10772
11924
  findConfig,
11925
+ hasPositiveTokens,
10773
11926
  importModule,
10774
11927
  isCostBearingLeaf,
11928
+ isRetryableStatus,
10775
11929
  isRootLevel,
11930
+ isUnpricedLeaf,
11931
+ isUsableCost,
10776
11932
  needsTsxLoader,
11933
+ openaiReasoningEmit,
10777
11934
  parsePartialJson,
10778
11935
  pickDefault,
10779
11936
  pickExport,
11937
+ priceFromTable,
11938
+ reasoningEffortEmit,
11939
+ reasoningObjectEmit,
10780
11940
  redactEvent,
10781
11941
  registerConditions,
11942
+ resolveApiKey,
11943
+ resolvePerModel,
10782
11944
  resolveRuntime,
10783
11945
  resolveThinkingOptions,
10784
11946
  stringStreamFromEvents,