@axlsdk/axl 0.18.2 → 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.cjs CHANGED
@@ -111,57 +111,87 @@ var init_span_manager = __esm({
111
111
  var index_exports = {};
112
112
  __export(index_exports, {
113
113
  AXL_EVENT_TYPES: () => AXL_EVENT_TYPES,
114
+ AZURE_PROFILE: () => AZURE_PROFILE,
114
115
  AnthropicProvider: () => AnthropicProvider,
115
116
  AxlError: () => AxlError,
116
117
  AxlEventBus: () => AxlEventBus,
117
118
  AxlRuntime: () => AxlRuntime,
118
119
  AxlStream: () => AxlStream,
120
+ BEDROCK_PROFILE: () => BEDROCK_PROFILE,
121
+ BUILTIN_PROFILES: () => BUILTIN_PROFILES,
119
122
  BudgetExceededError: () => BudgetExceededError,
120
123
  CONFIG_CANDIDATES: () => CONFIG_CANDIDATES,
121
124
  COST_BEARING_LEAF_TYPES: () => COST_BEARING_LEAF_TYPES,
125
+ DEEPSEEK_PROFILE: () => DEEPSEEK_PROFILE,
122
126
  EventStreamOverflowError: () => EventStreamOverflowError,
127
+ GROQ_PROFILE: () => GROQ_PROFILE,
123
128
  GeminiProvider: () => GeminiProvider,
124
129
  GuardrailError: () => GuardrailError,
125
130
  InMemoryVectorStore: () => InMemoryVectorStore,
131
+ LLAMACPP_PROFILE: () => LLAMACPP_PROFILE,
132
+ LMSTUDIO_PROFILE: () => LMSTUDIO_PROFILE,
133
+ MISTRAL_PROFILE: () => MISTRAL_PROFILE,
126
134
  MaxTurnsError: () => MaxTurnsError,
127
135
  McpManager: () => McpManager,
128
136
  MemoryManager: () => MemoryManager,
129
137
  MemoryStore: () => MemoryStore,
130
138
  NoConsensus: () => NoConsensus,
131
139
  NoopSpanManager: () => NoopSpanManager,
140
+ OLLAMA_PROFILE: () => OLLAMA_PROFILE,
141
+ OPENAI_PRICING: () => OPENAI_PRICING,
142
+ OPENAI_PROFILE: () => OPENAI_PROFILE,
143
+ OPENROUTER_PROFILE: () => OPENROUTER_PROFILE,
144
+ OpenAICompatibleProvider: () => OpenAICompatibleProvider,
132
145
  OpenAIEmbedder: () => OpenAIEmbedder,
133
146
  OpenAIProvider: () => OpenAIProvider,
134
147
  OpenAIResponsesProvider: () => OpenAIResponsesProvider,
148
+ ProviderError: () => ProviderError,
135
149
  ProviderRegistry: () => ProviderRegistry,
136
150
  QuorumNotMet: () => QuorumNotMet,
137
151
  REDACTED: () => REDACTED,
138
152
  REDACTION_RULES: () => REDACTION_RULES,
139
153
  RateLimiter: () => RateLimiter,
140
154
  RedisStore: () => RedisStore,
155
+ SGLANG_PROFILE: () => SGLANG_PROFILE,
141
156
  SQLiteStore: () => SQLiteStore,
142
157
  Session: () => Session,
143
158
  SqliteVectorStore: () => SqliteVectorStore,
159
+ ThinkTagScanner: () => ThinkTagScanner,
144
160
  TimeoutError: () => TimeoutError,
145
161
  ToolDenied: () => ToolDenied,
162
+ VLLM_PROFILE: () => VLLM_PROFILE,
146
163
  ValidationError: () => ValidationError,
147
164
  VerifyError: () => VerifyError,
148
165
  WorkflowContext: () => WorkflowContext,
166
+ XAI_PROFILE: () => XAI_PROFILE,
149
167
  agent: () => agent,
150
168
  createSpanManager: () => createSpanManager,
151
169
  defineConfig: () => defineConfig,
170
+ estimateOpenAICost: () => estimateOpenAICost,
152
171
  eventCostContribution: () => eventCostContribution,
153
172
  expandGlob: () => expandGlob,
154
173
  extractJson: () => extractJson,
174
+ extractThinkTags: () => extractThinkTags,
155
175
  findConfig: () => findConfig,
176
+ hasPositiveTokens: () => hasPositiveTokens,
156
177
  importModule: () => importModule,
157
178
  isCostBearingLeaf: () => isCostBearingLeaf,
179
+ isRetryableStatus: () => isRetryableStatus,
158
180
  isRootLevel: () => isRootLevel,
181
+ isUnpricedLeaf: () => isUnpricedLeaf,
182
+ isUsableCost: () => isUsableCost,
159
183
  needsTsxLoader: () => needsTsxLoader,
184
+ openaiReasoningEmit: () => openaiReasoningEmit,
160
185
  parsePartialJson: () => parsePartialJson,
161
186
  pickDefault: () => pickDefault,
162
187
  pickExport: () => pickExport,
188
+ priceFromTable: () => priceFromTable,
189
+ reasoningEffortEmit: () => reasoningEffortEmit,
190
+ reasoningObjectEmit: () => reasoningObjectEmit,
163
191
  redactEvent: () => redactEvent,
164
192
  registerConditions: () => registerConditions,
193
+ resolveApiKey: () => resolveApiKey,
194
+ resolvePerModel: () => resolvePerModel,
165
195
  resolveRuntime: () => resolveRuntime,
166
196
  resolveThinkingOptions: () => resolveThinkingOptions,
167
197
  stringStreamFromEvents: () => stringStreamFromEvents,
@@ -289,11 +319,218 @@ function resolveThinkingOptions(options) {
289
319
  hasBudgetOverride
290
320
  };
291
321
  }
322
+ async function resolveApiKey(source) {
323
+ if (typeof source === "function") return await source() ?? "";
324
+ return source ?? "";
325
+ }
326
+
327
+ // src/errors.ts
328
+ var AxlError = class extends Error {
329
+ code;
330
+ constructor(code, message) {
331
+ super(message);
332
+ this.name = "AxlError";
333
+ this.code = code;
334
+ }
335
+ };
336
+ var VerifyError = class extends AxlError {
337
+ lastOutput;
338
+ zodError;
339
+ retries;
340
+ constructor(lastOutput, zodError, retries) {
341
+ super("VERIFY_ERROR", `Schema validation failed after ${retries} retries: ${zodError.message}`);
342
+ this.name = "VerifyError";
343
+ this.lastOutput = lastOutput;
344
+ this.zodError = zodError;
345
+ this.retries = retries;
346
+ }
347
+ };
348
+ var QuorumNotMet = class extends AxlError {
349
+ results;
350
+ constructor(required, actual, results) {
351
+ super("QUORUM_NOT_MET", `Quorum not met: needed ${required} successes, got ${actual}`);
352
+ this.name = "QuorumNotMet";
353
+ this.results = results;
354
+ }
355
+ };
356
+ var NoConsensus = class extends AxlError {
357
+ constructor(reason) {
358
+ super("NO_CONSENSUS", `No consensus: ${reason}`);
359
+ this.name = "NoConsensus";
360
+ }
361
+ };
362
+ var TimeoutError = class extends AxlError {
363
+ constructor(operation, timeoutMs) {
364
+ super("TIMEOUT", `${operation} exceeded timeout of ${timeoutMs}ms`);
365
+ this.name = "TimeoutError";
366
+ }
367
+ };
368
+ function formatBudgetCost(cost) {
369
+ if (Number.isNaN(cost)) return "$NaN";
370
+ if (cost === Infinity) return "$Infinity";
371
+ if (cost === -Infinity) return "-$Infinity";
372
+ if (cost === 0) return "$0.00";
373
+ const sign = cost < 0 ? "-" : "";
374
+ const abs = Math.abs(cost);
375
+ if (abs < 1e-6) return `${sign}< $0.000001`;
376
+ if (abs < 1e-4) {
377
+ const [mantissa, exponent] = abs.toExponential(2).split("e");
378
+ return `${sign}$${mantissa}e${parseInt(exponent, 10)}`;
379
+ }
380
+ if (abs < 0.01) return `${sign}$${abs.toFixed(6)}`;
381
+ return `${sign}$${abs.toFixed(2)}`;
382
+ }
383
+ var BudgetExceededError = class extends AxlError {
384
+ limit;
385
+ spent;
386
+ policy;
387
+ constructor(limit, spent, policy) {
388
+ super(
389
+ "BUDGET_EXCEEDED",
390
+ `Budget exceeded: spent ${formatBudgetCost(spent)} of ${formatBudgetCost(
391
+ limit
392
+ )} limit (policy: ${policy})`
393
+ );
394
+ this.name = "BudgetExceededError";
395
+ this.limit = limit;
396
+ this.spent = spent;
397
+ this.policy = policy;
398
+ }
399
+ };
400
+ var MaxTurnsError = class extends AxlError {
401
+ maxTurns;
402
+ constructor(operation, maxTurns) {
403
+ super("MAX_TURNS", `${operation} exceeded maximum of ${maxTurns} turns`);
404
+ this.name = "MaxTurnsError";
405
+ this.maxTurns = maxTurns;
406
+ }
407
+ };
408
+ var GuardrailError = class extends AxlError {
409
+ guardrailType;
410
+ reason;
411
+ constructor(guardrailType, reason) {
412
+ super("GUARDRAIL_BLOCKED", `${guardrailType} guardrail blocked: ${reason}`);
413
+ this.name = "GuardrailError";
414
+ this.guardrailType = guardrailType;
415
+ this.reason = reason;
416
+ }
417
+ };
418
+ var ValidationError = class extends AxlError {
419
+ lastOutput;
420
+ reason;
421
+ retries;
422
+ constructor(lastOutput, reason, retries) {
423
+ super("VALIDATION_ERROR", `Validation failed after ${retries} retries: ${reason}`);
424
+ this.name = "ValidationError";
425
+ this.lastOutput = lastOutput;
426
+ this.reason = reason;
427
+ this.retries = retries;
428
+ }
429
+ };
430
+ var ToolDenied = class extends AxlError {
431
+ toolName;
432
+ agentName;
433
+ constructor(toolName, agentName) {
434
+ super(
435
+ "TOOL_DENIED",
436
+ `Agent "${agentName}" attempted to call tool "${toolName}" which is not in its ACL`
437
+ );
438
+ this.name = "ToolDenied";
439
+ this.toolName = toolName;
440
+ this.agentName = agentName;
441
+ }
442
+ };
443
+
444
+ // src/providers/errors.ts
445
+ var ProviderError = class extends AxlError {
446
+ /** Adapter/profile name, e.g. `'openai'`, `'anthropic'`, `'google'`. */
447
+ provider;
448
+ /** HTTP status code; `0` for non-HTTP (network) failures. */
449
+ status;
450
+ /** Semantic failover hint (see module doc) — NOT the transport-retry set. */
451
+ retryable;
452
+ /** Parsed `Retry-After` delay in ms (RAW/unclamped), when the header was present. */
453
+ retryAfterMs;
454
+ /** Provider request id from response headers, when present. */
455
+ requestId;
456
+ /**
457
+ * Raw provider error body. Lives ONLY on the error object — it is
458
+ * intentionally never placed on the event stream (it can echo prompt text
459
+ * and is redaction-eligible). See `docs/security.md`.
460
+ */
461
+ body;
462
+ constructor(args) {
463
+ super("PROVIDER_ERROR", args.message);
464
+ this.name = "ProviderError";
465
+ this.provider = args.provider;
466
+ this.status = args.status;
467
+ this.retryable = args.retryable;
468
+ this.retryAfterMs = args.retryAfterMs;
469
+ this.requestId = args.requestId;
470
+ this.body = args.body;
471
+ }
472
+ };
473
+ function isRetryableStatus(status) {
474
+ switch (status) {
475
+ case 0:
476
+ return true;
477
+ case 408:
478
+ // request timeout
479
+ case 429:
480
+ // rate limit
481
+ case 500:
482
+ case 502:
483
+ case 503:
484
+ case 504:
485
+ case 529:
486
+ return true;
487
+ // Explicitly non-retryable (bad request / auth / not found / payload):
488
+ // 400, 401, 403, 404, 413, 422 fall through to the default.
489
+ default:
490
+ return false;
491
+ }
492
+ }
493
+ function parseRetryAfter(headers) {
494
+ const raw = headers.get("retry-after");
495
+ if (!raw) return void 0;
496
+ const trimmed = raw.trim();
497
+ if (/^\d+(\.\d+)?$/.test(trimmed)) {
498
+ const ms = Number(trimmed) * 1e3;
499
+ return ms > 0 ? ms : void 0;
500
+ }
501
+ const dateMs = Date.parse(trimmed);
502
+ if (Number.isNaN(dateMs)) return void 0;
503
+ const delta = dateMs - Date.now();
504
+ return delta > 0 ? delta : void 0;
505
+ }
506
+ var REQUEST_ID_HEADERS = ["x-request-id", "request-id"];
507
+ function extractRequestId(headers) {
508
+ for (const name of REQUEST_ID_HEADERS) {
509
+ const value = headers.get(name);
510
+ if (value) return value;
511
+ }
512
+ return void 0;
513
+ }
514
+ function buildProviderError(args) {
515
+ return new ProviderError({
516
+ provider: args.provider,
517
+ status: args.status,
518
+ retryable: isRetryableStatus(args.status),
519
+ message: args.message,
520
+ retryAfterMs: args.headers ? parseRetryAfter(args.headers) : void 0,
521
+ requestId: args.headers ? extractRequestId(args.headers) : void 0,
522
+ body: args.body
523
+ });
524
+ }
292
525
 
293
526
  // src/providers/retry.ts
294
527
  var RETRYABLE_STATUS_CODES = /* @__PURE__ */ new Set([429, 503, 529]);
295
528
  var MAX_RETRIES = 2;
296
529
  var BASE_DELAY_MS = 1e3;
530
+ var MAX_BACKOFF_MS = 6e4;
531
+ function jitter(ms) {
532
+ return ms * (0.75 + Math.random() * 0.5);
533
+ }
297
534
  function sleep2(ms, signal) {
298
535
  return new Promise((resolve2) => {
299
536
  if (signal?.aborted) {
@@ -311,9 +548,14 @@ function sleep2(ms, signal) {
311
548
  );
312
549
  });
313
550
  }
551
+ function isAbortError(err, signal) {
552
+ if (signal?.aborted) return true;
553
+ return typeof err === "object" && err !== null && "name" in err && err.name === "AbortError";
554
+ }
314
555
  async function fetchWithRetry(input, init, opts) {
315
556
  const maxRetries = opts?.maxRetries ?? MAX_RETRIES;
316
557
  const governor = opts?.governor;
558
+ const provider = opts?.provider ?? "unknown";
317
559
  let acquired = false;
318
560
  if (governor) {
319
561
  await governor.acquire(init?.signal ?? void 0);
@@ -321,7 +563,21 @@ async function fetchWithRetry(input, init, opts) {
321
563
  }
322
564
  try {
323
565
  for (let attempt = 0; ; attempt++) {
324
- const res = await fetch(input, init);
566
+ let res;
567
+ try {
568
+ res = await fetch(input, init);
569
+ } catch (err) {
570
+ if (isAbortError(err, init?.signal ?? void 0)) throw err;
571
+ if (attempt >= maxRetries) {
572
+ throw buildProviderError({
573
+ provider,
574
+ status: 0,
575
+ message: err instanceof Error ? err.message : String(err)
576
+ });
577
+ }
578
+ await sleep2(jitter(BASE_DELAY_MS * 2 ** attempt), init?.signal ?? void 0);
579
+ continue;
580
+ }
325
581
  governor?.observe(res);
326
582
  if (res.ok || !RETRYABLE_STATUS_CODES.has(res.status) || attempt >= maxRetries) {
327
583
  return res;
@@ -329,15 +585,9 @@ async function fetchWithRetry(input, init, opts) {
329
585
  if (init?.signal?.aborted) {
330
586
  return res;
331
587
  }
332
- const retryAfter = res.headers.get("retry-after");
333
- let delay;
334
- if (retryAfter && !isNaN(Number(retryAfter))) {
335
- delay = Number(retryAfter) * 1e3;
336
- } else {
337
- delay = BASE_DELAY_MS * 2 ** attempt;
338
- }
339
- delay *= 0.75 + Math.random() * 0.5;
340
- await sleep2(delay, init?.signal ?? void 0);
588
+ const retryAfterMs = parseRetryAfter(res.headers);
589
+ const baseDelay = retryAfterMs !== void 0 ? Math.min(retryAfterMs, MAX_BACKOFF_MS) : BASE_DELAY_MS * 2 ** attempt;
590
+ await sleep2(jitter(baseDelay), init?.signal ?? void 0);
341
591
  }
342
592
  } finally {
343
593
  if (acquired) governor.release();
@@ -485,254 +735,467 @@ var RateLimiter = class {
485
735
  }
486
736
  };
487
737
 
488
- // src/providers/openai.ts
489
- var OPENAI_PRICING = {
490
- // gpt-4o era — cache reads at 50% of input rate
491
- "gpt-4o": [25e-7, 1e-5, 0.5],
492
- "gpt-4o-mini": [15e-8, 6e-7, 0.5],
493
- "gpt-4-turbo": [1e-5, 3e-5, 0.5],
494
- "gpt-4": [3e-5, 6e-5, 0.5],
495
- "gpt-3.5-turbo": [5e-7, 15e-7, 0.5],
496
- o1: [15e-6, 6e-5, 0.5],
497
- "o1-mini": [3e-6, 12e-6, 0.5],
498
- "o1-pro": [15e-5, 6e-4, 0.5],
499
- // gpt-4.1 / o3 / o4 era — cache reads at 25% of input rate
500
- "gpt-4.1": [2e-6, 8e-6, 0.25],
501
- "gpt-4.1-mini": [4e-7, 16e-7, 0.25],
502
- "gpt-4.1-nano": [1e-7, 4e-7, 0.25],
503
- o3: [1e-5, 4e-5, 0.25],
504
- "o3-mini": [11e-7, 44e-7, 0.25],
505
- "o3-pro": [2e-5, 8e-5, 0.25],
506
- "o4-mini": [11e-7, 44e-7, 0.25],
507
- // gpt-5 era — cache reads at 10% of input rate
508
- "gpt-5": [125e-8, 1e-5, 0.1],
509
- "gpt-5-mini": [25e-8, 2e-6, 0.1],
510
- "gpt-5-nano": [5e-8, 4e-7, 0.1],
511
- "gpt-5.1": [125e-8, 1e-5, 0.1],
512
- "gpt-5.2": [175e-8, 14e-6, 0.1],
513
- "gpt-5.3": [175e-8, 14e-6, 0.1],
514
- "gpt-5.4": [25e-7, 15e-6, 0.1],
515
- "gpt-5.4-pro": [3e-5, 18e-5, 0.1],
516
- // gpt-5.5 era (flagship released 2026-04) — cache reads at 10% of input rate
517
- "gpt-5.5": [5e-6, 3e-5, 0.1],
518
- "gpt-5.5-pro": [3e-5, 18e-5, 0.1]
519
- };
520
- var PRICING_KEYS_BY_LENGTH = Object.keys(OPENAI_PRICING).sort((a, b) => b.length - a.length);
521
- function estimateOpenAICost(model, promptTokens, completionTokens, cachedTokens) {
522
- let pricing = OPENAI_PRICING[model];
738
+ // src/providers/openai-compatible.ts
739
+ function resolvePerModel(value, model, fallback) {
740
+ if (value === void 0) return fallback;
741
+ return typeof value === "function" ? value(model) : value;
742
+ }
743
+ var SORTED_KEYS_CACHE = /* @__PURE__ */ new WeakMap();
744
+ function sortedKeys(table) {
745
+ let keys = SORTED_KEYS_CACHE.get(table);
746
+ if (!keys) {
747
+ keys = Object.keys(table).sort((a, b) => b.length - a.length);
748
+ SORTED_KEYS_CACHE.set(table, keys);
749
+ }
750
+ return keys;
751
+ }
752
+ function priceFromTable(table, model, promptTokens, completionTokens, cachedTokens) {
753
+ let pricing = table[model];
523
754
  if (!pricing) {
524
- for (const key of PRICING_KEYS_BY_LENGTH) {
755
+ for (const key of sortedKeys(table)) {
525
756
  if (model.startsWith(key)) {
526
- pricing = OPENAI_PRICING[key];
757
+ pricing = table[key];
527
758
  break;
528
759
  }
529
760
  }
530
761
  }
531
- if (!pricing) return 0;
762
+ if (!pricing) return void 0;
532
763
  const [inputRate, outputRate, cacheMultiplier] = pricing;
533
764
  const cached = cachedTokens ?? 0;
534
765
  const inputCost = (promptTokens - cached) * inputRate + cached * inputRate * cacheMultiplier;
535
766
  return inputCost + completionTokens * outputRate;
536
767
  }
537
- function isOSeriesModel(model) {
538
- return /^(o1|o3|o4-mini)/.test(model);
768
+ var THINK_OPEN = "<think>";
769
+ var THINK_CLOSE = "</think>";
770
+ function partialTagSuffixLen(s, tag) {
771
+ const max = Math.min(s.length, tag.length - 1);
772
+ for (let n = max; n > 0; n--) {
773
+ if (tag.startsWith(s.slice(s.length - n))) return n;
774
+ }
775
+ return 0;
539
776
  }
540
- function supportsReasoningEffort(model) {
541
- return isOSeriesModel(model) || /^gpt-5/.test(model);
542
- }
543
- function supportsReasoningNone(model) {
544
- return /^gpt-5\.[1-9]/.test(model);
777
+ var ThinkTagScanner = class {
778
+ buffer = "";
779
+ inside = false;
780
+ push(chunk) {
781
+ this.buffer += chunk;
782
+ let text = "";
783
+ let thinking = "";
784
+ for (; ; ) {
785
+ const tag = this.inside ? THINK_CLOSE : THINK_OPEN;
786
+ const idx = this.buffer.indexOf(tag);
787
+ if (idx === -1) {
788
+ const hold = partialTagSuffixLen(this.buffer, tag);
789
+ const emit = this.buffer.slice(0, this.buffer.length - hold);
790
+ if (this.inside) thinking += emit;
791
+ else text += emit;
792
+ this.buffer = this.buffer.slice(this.buffer.length - hold);
793
+ break;
794
+ }
795
+ const before = this.buffer.slice(0, idx);
796
+ if (this.inside) thinking += before;
797
+ else text += before;
798
+ this.buffer = this.buffer.slice(idx + tag.length);
799
+ this.inside = !this.inside;
800
+ }
801
+ return { text, thinking };
802
+ }
803
+ /** Flush any residual buffer at stream end (treat as the current channel). */
804
+ flush() {
805
+ const rest = this.buffer;
806
+ this.buffer = "";
807
+ return this.inside ? { text: "", thinking: rest } : { text: rest, thinking: "" };
808
+ }
809
+ };
810
+ function extractThinkTags(content) {
811
+ const scanner = new ThinkTagScanner();
812
+ const a = scanner.push(content);
813
+ const b = scanner.flush();
814
+ return { content: a.text + b.text, thinking: a.thinking + b.thinking };
545
815
  }
546
- function supportsXhigh(model) {
547
- return /^gpt-5\.([2-9]|\d{2,})/.test(model);
816
+ function reasoningEffortEmit(map) {
817
+ return (body, resolved, model) => {
818
+ const value = map(resolved, model);
819
+ if (value) body.reasoning_effort = value;
820
+ };
548
821
  }
549
- function clampReasoningEffort(model, effort) {
550
- if (model.startsWith("gpt-5-pro")) return "high";
551
- if (effort === "none" && !supportsReasoningNone(model)) return "minimal";
552
- if (effort === "xhigh" && !supportsXhigh(model)) return "high";
553
- return effort;
822
+ function reasoningObjectEmit(mapEffort) {
823
+ return (body, resolved, model) => {
824
+ if (resolved.hasBudgetOverride) {
825
+ body.reasoning = { max_tokens: resolved.thinkingBudget };
826
+ } else if (resolved.activeEffort) {
827
+ body.reasoning = { effort: mapEffort(resolved.activeEffort, model) };
828
+ } else if (resolved.thinkingDisabled) {
829
+ body.reasoning = { enabled: false };
830
+ }
831
+ };
554
832
  }
555
- function effortToReasoningEffort(effort) {
556
- return effort === "max" ? "xhigh" : effort;
833
+ var ROUND_TRIP_FIELDS = ["reasoning_content", "reasoning", "reasoning_details"];
834
+ function isRoundTripField(field) {
835
+ return typeof field === "string" && ROUND_TRIP_FIELDS.includes(field);
557
836
  }
558
- function budgetToReasoningEffort(budget) {
559
- if (budget <= 1024) return "low";
560
- if (budget <= 8192) return "medium";
561
- return "high";
837
+ function normalizeMessageContent(content) {
838
+ if (typeof content === "string") return content;
839
+ if (content == null) return "";
840
+ if (!Array.isArray(content)) return "";
841
+ return content.map((part) => {
842
+ if (typeof part === "string") return part;
843
+ if (!part || typeof part !== "object") return "";
844
+ if (typeof part.text === "string") return part.text;
845
+ if (typeof part.content === "string") return part.content;
846
+ return "";
847
+ }).join("");
562
848
  }
563
- var OpenAIProvider = class {
564
- name = "openai";
849
+ var OpenAICompatibleProvider = class {
850
+ name;
851
+ /** `json_schema` is honored natively when the profile's `supportsJsonSchema`
852
+ * capability is true for this model (the default); otherwise the engine
853
+ * downgrades the request to plain `json_object` (see `buildRequestBody`),
854
+ * which does NOT enforce the schema shape. */
855
+ nativeStructuredOutputSupport(model) {
856
+ const supportsSchema = resolvePerModel(
857
+ this.profile.capabilities?.supportsJsonSchema,
858
+ model,
859
+ true
860
+ );
861
+ return supportsSchema ? "schema" : "downgraded";
862
+ }
863
+ profile;
565
864
  baseUrl;
566
- apiKey;
865
+ /** A key string, or a resolver invoked per request (expiring tokens). */
866
+ apiKeySource;
867
+ authHeader;
567
868
  governor;
568
- constructor(options = {}) {
569
- this.apiKey = options.apiKey ?? process.env.OPENAI_API_KEY ?? "";
570
- this.baseUrl = (options.baseUrl ?? process.env.OPENAI_BASE_URL ?? "https://api.openai.com/v1").replace(/\/$/, "");
869
+ constructor(options) {
870
+ const p = options.profile;
871
+ this.profile = p;
872
+ this.name = p.name;
873
+ this.apiKeySource = options.apiKey ?? (p.envApiKey ? process.env[p.envApiKey] : void 0) ?? "";
874
+ this.authHeader = options.authHeader;
875
+ const envBase = p.envBaseUrl ? process.env[p.envBaseUrl] : void 0;
876
+ const explicitBase = options.baseUrl ?? envBase;
877
+ this.baseUrl = (explicitBase ?? p.defaultBaseUrl).replace(/\/$/, "");
571
878
  this.governor = options.rateLimit ? new RateLimiter(options.rateLimit) : void 0;
572
- if (!this.apiKey) {
573
- throw new Error("OpenAI API key is required. Set OPENAI_API_KEY or pass apiKey in options.");
879
+ const label = p.label ?? p.name;
880
+ if (p.requireExplicitBaseUrl && explicitBase === void 0) {
881
+ const env = p.envBaseUrl ? ` or ${p.envBaseUrl}` : "";
882
+ throw new Error(
883
+ `${label} requires a base URL. Set providers.${p.name}.baseUrl${env} (it is resource-specific and has no default).`
884
+ );
885
+ }
886
+ if (typeof this.apiKeySource === "string" && !this.apiKeySource && !p.allowMissingApiKey) {
887
+ const env = p.envApiKey ?? "the API key env var";
888
+ throw new Error(`${label} API key is required. Set ${env} or pass apiKey in options.`);
574
889
  }
575
890
  }
576
- // ---------------------------------------------------------------------------
577
- // chat - non-streaming completion
578
- // ---------------------------------------------------------------------------
891
+ /** Resolve the API key for one request, validating against allowMissingApiKey. */
892
+ async resolveKey() {
893
+ const key = await resolveApiKey(this.apiKeySource);
894
+ if (!key && !this.profile.allowMissingApiKey) {
895
+ const label = this.profile.label ?? this.profile.name;
896
+ const env = this.profile.envApiKey ?? "the API key env var";
897
+ throw new Error(`${label} API key is required. Set ${env} or pass apiKey in options.`);
898
+ }
899
+ return key;
900
+ }
579
901
  async chat(messages, options) {
902
+ const headers = this.buildHeaders(await this.resolveKey());
580
903
  const body = this.buildRequestBody(messages, options, false);
581
904
  const res = await fetchWithRetry(
582
905
  `${this.baseUrl}/chat/completions`,
583
906
  {
584
907
  method: "POST",
585
- headers: {
586
- "Content-Type": "application/json",
587
- Authorization: `Bearer ${this.apiKey}`
588
- },
908
+ headers,
589
909
  body: JSON.stringify(body),
590
910
  signal: options.signal
591
911
  },
592
- { governor: this.governor }
912
+ { governor: this.governor, provider: this.name }
593
913
  );
594
914
  if (!res.ok) {
595
915
  const errorBody = await res.text();
596
- const message = this.extractErrorMessage(errorBody, res.status);
597
- throw new Error(message);
916
+ throw buildProviderError({
917
+ provider: this.name,
918
+ status: res.status,
919
+ headers: res.headers,
920
+ message: this.extractErrorMessage(errorBody, res.status),
921
+ body: errorBody
922
+ });
598
923
  }
599
924
  const json = await res.json();
600
- const choice = json.choices[0];
601
- const usage = json.usage ? {
602
- prompt_tokens: json.usage.prompt_tokens,
603
- completion_tokens: json.usage.completion_tokens,
604
- total_tokens: json.usage.total_tokens,
605
- reasoning_tokens: json.usage.completion_tokens_details?.reasoning_tokens,
606
- cached_tokens: json.usage.prompt_tokens_details?.cached_tokens
607
- } : void 0;
608
- const cost = usage ? estimateOpenAICost(
609
- options.model,
610
- usage.prompt_tokens,
611
- usage.completion_tokens,
612
- usage.cached_tokens
613
- ) : void 0;
614
- return {
615
- content: choice.message.content ?? "",
616
- tool_calls: choice.message.tool_calls?.map((tc) => ({
617
- id: tc.id,
618
- type: "function",
619
- function: {
620
- name: tc.function.name,
621
- arguments: tc.function.arguments
622
- }
623
- })),
624
- usage,
625
- cost
626
- };
925
+ return this.parseResponse(json, options.model);
627
926
  }
628
- // ---------------------------------------------------------------------------
629
- // stream - SSE streaming completion
630
- // ---------------------------------------------------------------------------
631
927
  async *stream(messages, options) {
928
+ const headers = this.buildHeaders(await this.resolveKey());
632
929
  const body = this.buildRequestBody(messages, options, true);
633
930
  const res = await fetchWithRetry(
634
931
  `${this.baseUrl}/chat/completions`,
635
932
  {
636
933
  method: "POST",
637
- headers: {
638
- "Content-Type": "application/json",
639
- Authorization: `Bearer ${this.apiKey}`
640
- },
934
+ headers,
641
935
  body: JSON.stringify(body),
642
936
  signal: options.signal
643
937
  },
644
- { governor: this.governor }
938
+ { governor: this.governor, provider: this.name }
645
939
  );
646
940
  if (!res.ok) {
647
941
  const errorBody = await res.text();
648
- const message = this.extractErrorMessage(errorBody, res.status);
649
- throw new Error(message);
942
+ throw buildProviderError({
943
+ provider: this.name,
944
+ status: res.status,
945
+ headers: res.headers,
946
+ message: this.extractErrorMessage(errorBody, res.status),
947
+ body: errorBody
948
+ });
650
949
  }
651
950
  if (!res.body) {
652
- throw new Error("OpenAI stream response has no body");
951
+ throw new Error(`${this.profile.label ?? this.profile.name} stream response has no body`);
653
952
  }
654
953
  yield* this.parseSSEStream(res.body, options.model);
655
954
  }
656
955
  // ---------------------------------------------------------------------------
657
- // Internal helpers
956
+ // Request building
658
957
  // ---------------------------------------------------------------------------
659
- buildRequestBody(messages, options, stream) {
660
- const oSeries = isOSeriesModel(options.model);
661
- const reasoningCapable = supportsReasoningEffort(options.model);
662
- const { thinkingBudget, thinkingDisabled, activeEffort, hasBudgetOverride } = resolveThinkingOptions(options);
663
- let wireEffort;
664
- if (reasoningCapable) {
665
- if (hasBudgetOverride) {
666
- wireEffort = clampReasoningEffort(options.model, budgetToReasoningEffort(thinkingBudget));
667
- } else if (!thinkingDisabled && activeEffort) {
668
- wireEffort = clampReasoningEffort(options.model, effortToReasoningEffort(activeEffort));
669
- } else if (thinkingDisabled) {
670
- wireEffort = clampReasoningEffort(options.model, "none");
671
- }
958
+ buildHeaders(apiKey) {
959
+ const headers = {
960
+ "Content-Type": "application/json",
961
+ ...this.profile.headers ?? {}
962
+ };
963
+ if (apiKey) {
964
+ const auth = this.authHeader ?? this.profile.authHeader ?? "bearer";
965
+ if (auth === "bearer") headers.Authorization = `Bearer ${apiKey}`;
966
+ else if (auth === "api-key") headers["api-key"] = apiKey;
967
+ else headers[auth.header] = auth.scheme ? `${auth.scheme} ${apiKey}` : apiKey;
672
968
  }
673
- const stripTemp = oSeries || reasoningCapable && wireEffort !== void 0;
969
+ return headers;
970
+ }
971
+ buildRequestBody(messages, options, stream) {
972
+ const profile = this.profile;
973
+ const resolved = resolveThinkingOptions(options);
674
974
  const body = {
675
975
  model: options.model,
676
- messages: messages.map((m) => this.formatMessage(m, oSeries)),
976
+ messages: messages.map((m) => this.formatMessage(m, options.model)),
677
977
  stream
678
978
  };
679
- if (options.temperature !== void 0 && !stripTemp) {
979
+ const emitResult = profile.reasoning.emit(body, resolved, options.model) ?? {};
980
+ const stripTemperature = emitResult.stripTemperature ?? false;
981
+ if (options.temperature !== void 0 && !stripTemperature) {
680
982
  body.temperature = options.temperature;
681
983
  }
682
984
  if (options.maxTokens !== void 0) {
683
- body.max_completion_tokens = options.maxTokens;
985
+ body[profile.maxTokensField ?? "max_completion_tokens"] = options.maxTokens;
684
986
  }
685
987
  if (options.stop) body.stop = options.stop;
686
988
  if (options.tools && options.tools.length > 0) {
687
989
  body.tools = options.tools;
688
- if (!oSeries) {
990
+ if (resolvePerModel(profile.parallelToolCalls, options.model, false)) {
689
991
  body.parallel_tool_calls = true;
690
992
  }
691
993
  }
692
- if (options.toolChoice !== void 0) {
693
- body.tool_choice = options.toolChoice;
694
- }
994
+ if (options.toolChoice !== void 0) body.tool_choice = options.toolChoice;
695
995
  if (options.responseFormat) {
696
- body.response_format = options.responseFormat;
996
+ const supportsSchema = resolvePerModel(
997
+ profile.capabilities?.supportsJsonSchema,
998
+ options.model,
999
+ true
1000
+ );
1001
+ body.response_format = options.responseFormat.type === "json_schema" && !supportsSchema ? { type: "json_object" } : options.responseFormat;
697
1002
  }
698
- if (wireEffort) body.reasoning_effort = wireEffort;
699
- if (stream) {
1003
+ if (stream && (profile.capabilities?.supportsStreamUsage ?? true)) {
700
1004
  body.stream_options = { include_usage: true };
701
1005
  }
702
- if (options.providerOptions) {
703
- Object.assign(body, options.providerOptions);
1006
+ if (profile.requestDefaults) Object.assign(body, profile.requestDefaults);
1007
+ const userKeys = options.providerOptions ? Object.keys(options.providerOptions) : [];
1008
+ if (options.providerOptions) Object.assign(body, options.providerOptions);
1009
+ const forbidden = resolvePerModel(profile.capabilities?.forbiddenParams, options.model, []);
1010
+ for (const key of forbidden) {
1011
+ if (!userKeys.includes(key)) delete body[key];
704
1012
  }
705
1013
  return body;
706
1014
  }
707
- /** Extract a human-readable message from an API error response body. */
1015
+ formatMessage(msg, model) {
1016
+ const out = {
1017
+ role: this.profile.roleFor ? this.profile.roleFor(msg.role, model) : msg.role,
1018
+ content: msg.content
1019
+ };
1020
+ if (msg.name && (this.profile.capabilities?.emitsMessageName ?? true)) out.name = msg.name;
1021
+ if (msg.tool_calls) out.tool_calls = msg.tool_calls;
1022
+ if (msg.tool_call_id) out.tool_call_id = msg.tool_call_id;
1023
+ if (this.profile.reasoning.roundTrip === "on-tool-call-turns" && msg.role === "assistant" && msg.tool_calls && msg.tool_calls.length > 0) {
1024
+ const rt = msg.providerMetadata?.openaiCompatReasoning;
1025
+ if (rt && rt.provider === this.name && isRoundTripField(rt.field) && rt.value !== void 0 && rt.value !== null) {
1026
+ out[rt.field] = rt.value;
1027
+ }
1028
+ }
1029
+ return out;
1030
+ }
1031
+ // ---------------------------------------------------------------------------
1032
+ // Response parsing
1033
+ // ---------------------------------------------------------------------------
708
1034
  extractErrorMessage(body, status) {
1035
+ const label = this.profile.label ?? this.profile.name;
709
1036
  try {
710
1037
  const json = JSON.parse(body);
711
- if (json.error?.message) {
712
- return `OpenAI API error (${status}): ${json.error.message}`;
713
- }
1038
+ const message = json.error?.message ?? json.message ?? json.detail;
1039
+ if (message) return `${label} API error (${status}): ${message}`;
714
1040
  } catch {
715
1041
  }
716
- return `OpenAI API error (${status}): ${body}`;
1042
+ return `${label} API error (${status}): ${body}`;
717
1043
  }
718
- formatMessage(msg, oSeries) {
719
- const out = {
720
- role: msg.role === "system" && oSeries ? "developer" : msg.role,
721
- content: msg.content
1044
+ toUsage(raw) {
1045
+ if (!raw) return void 0;
1046
+ return {
1047
+ prompt_tokens: raw.prompt_tokens,
1048
+ completion_tokens: raw.completion_tokens,
1049
+ total_tokens: raw.total_tokens,
1050
+ reasoning_tokens: raw.completion_tokens_details?.reasoning_tokens,
1051
+ cached_tokens: raw.prompt_tokens_details?.cached_tokens
722
1052
  };
723
- if (msg.name) out.name = msg.name;
724
- if (msg.tool_calls) out.tool_calls = msg.tool_calls;
725
- if (msg.tool_call_id) out.tool_call_id = msg.tool_call_id;
726
- return out;
727
1053
  }
1054
+ computeCost(model, usage, reportedCost) {
1055
+ const p = this.profile.pricing;
1056
+ if (p.kind === "zero") return 0;
1057
+ if (!usage) return void 0;
1058
+ switch (p.kind) {
1059
+ case "unknown":
1060
+ return void 0;
1061
+ case "from-response":
1062
+ return typeof reportedCost === "number" && Number.isFinite(reportedCost) ? reportedCost : void 0;
1063
+ case "table": {
1064
+ const c = priceFromTable(
1065
+ p.table,
1066
+ model,
1067
+ usage.prompt_tokens,
1068
+ usage.completion_tokens,
1069
+ usage.cached_tokens
1070
+ );
1071
+ return c !== void 0 && Number.isFinite(c) ? c : void 0;
1072
+ }
1073
+ default: {
1074
+ const _exhaustive = p;
1075
+ throw new Error(`Unhandled pricing source: ${JSON.stringify(_exhaustive)}`);
1076
+ }
1077
+ }
1078
+ }
1079
+ parseResponse(json, model) {
1080
+ const message = json.choices?.[0]?.message ?? {};
1081
+ let content = normalizeMessageContent(message.content);
1082
+ let thinking;
1083
+ let roundTrip;
1084
+ switch (this.profile.reasoning.capture) {
1085
+ case "reasoning_content":
1086
+ if (typeof message.reasoning_content === "string") {
1087
+ thinking = message.reasoning_content;
1088
+ roundTrip = {
1089
+ provider: this.name,
1090
+ field: "reasoning_content",
1091
+ value: message.reasoning_content
1092
+ };
1093
+ }
1094
+ break;
1095
+ case "reasoning":
1096
+ if (typeof message.reasoning === "string") {
1097
+ thinking = message.reasoning;
1098
+ roundTrip = { provider: this.name, field: "reasoning", value: message.reasoning };
1099
+ }
1100
+ break;
1101
+ case "reasoning_details":
1102
+ if (typeof message.reasoning === "string") thinking = message.reasoning;
1103
+ if (message.reasoning_details !== void 0) {
1104
+ roundTrip = {
1105
+ provider: this.name,
1106
+ field: "reasoning_details",
1107
+ value: message.reasoning_details
1108
+ };
1109
+ }
1110
+ break;
1111
+ case "think_tags": {
1112
+ const ex = extractThinkTags(content);
1113
+ content = ex.content;
1114
+ if (ex.thinking) thinking = ex.thinking;
1115
+ break;
1116
+ }
1117
+ case "none":
1118
+ break;
1119
+ default: {
1120
+ const _exhaustive = this.profile.reasoning.capture;
1121
+ throw new Error(`Unhandled reasoning capture: ${String(_exhaustive)}`);
1122
+ }
1123
+ }
1124
+ const usage = this.toUsage(json.usage);
1125
+ const cost = this.computeCost(model, usage, json.usage?.cost);
1126
+ return {
1127
+ content,
1128
+ thinking_content: thinking || void 0,
1129
+ tool_calls: message.tool_calls?.map((tc) => ({
1130
+ id: tc.id,
1131
+ type: "function",
1132
+ function: { name: tc.function.name, arguments: tc.function.arguments }
1133
+ })),
1134
+ usage,
1135
+ cost,
1136
+ providerMetadata: this.roundTripMetadata(roundTrip)
1137
+ };
1138
+ }
1139
+ roundTripMetadata(rt) {
1140
+ if (!rt || this.profile.reasoning.roundTrip === void 0 || this.profile.reasoning.roundTrip === "none") {
1141
+ return void 0;
1142
+ }
1143
+ return rt.value === void 0 || rt.value === null ? void 0 : { openaiCompatReasoning: rt };
1144
+ }
1145
+ // ---------------------------------------------------------------------------
1146
+ // SSE streaming
1147
+ // ---------------------------------------------------------------------------
728
1148
  async *parseSSEStream(body, model) {
729
1149
  const reader = body.getReader();
730
1150
  const decoder = new TextDecoder();
731
1151
  let buffer = "";
732
1152
  let usageData;
1153
+ const capture = this.profile.reasoning.capture;
1154
+ const thinkScanner = capture === "think_tags" ? new ThinkTagScanner() : void 0;
1155
+ let reasoningText = "";
1156
+ const reasoningDetails = [];
733
1157
  const indexToId = /* @__PURE__ */ new Map();
1158
+ const pendingToolCalls = /* @__PURE__ */ new Map();
1159
+ const flushPendingToolCalls = () => {
1160
+ const out = [];
1161
+ for (const [index, buf] of pendingToolCalls) {
1162
+ out.push({
1163
+ type: "tool_call_delta",
1164
+ id: `__toolcall_${index}`,
1165
+ name: buf.name,
1166
+ arguments: buf.arguments
1167
+ });
1168
+ }
1169
+ pendingToolCalls.clear();
1170
+ return out;
1171
+ };
1172
+ const makeDone = () => {
1173
+ const usage = this.toUsage(usageData);
1174
+ let providerMetadata;
1175
+ if (this.profile.reasoning.roundTrip === "on-tool-call-turns") {
1176
+ if (capture === "reasoning_details" && reasoningDetails.length > 0) {
1177
+ providerMetadata = {
1178
+ openaiCompatReasoning: {
1179
+ provider: this.name,
1180
+ field: "reasoning_details",
1181
+ value: reasoningDetails
1182
+ }
1183
+ };
1184
+ } else if ((capture === "reasoning_content" || capture === "reasoning") && reasoningText) {
1185
+ providerMetadata = {
1186
+ openaiCompatReasoning: { provider: this.name, field: capture, value: reasoningText }
1187
+ };
1188
+ }
1189
+ }
1190
+ return {
1191
+ type: "done",
1192
+ usage,
1193
+ cost: this.computeCost(model, usage, usageData?.cost),
1194
+ providerMetadata
1195
+ };
1196
+ };
734
1197
  try {
735
- while (true) {
1198
+ for (; ; ) {
736
1199
  const { done, value } = await reader.read();
737
1200
  if (done) break;
738
1201
  buffer += decoder.decode(value, { stream: true });
@@ -742,47 +1205,76 @@ var OpenAIProvider = class {
742
1205
  const trimmed = line.trim();
743
1206
  if (!trimmed || trimmed.startsWith(":")) continue;
744
1207
  if (trimmed === "data: [DONE]") {
745
- yield {
746
- type: "done",
747
- usage: usageData,
748
- cost: usageData ? estimateOpenAICost(
749
- model,
750
- usageData.prompt_tokens,
751
- usageData.completion_tokens,
752
- usageData.cached_tokens
753
- ) : void 0
754
- };
1208
+ if (thinkScanner) {
1209
+ const flushed = thinkScanner.flush();
1210
+ if (flushed.text) yield { type: "text_delta", content: flushed.text };
1211
+ if (flushed.thinking) yield { type: "thinking_delta", content: flushed.thinking };
1212
+ }
1213
+ yield* flushPendingToolCalls();
1214
+ yield makeDone();
755
1215
  return;
756
1216
  }
757
- if (trimmed.startsWith("data: ")) {
758
- const jsonStr = trimmed.slice(6);
759
- let parsed;
760
- try {
761
- parsed = JSON.parse(jsonStr);
762
- } catch {
763
- continue;
764
- }
765
- if (parsed.usage) {
766
- usageData = {
767
- prompt_tokens: parsed.usage.prompt_tokens,
768
- completion_tokens: parsed.usage.completion_tokens,
769
- total_tokens: parsed.usage.total_tokens,
770
- reasoning_tokens: parsed.usage.completion_tokens_details?.reasoning_tokens,
771
- cached_tokens: parsed.usage.prompt_tokens_details?.cached_tokens
772
- };
773
- continue;
774
- }
775
- const delta = parsed.choices?.[0]?.delta;
776
- if (!delta) continue;
777
- if (delta.content) {
1217
+ if (!trimmed.startsWith("data: ")) continue;
1218
+ let parsed;
1219
+ try {
1220
+ parsed = JSON.parse(trimmed.slice(6));
1221
+ } catch {
1222
+ continue;
1223
+ }
1224
+ if (parsed.error) {
1225
+ const message = typeof parsed.error.message === "string" ? parsed.error.message : `${this.profile.label ?? this.profile.name} stream error`;
1226
+ throw new ProviderError({
1227
+ provider: this.name,
1228
+ status: 0,
1229
+ retryable: false,
1230
+ message,
1231
+ body: JSON.stringify(parsed.error)
1232
+ });
1233
+ }
1234
+ if (parsed.usage) {
1235
+ usageData = parsed.usage;
1236
+ if (!parsed.choices || parsed.choices.length === 0) continue;
1237
+ }
1238
+ const delta = parsed.choices?.[0]?.delta;
1239
+ if (!delta) continue;
1240
+ const deltaReasoning = capture === "reasoning_content" ? delta.reasoning_content : capture === "reasoning" || capture === "reasoning_details" ? delta.reasoning : void 0;
1241
+ if (typeof deltaReasoning === "string" && deltaReasoning.length > 0) {
1242
+ reasoningText += deltaReasoning;
1243
+ yield { type: "thinking_delta", content: deltaReasoning };
1244
+ }
1245
+ if (capture === "reasoning_details" && Array.isArray(delta.reasoning_details)) {
1246
+ reasoningDetails.push(...delta.reasoning_details);
1247
+ }
1248
+ if (typeof delta.content === "string" && delta.content.length > 0) {
1249
+ if (thinkScanner) {
1250
+ const { text, thinking } = thinkScanner.push(delta.content);
1251
+ if (text) yield { type: "text_delta", content: text };
1252
+ if (thinking) yield { type: "thinking_delta", content: thinking };
1253
+ } else {
778
1254
  yield { type: "text_delta", content: delta.content };
779
1255
  }
780
- if (delta.tool_calls) {
781
- for (const tc of delta.tool_calls) {
782
- if (tc.id) {
783
- indexToId.set(tc.index, tc.id);
784
- }
785
- const id = indexToId.get(tc.index) ?? `__pending_${tc.index}`;
1256
+ }
1257
+ if (delta.tool_calls) {
1258
+ for (const tc of delta.tool_calls) {
1259
+ if (tc.id) indexToId.set(tc.index, tc.id);
1260
+ const id = indexToId.get(tc.index);
1261
+ if (id === void 0) {
1262
+ const buf2 = pendingToolCalls.get(tc.index) ?? { arguments: "" };
1263
+ if (tc.function?.name) buf2.name = tc.function.name;
1264
+ if (tc.function?.arguments) buf2.arguments += tc.function.arguments;
1265
+ pendingToolCalls.set(tc.index, buf2);
1266
+ continue;
1267
+ }
1268
+ const buf = pendingToolCalls.get(tc.index);
1269
+ if (buf) {
1270
+ pendingToolCalls.delete(tc.index);
1271
+ yield {
1272
+ type: "tool_call_delta",
1273
+ id,
1274
+ name: buf.name ?? tc.function?.name,
1275
+ arguments: buf.arguments + (tc.function?.arguments ?? "")
1276
+ };
1277
+ } else {
786
1278
  yield {
787
1279
  type: "tool_call_delta",
788
1280
  id,
@@ -794,58 +1286,176 @@ var OpenAIProvider = class {
794
1286
  }
795
1287
  }
796
1288
  }
797
- yield {
798
- type: "done",
799
- usage: usageData,
800
- cost: usageData ? estimateOpenAICost(
801
- model,
802
- usageData.prompt_tokens,
803
- usageData.completion_tokens,
804
- usageData.cached_tokens
805
- ) : void 0
806
- };
1289
+ if (thinkScanner) {
1290
+ const flushed = thinkScanner.flush();
1291
+ if (flushed.text) yield { type: "text_delta", content: flushed.text };
1292
+ if (flushed.thinking) yield { type: "thinking_delta", content: flushed.thinking };
1293
+ }
1294
+ yield* flushPendingToolCalls();
1295
+ throw new ProviderError({
1296
+ provider: this.name,
1297
+ status: 0,
1298
+ retryable: true,
1299
+ message: `${this.profile.label ?? this.profile.name} stream ended before [DONE]`
1300
+ });
807
1301
  } finally {
808
1302
  reader.releaseLock();
809
1303
  }
810
1304
  }
811
1305
  };
812
1306
 
1307
+ // src/providers/openai.ts
1308
+ var OPENAI_PRICING = {
1309
+ // gpt-4o era — cache reads at 50% of input rate
1310
+ "gpt-4o": [25e-7, 1e-5, 0.5],
1311
+ "gpt-4o-mini": [15e-8, 6e-7, 0.5],
1312
+ "gpt-4-turbo": [1e-5, 3e-5, 0.5],
1313
+ "gpt-4": [3e-5, 6e-5, 0.5],
1314
+ "gpt-3.5-turbo": [5e-7, 15e-7, 0.5],
1315
+ o1: [15e-6, 6e-5, 0.5],
1316
+ "o1-mini": [3e-6, 12e-6, 0.5],
1317
+ "o1-pro": [15e-5, 6e-4, 0.5],
1318
+ // gpt-4.1 / o3 / o4 era — cache reads at 25% of input rate
1319
+ "gpt-4.1": [2e-6, 8e-6, 0.25],
1320
+ "gpt-4.1-mini": [4e-7, 16e-7, 0.25],
1321
+ "gpt-4.1-nano": [1e-7, 4e-7, 0.25],
1322
+ o3: [1e-5, 4e-5, 0.25],
1323
+ "o3-mini": [11e-7, 44e-7, 0.25],
1324
+ "o3-pro": [2e-5, 8e-5, 0.25],
1325
+ "o4-mini": [11e-7, 44e-7, 0.25],
1326
+ // gpt-5 era — cache reads at 10% of input rate
1327
+ "gpt-5": [125e-8, 1e-5, 0.1],
1328
+ "gpt-5-mini": [25e-8, 2e-6, 0.1],
1329
+ "gpt-5-nano": [5e-8, 4e-7, 0.1],
1330
+ "gpt-5.1": [125e-8, 1e-5, 0.1],
1331
+ "gpt-5.2": [175e-8, 14e-6, 0.1],
1332
+ "gpt-5.3": [175e-8, 14e-6, 0.1],
1333
+ "gpt-5.4": [25e-7, 15e-6, 0.1],
1334
+ "gpt-5.4-pro": [3e-5, 18e-5, 0.1],
1335
+ // gpt-5.5 era (flagship released 2026-04) — cache reads at 10% of input rate
1336
+ "gpt-5.5": [5e-6, 3e-5, 0.1],
1337
+ "gpt-5.5-pro": [3e-5, 18e-5, 0.1]
1338
+ };
1339
+ function estimateOpenAICost(model, promptTokens, completionTokens, cachedTokens) {
1340
+ return priceFromTable(OPENAI_PRICING, model, promptTokens, completionTokens, cachedTokens);
1341
+ }
1342
+ function isOSeriesModel(model) {
1343
+ return /^(o1|o3|o4-mini)/.test(model);
1344
+ }
1345
+ function supportsReasoningEffort(model) {
1346
+ return isOSeriesModel(model) || /^gpt-5/.test(model);
1347
+ }
1348
+ function supportsReasoningNone(model) {
1349
+ return /^gpt-5\.[1-9]/.test(model);
1350
+ }
1351
+ function supportsXhigh(model) {
1352
+ return /^gpt-5\.([2-9]|\d{2,})/.test(model);
1353
+ }
1354
+ function clampReasoningEffort(model, effort) {
1355
+ if (model.startsWith("gpt-5-pro")) return "high";
1356
+ if (effort === "none" && !supportsReasoningNone(model)) return "minimal";
1357
+ if (effort === "xhigh" && !supportsXhigh(model)) return "high";
1358
+ return effort;
1359
+ }
1360
+ function effortToReasoningEffort(effort) {
1361
+ return effort === "max" ? "xhigh" : effort;
1362
+ }
1363
+ function budgetToReasoningEffort(budget) {
1364
+ if (budget <= 1024) return "low";
1365
+ if (budget <= 8192) return "medium";
1366
+ return "high";
1367
+ }
1368
+ var openaiReasoningEmit = (body, resolved, model) => {
1369
+ const oSeries = isOSeriesModel(model);
1370
+ const reasoningCapable = supportsReasoningEffort(model);
1371
+ const { thinkingBudget, thinkingDisabled, activeEffort, hasBudgetOverride } = resolved;
1372
+ let wireEffort;
1373
+ if (reasoningCapable) {
1374
+ if (hasBudgetOverride) {
1375
+ wireEffort = clampReasoningEffort(model, budgetToReasoningEffort(thinkingBudget));
1376
+ } else if (!thinkingDisabled && activeEffort) {
1377
+ wireEffort = clampReasoningEffort(model, effortToReasoningEffort(activeEffort));
1378
+ } else if (thinkingDisabled) {
1379
+ wireEffort = clampReasoningEffort(model, "none");
1380
+ }
1381
+ }
1382
+ if (wireEffort) body.reasoning_effort = wireEffort;
1383
+ return { stripTemperature: oSeries || reasoningCapable && wireEffort !== void 0 };
1384
+ };
1385
+ var OPENAI_PROFILE = {
1386
+ name: "openai",
1387
+ label: "OpenAI",
1388
+ defaultBaseUrl: "https://api.openai.com/v1",
1389
+ envApiKey: "OPENAI_API_KEY",
1390
+ envBaseUrl: "OPENAI_BASE_URL",
1391
+ pricing: { kind: "table", table: OPENAI_PRICING },
1392
+ reasoning: { emit: openaiReasoningEmit, capture: "none" },
1393
+ roleFor: (role, model) => role === "system" && isOSeriesModel(model) ? "developer" : role,
1394
+ maxTokensField: "max_completion_tokens",
1395
+ parallelToolCalls: (model) => !isOSeriesModel(model)
1396
+ };
1397
+ var OpenAIProvider = class extends OpenAICompatibleProvider {
1398
+ constructor(options = {}) {
1399
+ super({ profile: OPENAI_PROFILE, ...options });
1400
+ }
1401
+ };
1402
+
813
1403
  // src/providers/openai-responses.ts
814
1404
  var OpenAIResponsesProvider = class {
815
1405
  name = "openai-responses";
1406
+ /** The Responses API honors `json_schema` (structured outputs) natively. */
1407
+ nativeStructuredOutputSupport() {
1408
+ return "schema";
1409
+ }
816
1410
  baseUrl;
817
- apiKey;
1411
+ apiKeySource;
818
1412
  governor;
819
1413
  constructor(options = {}) {
820
- this.apiKey = options.apiKey ?? process.env.OPENAI_API_KEY ?? "";
1414
+ this.apiKeySource = options.apiKey ?? process.env.OPENAI_API_KEY ?? "";
821
1415
  this.baseUrl = (options.baseUrl ?? process.env.OPENAI_BASE_URL ?? "https://api.openai.com/v1").replace(/\/$/, "");
822
1416
  this.governor = options.rateLimit ? new RateLimiter(options.rateLimit) : void 0;
823
- if (!this.apiKey) {
1417
+ if (typeof this.apiKeySource === "string" && !this.apiKeySource) {
824
1418
  throw new Error("OpenAI API key is required. Set OPENAI_API_KEY or pass apiKey in options.");
825
1419
  }
826
1420
  }
1421
+ /** Resolve the API key for one request (supports an expiring-token callback). */
1422
+ async resolveKey() {
1423
+ const key = await resolveApiKey(this.apiKeySource);
1424
+ if (!key) {
1425
+ throw new Error("OpenAI API key is required. Set OPENAI_API_KEY or pass apiKey in options.");
1426
+ }
1427
+ return key;
1428
+ }
1429
+ /** Build request headers from a resolved key (used by both chat and stream). */
1430
+ buildHeaders(apiKey) {
1431
+ return { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` };
1432
+ }
827
1433
  // ---------------------------------------------------------------------------
828
1434
  // chat - non-streaming
829
1435
  // ---------------------------------------------------------------------------
830
1436
  async chat(messages, options) {
1437
+ const headers = this.buildHeaders(await this.resolveKey());
831
1438
  const body = this.buildRequestBody(messages, options, false);
832
1439
  const res = await fetchWithRetry(
833
1440
  `${this.baseUrl}/responses`,
834
1441
  {
835
1442
  method: "POST",
836
- headers: {
837
- "Content-Type": "application/json",
838
- Authorization: `Bearer ${this.apiKey}`
839
- },
1443
+ headers,
840
1444
  body: JSON.stringify(body),
841
1445
  signal: options.signal
842
1446
  },
843
- { governor: this.governor }
1447
+ { governor: this.governor, provider: this.name }
844
1448
  );
845
1449
  if (!res.ok) {
846
1450
  const errorBody = await res.text();
847
1451
  const message = this.extractErrorMessage(errorBody, res.status);
848
- throw new Error(message);
1452
+ throw buildProviderError({
1453
+ provider: this.name,
1454
+ status: res.status,
1455
+ headers: res.headers,
1456
+ message,
1457
+ body: errorBody
1458
+ });
849
1459
  }
850
1460
  const json = await res.json();
851
1461
  return this.parseResponse(json, options.model);
@@ -854,24 +1464,28 @@ var OpenAIResponsesProvider = class {
854
1464
  // stream - SSE streaming
855
1465
  // ---------------------------------------------------------------------------
856
1466
  async *stream(messages, options) {
1467
+ const headers = this.buildHeaders(await this.resolveKey());
857
1468
  const body = this.buildRequestBody(messages, options, true);
858
1469
  const res = await fetchWithRetry(
859
1470
  `${this.baseUrl}/responses`,
860
1471
  {
861
1472
  method: "POST",
862
- headers: {
863
- "Content-Type": "application/json",
864
- Authorization: `Bearer ${this.apiKey}`
865
- },
1473
+ headers,
866
1474
  body: JSON.stringify(body),
867
1475
  signal: options.signal
868
1476
  },
869
- { governor: this.governor }
1477
+ { governor: this.governor, provider: this.name }
870
1478
  );
871
1479
  if (!res.ok) {
872
1480
  const errorBody = await res.text();
873
1481
  const message = this.extractErrorMessage(errorBody, res.status);
874
- throw new Error(message);
1482
+ throw buildProviderError({
1483
+ provider: this.name,
1484
+ status: res.status,
1485
+ headers: res.headers,
1486
+ message,
1487
+ body: errorBody
1488
+ });
875
1489
  }
876
1490
  if (!res.body) {
877
1491
  throw new Error("OpenAI Responses stream has no body");
@@ -1160,7 +1774,12 @@ var OpenAIResponsesProvider = class {
1160
1774
  }
1161
1775
  case "response.failed": {
1162
1776
  const errorMsg = data.response?.error?.message ?? data.response?.status_details?.error?.message ?? "Unknown error";
1163
- throw new Error(`OpenAI Responses API error: ${errorMsg}`);
1777
+ throw new ProviderError({
1778
+ provider: this.name,
1779
+ status: 0,
1780
+ retryable: false,
1781
+ message: `OpenAI Responses API error: ${errorMsg}`
1782
+ });
1164
1783
  }
1165
1784
  default:
1166
1785
  return null;
@@ -1214,7 +1833,7 @@ function estimateAnthropicCost(model, inputTokens, outputTokens, cacheReadTokens
1214
1833
  }
1215
1834
  }
1216
1835
  }
1217
- if (!pricing) return 0;
1836
+ if (!pricing) return void 0;
1218
1837
  const [inputRate, outputRate] = pricing;
1219
1838
  const cacheRead = cacheReadTokens ?? 0;
1220
1839
  const cacheWrite = cacheWriteTokens ?? 0;
@@ -1248,63 +1867,90 @@ function clampAnthropicEffort(model, effort) {
1248
1867
  }
1249
1868
  var AnthropicProvider = class {
1250
1869
  name = "anthropic";
1870
+ /** Anthropic ignores native `json_schema` structurally — Axl uses a system
1871
+ * prompt JSON instruction + client-side Zod validation (see buildRequest). */
1872
+ nativeStructuredOutputSupport() {
1873
+ return "unsupported";
1874
+ }
1251
1875
  baseUrl;
1252
- apiKey;
1253
- currentModel;
1876
+ apiKeySource;
1254
1877
  governor;
1255
1878
  constructor(options = {}) {
1256
- this.apiKey = options.apiKey ?? process.env.ANTHROPIC_API_KEY ?? "";
1879
+ this.apiKeySource = options.apiKey ?? process.env.ANTHROPIC_API_KEY ?? "";
1257
1880
  this.baseUrl = (options.baseUrl ?? "https://api.anthropic.com/v1").replace(/\/$/, "");
1258
1881
  this.governor = options.rateLimit ? new RateLimiter(options.rateLimit) : void 0;
1259
- if (!this.apiKey) {
1882
+ if (typeof this.apiKeySource === "string" && !this.apiKeySource) {
1883
+ throw new Error(
1884
+ "Anthropic API key is required. Set ANTHROPIC_API_KEY or pass apiKey in options."
1885
+ );
1886
+ }
1887
+ }
1888
+ /** Resolve the API key for one request (supports an expiring-token callback). */
1889
+ async resolveKey() {
1890
+ const key = await resolveApiKey(this.apiKeySource);
1891
+ if (!key) {
1260
1892
  throw new Error(
1261
1893
  "Anthropic API key is required. Set ANTHROPIC_API_KEY or pass apiKey in options."
1262
1894
  );
1263
1895
  }
1896
+ return key;
1264
1897
  }
1265
1898
  // ---------------------------------------------------------------------------
1266
1899
  // chat - non-streaming completion
1267
1900
  // ---------------------------------------------------------------------------
1268
1901
  async chat(messages, options) {
1269
- this.currentModel = options.model;
1902
+ const headers = this.buildHeaders(await this.resolveKey());
1270
1903
  const body = this.buildRequestBody(messages, options, false);
1271
1904
  const res = await fetchWithRetry(
1272
1905
  `${this.baseUrl}/messages`,
1273
1906
  {
1274
1907
  method: "POST",
1275
- headers: this.buildHeaders(),
1908
+ headers,
1276
1909
  body: JSON.stringify(body),
1277
1910
  signal: options.signal
1278
1911
  },
1279
- { governor: this.governor }
1912
+ { governor: this.governor, provider: this.name }
1280
1913
  );
1281
1914
  if (!res.ok) {
1282
1915
  const errorBody = await res.text();
1283
1916
  const message = this.extractErrorMessage(errorBody, res.status);
1284
- throw new Error(message);
1917
+ throw buildProviderError({
1918
+ provider: this.name,
1919
+ status: res.status,
1920
+ headers: res.headers,
1921
+ message,
1922
+ body: errorBody
1923
+ });
1285
1924
  }
1286
1925
  const json = await res.json();
1287
- return this.parseResponse(json);
1926
+ return this.parseResponse(json, options.model);
1288
1927
  }
1289
1928
  // ---------------------------------------------------------------------------
1290
1929
  // stream - SSE streaming completion
1291
1930
  // ---------------------------------------------------------------------------
1292
1931
  async *stream(messages, options) {
1932
+ const headers = this.buildHeaders(await this.resolveKey());
1293
1933
  const body = this.buildRequestBody(messages, options, true);
1294
1934
  const res = await fetchWithRetry(
1295
1935
  `${this.baseUrl}/messages`,
1296
1936
  {
1297
1937
  method: "POST",
1298
- headers: this.buildHeaders(),
1938
+ headers,
1299
1939
  body: JSON.stringify(body),
1300
1940
  signal: options.signal
1301
1941
  },
1302
- { governor: this.governor }
1942
+ { governor: this.governor, provider: this.name }
1303
1943
  );
1304
1944
  if (!res.ok) {
1305
1945
  const errorBody = await res.text();
1306
1946
  const message = this.extractErrorMessage(errorBody, res.status);
1307
- throw new Error(message);
1947
+ throw buildProviderError({
1948
+ provider: this.name,
1949
+ status: res.status,
1950
+ headers: res.headers,
1951
+ message,
1952
+ body: errorBody
1953
+ });
1308
1954
  }
1309
1955
  if (!res.body) {
1310
1956
  throw new Error("Anthropic stream response has no body");
@@ -1314,10 +1960,10 @@ var AnthropicProvider = class {
1314
1960
  // ---------------------------------------------------------------------------
1315
1961
  // Internal: request building
1316
1962
  // ---------------------------------------------------------------------------
1317
- buildHeaders() {
1963
+ buildHeaders(apiKey) {
1318
1964
  return {
1319
1965
  "Content-Type": "application/json",
1320
- "x-api-key": this.apiKey,
1966
+ "x-api-key": apiKey,
1321
1967
  "anthropic-version": ANTHROPIC_API_VERSION
1322
1968
  };
1323
1969
  }
@@ -1502,7 +2148,7 @@ ${jsonInstruction}` : jsonInstruction;
1502
2148
  // ---------------------------------------------------------------------------
1503
2149
  // Internal: response parsing
1504
2150
  // ---------------------------------------------------------------------------
1505
- parseResponse(json) {
2151
+ parseResponse(json, model) {
1506
2152
  let content = "";
1507
2153
  let thinkingContent = "";
1508
2154
  const toolCalls = [];
@@ -1531,13 +2177,7 @@ ${jsonInstruction}` : jsonInstruction;
1531
2177
  total_tokens: totalInput + json.usage.output_tokens,
1532
2178
  cached_tokens: cacheRead > 0 ? cacheRead : void 0
1533
2179
  } : void 0;
1534
- const cost = json.usage ? estimateAnthropicCost(
1535
- this.currentModel ?? "",
1536
- totalInput,
1537
- json.usage.output_tokens,
1538
- cacheRead,
1539
- cacheWrite
1540
- ) : void 0;
2180
+ const cost = json.usage ? estimateAnthropicCost(model, totalInput, json.usage.output_tokens, cacheRead, cacheWrite) : void 0;
1541
2181
  return {
1542
2182
  content,
1543
2183
  thinking_content: thinkingContent || void 0,
@@ -1739,7 +2379,7 @@ function estimateGeminiCost(model, inputTokens, outputTokens, cachedTokens) {
1739
2379
  }
1740
2380
  }
1741
2381
  }
1742
- if (!pricing) return 0;
2382
+ if (!pricing) return void 0;
1743
2383
  const [inputRate, outputRate] = pricing;
1744
2384
  const cached = cachedTokens ?? 0;
1745
2385
  const inputCost = (inputTokens - cached) * inputRate + cached * inputRate * 0.1;
@@ -1784,40 +2424,61 @@ function warnGemini3xEffortNone(model) {
1784
2424
  }
1785
2425
  var GeminiProvider = class {
1786
2426
  name = "google";
2427
+ /** Gemini accepts a `responseSchema` but `sanitizeSchemaForGemini` strips
2428
+ * keywords it doesn't support (`$ref`/`$defs`/`additionalProperties`/…), so a
2429
+ * derived schema can lose constraints — lossy, not faithful. */
2430
+ nativeStructuredOutputSupport() {
2431
+ return "lossy";
2432
+ }
1787
2433
  baseUrl;
1788
- apiKey;
2434
+ apiKeySource;
1789
2435
  callCounter = 0;
1790
2436
  governor;
1791
2437
  constructor(options = {}) {
1792
- this.apiKey = options.apiKey ?? process.env.GOOGLE_API_KEY ?? process.env.GEMINI_API_KEY ?? "";
2438
+ this.apiKeySource = options.apiKey ?? process.env.GOOGLE_API_KEY ?? process.env.GEMINI_API_KEY ?? "";
1793
2439
  this.baseUrl = (options.baseUrl ?? "https://generativelanguage.googleapis.com/v1beta").replace(
1794
2440
  /\/$/,
1795
2441
  ""
1796
2442
  );
1797
2443
  this.governor = options.rateLimit ? new RateLimiter(options.rateLimit) : void 0;
1798
- if (!this.apiKey) {
2444
+ if (typeof this.apiKeySource === "string" && !this.apiKeySource) {
2445
+ throw new Error("Google API key is required. Set GOOGLE_API_KEY or pass apiKey in options.");
2446
+ }
2447
+ }
2448
+ /** Resolve the API key for one request (supports an expiring-token callback). */
2449
+ async resolveKey() {
2450
+ const key = await resolveApiKey(this.apiKeySource);
2451
+ if (!key) {
1799
2452
  throw new Error("Google API key is required. Set GOOGLE_API_KEY or pass apiKey in options.");
1800
2453
  }
2454
+ return key;
1801
2455
  }
1802
2456
  // ---------------------------------------------------------------------------
1803
2457
  // chat - non-streaming completion
1804
2458
  // ---------------------------------------------------------------------------
1805
2459
  async chat(messages, options) {
2460
+ const headers = this.buildHeaders(await this.resolveKey());
1806
2461
  const body = this.buildRequestBody(messages, options);
1807
2462
  const res = await fetchWithRetry(
1808
2463
  `${this.baseUrl}/models/${options.model}:generateContent`,
1809
2464
  {
1810
2465
  method: "POST",
1811
- headers: this.buildHeaders(),
2466
+ headers,
1812
2467
  body: JSON.stringify(body),
1813
2468
  signal: options.signal
1814
2469
  },
1815
- { governor: this.governor }
2470
+ { governor: this.governor, provider: this.name }
1816
2471
  );
1817
2472
  if (!res.ok) {
1818
2473
  const errorBody = await res.text();
1819
2474
  const message = this.extractErrorMessage(errorBody, res.status);
1820
- throw new Error(message);
2475
+ throw buildProviderError({
2476
+ provider: this.name,
2477
+ status: res.status,
2478
+ headers: res.headers,
2479
+ message,
2480
+ body: errorBody
2481
+ });
1821
2482
  }
1822
2483
  const json = await res.json();
1823
2484
  return this.parseResponse(json, options.model);
@@ -1826,21 +2487,28 @@ var GeminiProvider = class {
1826
2487
  // stream - SSE streaming completion
1827
2488
  // ---------------------------------------------------------------------------
1828
2489
  async *stream(messages, options) {
2490
+ const headers = this.buildHeaders(await this.resolveKey());
1829
2491
  const body = this.buildRequestBody(messages, options);
1830
2492
  const res = await fetchWithRetry(
1831
2493
  `${this.baseUrl}/models/${options.model}:streamGenerateContent?alt=sse`,
1832
2494
  {
1833
2495
  method: "POST",
1834
- headers: this.buildHeaders(),
2496
+ headers,
1835
2497
  body: JSON.stringify(body),
1836
2498
  signal: options.signal
1837
2499
  },
1838
- { governor: this.governor }
2500
+ { governor: this.governor, provider: this.name }
1839
2501
  );
1840
2502
  if (!res.ok) {
1841
2503
  const errorBody = await res.text();
1842
2504
  const message = this.extractErrorMessage(errorBody, res.status);
1843
- throw new Error(message);
2505
+ throw buildProviderError({
2506
+ provider: this.name,
2507
+ status: res.status,
2508
+ headers: res.headers,
2509
+ message,
2510
+ body: errorBody
2511
+ });
1844
2512
  }
1845
2513
  if (!res.body) {
1846
2514
  throw new Error("Gemini stream response has no body");
@@ -1850,10 +2518,10 @@ var GeminiProvider = class {
1850
2518
  // ---------------------------------------------------------------------------
1851
2519
  // Internal: request building
1852
2520
  // ---------------------------------------------------------------------------
1853
- buildHeaders() {
2521
+ buildHeaders(apiKey) {
1854
2522
  return {
1855
2523
  "Content-Type": "application/json",
1856
- "x-goog-api-key": this.apiKey
2524
+ "x-goog-api-key": apiKey
1857
2525
  };
1858
2526
  }
1859
2527
  extractErrorMessage(body, status) {
@@ -2210,6 +2878,203 @@ var GeminiProvider = class {
2210
2878
  }
2211
2879
  };
2212
2880
 
2881
+ // src/providers/profiles/openrouter.ts
2882
+ var OPENROUTER_PROFILE = {
2883
+ name: "openrouter",
2884
+ label: "OpenRouter",
2885
+ defaultBaseUrl: "https://openrouter.ai/api/v1",
2886
+ envApiKey: "OPENROUTER_API_KEY",
2887
+ envBaseUrl: "OPENROUTER_BASE_URL",
2888
+ pricing: { kind: "from-response" },
2889
+ // Ask OpenRouter to report per-call cost in usage.cost.
2890
+ requestDefaults: { usage: { include: true } },
2891
+ reasoning: {
2892
+ // OpenRouter's effort vocabulary is low|medium|high; xhigh/max clamp to high.
2893
+ emit: reasoningObjectEmit(
2894
+ (effort) => effort === "xhigh" || effort === "max" ? "high" : effort
2895
+ ),
2896
+ capture: "reasoning_details",
2897
+ roundTrip: "on-tool-call-turns"
2898
+ },
2899
+ // OpenRouter normalizes to max_tokens across its backends.
2900
+ maxTokensField: "max_tokens"
2901
+ };
2902
+
2903
+ // src/providers/profiles/azure.ts
2904
+ var AZURE_PROFILE = {
2905
+ name: "azure",
2906
+ label: "Azure OpenAI",
2907
+ // Never actually used — requireExplicitBaseUrl forces a real value first.
2908
+ defaultBaseUrl: "https://YOUR-RESOURCE.openai.azure.com/openai/v1",
2909
+ requireExplicitBaseUrl: true,
2910
+ envApiKey: "AZURE_OPENAI_API_KEY",
2911
+ envBaseUrl: "AZURE_OPENAI_BASE_URL",
2912
+ authHeader: "api-key",
2913
+ pricing: { kind: "table", table: OPENAI_PRICING },
2914
+ reasoning: { emit: openaiReasoningEmit, capture: "none" },
2915
+ roleFor: (role, model) => role === "system" && isOSeriesModel(model) ? "developer" : role,
2916
+ maxTokensField: "max_completion_tokens",
2917
+ parallelToolCalls: (model) => !isOSeriesModel(model)
2918
+ };
2919
+
2920
+ // src/providers/profiles/xai.ts
2921
+ var acceptsReasoningEffort = (model) => model.startsWith("grok-3-mini");
2922
+ var isReasoningModel = (model) => /^grok-(3-mini|4)/.test(model);
2923
+ var XAI_PROFILE = {
2924
+ name: "xai",
2925
+ label: "xAI",
2926
+ defaultBaseUrl: "https://api.x.ai/v1",
2927
+ envApiKey: "XAI_API_KEY",
2928
+ envBaseUrl: "XAI_BASE_URL",
2929
+ pricing: { kind: "unknown" },
2930
+ reasoning: {
2931
+ emit: reasoningEffortEmit((resolved, model) => {
2932
+ if (!acceptsReasoningEffort(model)) return void 0;
2933
+ if (resolved.thinkingDisabled || !resolved.activeEffort) return void 0;
2934
+ return resolved.activeEffort === "low" ? "low" : "high";
2935
+ }),
2936
+ capture: "reasoning"
2937
+ },
2938
+ capabilities: {
2939
+ forbiddenParams: (model) => isReasoningModel(model) ? ["stop", "presence_penalty", "frequency_penalty"] : []
2940
+ }
2941
+ };
2942
+
2943
+ // src/providers/profiles/deepseek.ts
2944
+ var isReasoner = (model) => /reasoner|v4/i.test(model);
2945
+ var deepseekEmit = (_body, _resolved, model) => ({
2946
+ stripTemperature: isReasoner(model)
2947
+ });
2948
+ var DEEPSEEK_PROFILE = {
2949
+ name: "deepseek",
2950
+ label: "DeepSeek",
2951
+ defaultBaseUrl: "https://api.deepseek.com/v1",
2952
+ envApiKey: "DEEPSEEK_API_KEY",
2953
+ envBaseUrl: "DEEPSEEK_BASE_URL",
2954
+ pricing: { kind: "unknown" },
2955
+ reasoning: {
2956
+ emit: deepseekEmit,
2957
+ capture: "reasoning_content",
2958
+ roundTrip: "on-tool-call-turns"
2959
+ },
2960
+ capabilities: {
2961
+ supportsJsonSchema: false,
2962
+ forbiddenParams: (model) => isReasoner(model) ? ["top_p", "presence_penalty", "frequency_penalty"] : []
2963
+ }
2964
+ };
2965
+
2966
+ // src/providers/profiles/mistral.ts
2967
+ var acceptsReasoningEffort2 = (model) => /^mistral-(small|medium)/i.test(model);
2968
+ var MISTRAL_PROFILE = {
2969
+ name: "mistral",
2970
+ label: "Mistral",
2971
+ defaultBaseUrl: "https://api.mistral.ai/v1",
2972
+ envApiKey: "MISTRAL_API_KEY",
2973
+ envBaseUrl: "MISTRAL_BASE_URL",
2974
+ pricing: { kind: "unknown" },
2975
+ reasoning: {
2976
+ emit: reasoningEffortEmit((resolved, model) => {
2977
+ if (!acceptsReasoningEffort2(model)) return void 0;
2978
+ if (resolved.thinkingDisabled || !resolved.activeEffort) return void 0;
2979
+ return "high";
2980
+ }),
2981
+ capture: "reasoning_content"
2982
+ }
2983
+ };
2984
+
2985
+ // src/providers/profiles/groq.ts
2986
+ var isGptOss = (model) => /gpt-oss/i.test(model);
2987
+ var GROQ_PROFILE = {
2988
+ name: "groq",
2989
+ label: "Groq",
2990
+ defaultBaseUrl: "https://api.groq.com/openai/v1",
2991
+ envApiKey: "GROQ_API_KEY",
2992
+ envBaseUrl: "GROQ_BASE_URL",
2993
+ pricing: { kind: "unknown" },
2994
+ reasoning: {
2995
+ emit: reasoningEffortEmit((resolved, model) => {
2996
+ if (!isGptOss(model) || resolved.thinkingDisabled || !resolved.activeEffort) {
2997
+ return void 0;
2998
+ }
2999
+ const e = resolved.activeEffort;
3000
+ return e === "xhigh" || e === "max" ? "high" : e;
3001
+ }),
3002
+ capture: "reasoning"
3003
+ },
3004
+ capabilities: {
3005
+ emitsMessageName: false,
3006
+ // Groq's `response_format: json_schema` is supported only on a subset of
3007
+ // models — the `openai/gpt-oss-*` family accepts it; llama/gemma/etc 400
3008
+ // with "This model does not support response format `json_schema`".
3009
+ // Per-model so `nativeStructuredOutput` engages the native path only where
3010
+ // it works and otherwise downgrades cleanly to `json_object` (verified live).
3011
+ supportsJsonSchema: isGptOss
3012
+ }
3013
+ };
3014
+
3015
+ // src/providers/profiles/bedrock.ts
3016
+ var isGptOss2 = (model) => /gpt-oss/i.test(model);
3017
+ var BEDROCK_PROFILE = {
3018
+ name: "bedrock",
3019
+ label: "AWS Bedrock",
3020
+ // Never used — requireExplicitBaseUrl forces a real (region-specific) value first.
3021
+ defaultBaseUrl: "https://bedrock-mantle.us-east-1.api.aws/v1",
3022
+ requireExplicitBaseUrl: true,
3023
+ envApiKey: "AWS_BEARER_TOKEN_BEDROCK",
3024
+ envBaseUrl: "BEDROCK_BASE_URL",
3025
+ pricing: { kind: "unknown" },
3026
+ reasoning: {
3027
+ emit: reasoningEffortEmit((resolved, model) => {
3028
+ if (!isGptOss2(model) || resolved.thinkingDisabled || !resolved.activeEffort) {
3029
+ return void 0;
3030
+ }
3031
+ const e = resolved.activeEffort;
3032
+ return e === "xhigh" || e === "max" ? "high" : e;
3033
+ }),
3034
+ capture: "reasoning"
3035
+ }
3036
+ };
3037
+
3038
+ // src/providers/profiles/local.ts
3039
+ function localProfile(name, label, port, envApiKey) {
3040
+ return {
3041
+ name,
3042
+ label,
3043
+ defaultBaseUrl: `http://localhost:${port}/v1`,
3044
+ envApiKey,
3045
+ envBaseUrl: `${envApiKey.replace(/_API_KEY$/, "")}_BASE_URL`,
3046
+ allowMissingApiKey: true,
3047
+ pricing: { kind: "zero" },
3048
+ reasoning: { emit: () => {
3049
+ }, capture: "think_tags" },
3050
+ maxTokensField: "max_tokens"
3051
+ };
3052
+ }
3053
+ var OLLAMA_PROFILE = localProfile("ollama", "Ollama", 11434, "OLLAMA_API_KEY");
3054
+ var VLLM_PROFILE = localProfile("vllm", "vLLM", 8e3, "VLLM_API_KEY");
3055
+ var LMSTUDIO_PROFILE = localProfile("lmstudio", "LM Studio", 1234, "LMSTUDIO_API_KEY");
3056
+ var LLAMACPP_PROFILE = localProfile("llamacpp", "llama.cpp", 8080, "LLAMACPP_API_KEY");
3057
+ var SGLANG_PROFILE = localProfile("sglang", "SGLang", 3e4, "SGLANG_API_KEY");
3058
+ var LOCAL_PROFILES = [
3059
+ OLLAMA_PROFILE,
3060
+ VLLM_PROFILE,
3061
+ LMSTUDIO_PROFILE,
3062
+ LLAMACPP_PROFILE,
3063
+ SGLANG_PROFILE
3064
+ ];
3065
+
3066
+ // src/providers/profiles/index.ts
3067
+ var BUILTIN_PROFILES = [
3068
+ OPENROUTER_PROFILE,
3069
+ AZURE_PROFILE,
3070
+ XAI_PROFILE,
3071
+ DEEPSEEK_PROFILE,
3072
+ MISTRAL_PROFILE,
3073
+ GROQ_PROFILE,
3074
+ BEDROCK_PROFILE,
3075
+ ...LOCAL_PROFILES
3076
+ ];
3077
+
2213
3078
  // src/providers/registry.ts
2214
3079
  var builtinFactories = {
2215
3080
  openai: (config) => {
@@ -2249,6 +3114,23 @@ var builtinFactories = {
2249
3114
  });
2250
3115
  }
2251
3116
  };
3117
+ function presetFactory(profile) {
3118
+ return (config) => {
3119
+ const opts = config.providers?.[profile.name] ?? {};
3120
+ return new OpenAICompatibleProvider({
3121
+ profile,
3122
+ apiKey: opts.apiKey,
3123
+ baseUrl: opts.baseUrl,
3124
+ authHeader: opts.authHeader,
3125
+ rateLimit: opts.rateLimit
3126
+ });
3127
+ };
3128
+ }
3129
+ for (const profile of BUILTIN_PROFILES) {
3130
+ if (!(profile.name in builtinFactories)) {
3131
+ builtinFactories[profile.name] = presetFactory(profile);
3132
+ }
3133
+ }
2252
3134
  var ProviderRegistry = class {
2253
3135
  /** Cached provider instances, keyed by provider name */
2254
3136
  instances = /* @__PURE__ */ new Map();
@@ -2366,123 +3248,6 @@ var import_node_async_hooks = require("async_hooks");
2366
3248
  var import_node_crypto = require("crypto");
2367
3249
  var import_zod = require("zod");
2368
3250
 
2369
- // src/errors.ts
2370
- var AxlError = class extends Error {
2371
- code;
2372
- constructor(code, message) {
2373
- super(message);
2374
- this.name = "AxlError";
2375
- this.code = code;
2376
- }
2377
- };
2378
- var VerifyError = class extends AxlError {
2379
- lastOutput;
2380
- zodError;
2381
- retries;
2382
- constructor(lastOutput, zodError, retries) {
2383
- super("VERIFY_ERROR", `Schema validation failed after ${retries} retries: ${zodError.message}`);
2384
- this.name = "VerifyError";
2385
- this.lastOutput = lastOutput;
2386
- this.zodError = zodError;
2387
- this.retries = retries;
2388
- }
2389
- };
2390
- var QuorumNotMet = class extends AxlError {
2391
- results;
2392
- constructor(required, actual, results) {
2393
- super("QUORUM_NOT_MET", `Quorum not met: needed ${required} successes, got ${actual}`);
2394
- this.name = "QuorumNotMet";
2395
- this.results = results;
2396
- }
2397
- };
2398
- var NoConsensus = class extends AxlError {
2399
- constructor(reason) {
2400
- super("NO_CONSENSUS", `No consensus: ${reason}`);
2401
- this.name = "NoConsensus";
2402
- }
2403
- };
2404
- var TimeoutError = class extends AxlError {
2405
- constructor(operation, timeoutMs) {
2406
- super("TIMEOUT", `${operation} exceeded timeout of ${timeoutMs}ms`);
2407
- this.name = "TimeoutError";
2408
- }
2409
- };
2410
- function formatBudgetCost(cost) {
2411
- if (Number.isNaN(cost)) return "$NaN";
2412
- if (cost === Infinity) return "$Infinity";
2413
- if (cost === -Infinity) return "-$Infinity";
2414
- if (cost === 0) return "$0.00";
2415
- const sign = cost < 0 ? "-" : "";
2416
- const abs = Math.abs(cost);
2417
- if (abs < 1e-6) return `${sign}< $0.000001`;
2418
- if (abs < 1e-4) {
2419
- const [mantissa, exponent] = abs.toExponential(2).split("e");
2420
- return `${sign}$${mantissa}e${parseInt(exponent, 10)}`;
2421
- }
2422
- if (abs < 0.01) return `${sign}$${abs.toFixed(6)}`;
2423
- return `${sign}$${abs.toFixed(2)}`;
2424
- }
2425
- var BudgetExceededError = class extends AxlError {
2426
- limit;
2427
- spent;
2428
- policy;
2429
- constructor(limit, spent, policy) {
2430
- super(
2431
- "BUDGET_EXCEEDED",
2432
- `Budget exceeded: spent ${formatBudgetCost(spent)} of ${formatBudgetCost(
2433
- limit
2434
- )} limit (policy: ${policy})`
2435
- );
2436
- this.name = "BudgetExceededError";
2437
- this.limit = limit;
2438
- this.spent = spent;
2439
- this.policy = policy;
2440
- }
2441
- };
2442
- var MaxTurnsError = class extends AxlError {
2443
- maxTurns;
2444
- constructor(operation, maxTurns) {
2445
- super("MAX_TURNS", `${operation} exceeded maximum of ${maxTurns} turns`);
2446
- this.name = "MaxTurnsError";
2447
- this.maxTurns = maxTurns;
2448
- }
2449
- };
2450
- var GuardrailError = class extends AxlError {
2451
- guardrailType;
2452
- reason;
2453
- constructor(guardrailType, reason) {
2454
- super("GUARDRAIL_BLOCKED", `${guardrailType} guardrail blocked: ${reason}`);
2455
- this.name = "GuardrailError";
2456
- this.guardrailType = guardrailType;
2457
- this.reason = reason;
2458
- }
2459
- };
2460
- var ValidationError = class extends AxlError {
2461
- lastOutput;
2462
- reason;
2463
- retries;
2464
- constructor(lastOutput, reason, retries) {
2465
- super("VALIDATION_ERROR", `Validation failed after ${retries} retries: ${reason}`);
2466
- this.name = "ValidationError";
2467
- this.lastOutput = lastOutput;
2468
- this.reason = reason;
2469
- this.retries = retries;
2470
- }
2471
- };
2472
- var ToolDenied = class extends AxlError {
2473
- toolName;
2474
- agentName;
2475
- constructor(toolName, agentName) {
2476
- super(
2477
- "TOOL_DENIED",
2478
- `Agent "${agentName}" attempted to call tool "${toolName}" which is not in its ACL`
2479
- );
2480
- this.name = "ToolDenied";
2481
- this.toolName = toolName;
2482
- this.agentName = agentName;
2483
- }
2484
- };
2485
-
2486
3251
  // src/partial-json.ts
2487
3252
  var MAX_DEPTH = 256;
2488
3253
  function parsePartialJson(input) {
@@ -3057,6 +3822,14 @@ var COST_BEARING_LEAF_TYPES = [
3057
3822
  "memory_recall"
3058
3823
  ];
3059
3824
  var COST_LEAF_SET = new Set(COST_BEARING_LEAF_TYPES);
3825
+ function hasPositiveTokens(event) {
3826
+ const t = event.tokens;
3827
+ if (!t) return false;
3828
+ return (t.input ?? 0) + (t.output ?? 0) + (t.reasoning ?? 0) > 0;
3829
+ }
3830
+ function isUsableCost(cost) {
3831
+ return typeof cost === "number" && Number.isFinite(cost) && cost >= 0;
3832
+ }
3060
3833
  function eventCostContribution(event) {
3061
3834
  if (event.type === "ask_end") return 0;
3062
3835
  const c = event.cost;
@@ -3065,6 +3838,9 @@ function eventCostContribution(event) {
3065
3838
  function isCostBearingLeaf(event) {
3066
3839
  return COST_LEAF_SET.has(event.type);
3067
3840
  }
3841
+ function isUnpricedLeaf(event) {
3842
+ return event.type !== void 0 && COST_LEAF_SET.has(event.type) && !isUsableCost(event.cost) && hasPositiveTokens(event);
3843
+ }
3068
3844
  function isRootLevel(event) {
3069
3845
  const d = event.depth;
3070
3846
  return (d ?? 0) === 0;
@@ -3113,6 +3889,10 @@ var AXL_EVENT_TYPES = [
3113
3889
  "string_delta",
3114
3890
  // Verification
3115
3891
  "verify",
3892
+ // Schema capability diagnostics (spec 22, Problem E) — surface the silent
3893
+ // structured-output cliffs (oversized appended schema, dropped refinements,
3894
+ // streaming disabled, zero-guidance `schemaPrompt:'none'`).
3895
+ "schema_diagnostic",
3116
3896
  // Observability
3117
3897
  "log",
3118
3898
  "memory_remember",
@@ -3155,6 +3935,7 @@ var LIFECYCLE_TYPES = /* @__PURE__ */ new Set([
3155
3935
  "delegate",
3156
3936
  "pipeline",
3157
3937
  "verify",
3938
+ "schema_diagnostic",
3158
3939
  "workflow_start",
3159
3940
  "workflow_end",
3160
3941
  "checkpoint_save",
@@ -3959,6 +4740,11 @@ var REDACTION_RULES = {
3959
4740
  // Scrub only the delta — preserving path keeps "which fields stream" telemetry.
3960
4741
  string_delta: (e) => ({ ...e, data: { ...e.data, delta: REDACTED } }),
3961
4742
  verify: (e) => e.data.lastError !== void 0 ? { ...e, data: { ...e.data, lastError: REDACTED } } : e,
4743
+ // `schema_diagnostic` carries only structural schema metadata — token counts,
4744
+ // thresholds, JSON-Schema field paths, a root type name, a tool name — none of
4745
+ // which is user content or PII (field paths are schema-shape, preserved like
4746
+ // `string_delta.path`). Passthrough, as an explicit reviewed decision.
4747
+ schema_diagnostic: passthrough,
3962
4748
  log: (e) => {
3963
4749
  if (!e.data || typeof e.data !== "object" || Array.isArray(e.data)) return e;
3964
4750
  return { ...e, data: walkObjectOneLevel(e.data, LOG_PRESERVE_KEYS) };
@@ -3991,6 +4777,80 @@ function redactEvent(event) {
3991
4777
  return rule(event);
3992
4778
  }
3993
4779
 
4780
+ // src/schema-diagnostics.ts
4781
+ var DEFAULT_SCHEMA_OVERSIZED_TOKENS = 4e3;
4782
+ var refinementCache = /* @__PURE__ */ new WeakMap();
4783
+ function detectDroppedRefinements(schema) {
4784
+ const cached = refinementCache.get(schema);
4785
+ if (cached) return cached;
4786
+ const paths = /* @__PURE__ */ new Set();
4787
+ let count = 0;
4788
+ const visited = /* @__PURE__ */ new WeakSet();
4789
+ const walk = (node, path) => {
4790
+ if (node === null || typeof node !== "object") return;
4791
+ if (visited.has(node)) return;
4792
+ visited.add(node);
4793
+ const def = node._zod?.def;
4794
+ if (!def) return;
4795
+ const here = () => paths.add(path === "" ? "<root>" : path);
4796
+ const checks = def.checks;
4797
+ if (Array.isArray(checks)) {
4798
+ for (const check of checks) {
4799
+ const kind = check?._zod?.def?.check;
4800
+ if (kind === "custom") {
4801
+ count += 1;
4802
+ here();
4803
+ }
4804
+ }
4805
+ }
4806
+ if (def.check === "custom") {
4807
+ count += 1;
4808
+ here();
4809
+ }
4810
+ const join2 = (child) => path === "" ? child : `${path}.${child}`;
4811
+ const shape = def.shape;
4812
+ if (shape && typeof shape === "object") {
4813
+ for (const [key, child] of Object.entries(shape)) walk(child, join2(key));
4814
+ }
4815
+ if ("element" in def) walk(def.element, `${path}[]`);
4816
+ if (Array.isArray(def.options)) {
4817
+ for (const opt of def.options) walk(opt, path);
4818
+ }
4819
+ if ("innerType" in def) walk(def.innerType, path);
4820
+ if (Array.isArray(def.items)) {
4821
+ def.items.forEach((item, i) => walk(item, `${path}[${i}]`));
4822
+ }
4823
+ if ("rest" in def && def.rest) walk(def.rest, `${path}[]`);
4824
+ if ("catchall" in def && def.catchall) walk(def.catchall, `${path}[*]`);
4825
+ if ("keyType" in def) walk(def.keyType, `${path}[key]`);
4826
+ if ("valueType" in def) walk(def.valueType, `${path}[*]`);
4827
+ if ("in" in def) walk(def.in, path);
4828
+ if ("out" in def) walk(def.out, path);
4829
+ if ("left" in def) walk(def.left, path);
4830
+ if ("right" in def) walk(def.right, path);
4831
+ if (typeof def.getter === "function") {
4832
+ try {
4833
+ walk(def.getter(), path);
4834
+ } catch {
4835
+ }
4836
+ }
4837
+ };
4838
+ walk(schema, "");
4839
+ const result = { count, paths: [...paths] };
4840
+ refinementCache.set(schema, result);
4841
+ return result;
4842
+ }
4843
+ var DOCS_URL = "docs/observability.md#schema-diagnostics";
4844
+ var MAX_WARNED_KEYS = 2048;
4845
+ var warnedDiagnosticKeys = /* @__PURE__ */ new Set();
4846
+ function warnSchemaDiagnosticOnce(key, message, silent) {
4847
+ if (silent || process.env.AXL_DIAGNOSTICS_SILENT === "true") return;
4848
+ if (warnedDiagnosticKeys.has(key)) return;
4849
+ if (warnedDiagnosticKeys.size >= MAX_WARNED_KEYS) warnedDiagnosticKeys.clear();
4850
+ warnedDiagnosticKeys.add(key);
4851
+ console.warn(`[axl] ${message} See ${DOCS_URL}.`);
4852
+ }
4853
+
3994
4854
  // src/config.ts
3995
4855
  function defineConfig(config) {
3996
4856
  return config;
@@ -4069,11 +4929,47 @@ function resolveConfig(config) {
4069
4929
  // src/context.ts
4070
4930
  var signalStorage = new import_node_async_hooks.AsyncLocalStorage();
4071
4931
  var askStorage = new import_node_async_hooks.AsyncLocalStorage();
4932
+ var inlineSchemaCache = /* @__PURE__ */ new WeakMap();
4072
4933
  function zodToJsonSchema(schema) {
4934
+ const cached = inlineSchemaCache.get(schema);
4935
+ if (cached !== void 0) return cached;
4073
4936
  const result = import_zod.z.toJSONSchema(schema, { unrepresentable: "any" });
4074
4937
  delete result.$schema;
4938
+ Object.freeze(result);
4939
+ inlineSchemaCache.set(schema, result);
4075
4940
  return result;
4076
4941
  }
4942
+ var promptSchemaCache = /* @__PURE__ */ new WeakMap();
4943
+ var inlineSchemaTokenCache = /* @__PURE__ */ new WeakMap();
4944
+ function estimateInlineSchemaTokens(schema) {
4945
+ const cached = inlineSchemaTokenCache.get(schema);
4946
+ if (cached !== void 0) return cached;
4947
+ const tokens = estimateTokens(JSON.stringify(zodToJsonSchema(schema)));
4948
+ inlineSchemaTokenCache.set(schema, tokens);
4949
+ return tokens;
4950
+ }
4951
+ var nativeSchemaCache = /* @__PURE__ */ new WeakMap();
4952
+ function deriveNativeSchema(schema) {
4953
+ const cached = nativeSchemaCache.get(schema);
4954
+ if (cached !== void 0) return cached;
4955
+ const json = import_zod.z.toJSONSchema(schema, { unrepresentable: "any", io: "input" });
4956
+ delete json.$schema;
4957
+ nativeSchemaCache.set(schema, json);
4958
+ return json;
4959
+ }
4960
+ function renderSchemaForPrompt(schema) {
4961
+ const cached = promptSchemaCache.get(schema);
4962
+ if (cached !== void 0) return cached;
4963
+ const json = import_zod.z.toJSONSchema(schema, {
4964
+ unrepresentable: "any",
4965
+ io: "input",
4966
+ reused: "ref"
4967
+ });
4968
+ delete json.$schema;
4969
+ const rendered = JSON.stringify(json);
4970
+ promptSchemaCache.set(schema, rendered);
4971
+ return rendered;
4972
+ }
4077
4973
  function estimateTokens(text) {
4078
4974
  return Math.ceil(text.length / 4);
4079
4975
  }
@@ -4454,12 +5350,14 @@ var WorkflowContext = class _WorkflowContext {
4454
5350
  depth,
4455
5351
  agent: agent2._name,
4456
5352
  stepRef,
4457
- askCost: { value: 0 }
5353
+ askCost: { value: 0 },
5354
+ askUnpriced: false
4458
5355
  };
4459
5356
  return askStorage.run(frame, async () => {
4460
5357
  const askStart = Date.now();
4461
5358
  this.emitEvent({ type: "ask_start", prompt });
4462
5359
  const costBefore = this.budgetContext?.totalCost ?? 0;
5360
+ const unpricedCountBefore = this.budgetContext?.unpricedCount ?? 0;
4463
5361
  const resolveCtx = options?.metadata ? { metadata: { ...this.metadata, ...options.metadata } } : { metadata: this.metadata };
4464
5362
  const usageCapture = {};
4465
5363
  const doCall = async () => {
@@ -4505,6 +5403,7 @@ var WorkflowContext = class _WorkflowContext {
4505
5403
  ) : await doCall();
4506
5404
  outcome = { ok: true, result };
4507
5405
  const costAfter = this.budgetContext?.totalCost ?? 0;
5406
+ const unpricedCountAfter = this.budgetContext?.unpricedCount ?? 0;
4508
5407
  if (this.onAgentCallComplete) {
4509
5408
  try {
4510
5409
  this.onAgentCallComplete({
@@ -4513,6 +5412,7 @@ var WorkflowContext = class _WorkflowContext {
4513
5412
  response: typeof result === "string" ? result : JSON.stringify(result),
4514
5413
  model: agent2.resolveModel(resolveCtx),
4515
5414
  cost: costAfter - costBefore,
5415
+ unpriced: frame.askUnpriced || unpricedCountAfter > unpricedCountBefore,
4516
5416
  duration: Date.now() - askStart,
4517
5417
  promptVersion: agent2._config.version,
4518
5418
  temperature: options?.temperature ?? agent2._config.temperature,
@@ -4547,6 +5447,7 @@ var WorkflowContext = class _WorkflowContext {
4547
5447
  error: "ask_end emitted without outcome \u2014 internal bug"
4548
5448
  },
4549
5449
  cost: frame.askCost.value,
5450
+ ...frame.askUnpriced ? { unpriced: true } : {},
4550
5451
  duration: Date.now() - askStart
4551
5452
  });
4552
5453
  } catch (emitErr) {
@@ -4630,15 +5531,32 @@ var WorkflowContext = class _WorkflowContext {
4630
5531
  messages.push(msg);
4631
5532
  }
4632
5533
  }
5534
+ const schemaPromptMode = options?.schemaPrompt ?? agent2._config.schemaPrompt ?? "json-schema";
4633
5535
  let userContent = prompt;
5536
+ let appendedSchemaText;
4634
5537
  if (options?.schema) {
4635
- const jsonSchema = zodToJsonSchema(options.schema);
4636
- userContent += `
5538
+ const schema = options.schema;
5539
+ if (schemaPromptMode === "none") {
5540
+ } else if (schemaPromptMode === "json-schema") {
5541
+ appendedSchemaText = renderSchemaForPrompt(schema);
5542
+ userContent += `
4637
5543
 
4638
5544
  Respond with valid JSON matching this schema:
4639
- ${JSON.stringify(jsonSchema, null, 2)}`;
5545
+ ${appendedSchemaText}`;
5546
+ } else {
5547
+ appendedSchemaText = typeof schemaPromptMode.render === "function" ? schemaPromptMode.render(schema) : schemaPromptMode.render;
5548
+ userContent += `
5549
+
5550
+ ${appendedSchemaText}`;
5551
+ }
4640
5552
  }
4641
5553
  messages.push({ role: "user", content: userContent });
5554
+ this.emitSchemaDiagnostics(agent2, options, toolDefs, {
5555
+ mode: schemaPromptMode,
5556
+ appendedSchemaText,
5557
+ provider,
5558
+ model
5559
+ });
4642
5560
  if (handoffMessages && handoffMessages.length > 0) {
4643
5561
  const handoffContext = handoffMessages.filter(
4644
5562
  (m) => m.role === "assistant" || m.role === "tool"
@@ -4751,7 +5669,17 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
4751
5669
  signal: this.currentSignal
4752
5670
  };
4753
5671
  if (options?.schema && toolDefs.length === 0) {
4754
- chatOptions.responseFormat = { type: "json_object" };
5672
+ const nativeOut = options?.nativeStructuredOutput ?? agent2._config.nativeStructuredOutput ?? false;
5673
+ chatOptions.responseFormat = nativeOut ? {
5674
+ type: "json_schema",
5675
+ json_schema: {
5676
+ name: "response",
5677
+ // Input-side rendering — see `deriveNativeSchema`. Keeps the
5678
+ // native schema consistent with the prompt and non-empty for
5679
+ // `.transform()` schemas.
5680
+ schema: deriveNativeSchema(options.schema)
5681
+ }
5682
+ } : { type: "json_object" };
4755
5683
  }
4756
5684
  const callbackMeta = this.currentCallbackMeta(agent2._name);
4757
5685
  const callParams = {
@@ -4869,10 +5797,7 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
4869
5797
  }
4870
5798
  });
4871
5799
  }
4872
- response ??= {
4873
- content: content2,
4874
- usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 }
4875
- };
5800
+ response ??= { content: content2 };
4876
5801
  if (toolCalls.length > 0) {
4877
5802
  response.tool_calls = toolCalls;
4878
5803
  }
@@ -4896,7 +5821,11 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
4896
5821
  response: "",
4897
5822
  turn: turns,
4898
5823
  ...retryReason ? { retryReason } : {},
4899
- error: err instanceof Error ? err.message : String(err)
5824
+ error: err instanceof Error ? err.message : String(err),
5825
+ // Enrich with typed-error metadata when available. `body` is
5826
+ // deliberately NOT emitted — it's redaction-eligible (see
5827
+ // docs/security.md). `data.error` already carries the message.
5828
+ ...err instanceof ProviderError ? { status: err.status, retryable: err.retryable } : {}
4900
5829
  }
4901
5830
  });
4902
5831
  throw err;
@@ -4960,10 +5889,13 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
4960
5889
  depth: handoffTargetDepth,
4961
5890
  agent: descriptor.agent._name,
4962
5891
  stepRef: sourceFrame?.stepRef ?? this.stepRefRoot,
4963
- askCost: { value: 0 }
5892
+ askCost: { value: 0 },
5893
+ askUnpriced: false
4964
5894
  };
4965
5895
  const handoffOptions = options ? {
4966
5896
  schema: options.schema,
5897
+ schemaPrompt: options.schemaPrompt,
5898
+ nativeStructuredOutput: options.nativeStructuredOutput,
4967
5899
  retries: options.retries,
4968
5900
  metadata: options.metadata,
4969
5901
  validate: options.validate,
@@ -4984,6 +5916,7 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
4984
5916
  type: "ask_end",
4985
5917
  outcome: { ok: true, result },
4986
5918
  cost: targetFrame.askCost.value,
5919
+ ...targetFrame.askUnpriced ? { unpriced: true } : {},
4987
5920
  duration: Date.now() - targetAskStart
4988
5921
  });
4989
5922
  return result;
@@ -4995,6 +5928,7 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
4995
5928
  error: err instanceof Error ? err.message : String(err)
4996
5929
  },
4997
5930
  cost: targetFrame.askCost.value,
5931
+ ...targetFrame.askUnpriced ? { unpriced: true } : {},
4998
5932
  duration: Date.now() - targetAskStart
4999
5933
  });
5000
5934
  throw err;
@@ -5533,6 +6467,148 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
5533
6467
  ...providerMetadata ? { providerMetadata } : {}
5534
6468
  });
5535
6469
  }
6470
+ /**
6471
+ * Emit `schema_diagnostic` events for the silent structured-output cliffs
6472
+ * (spec 22, Problems B + E). Called once per ask. Covers:
6473
+ * - the appended **prompt** schema (oversized; dropped refinements; or the
6474
+ * zero-guidance `schemaPrompt:'none'` footgun),
6475
+ * - each user **tool-def** schema (oversized, dropped refinements),
6476
+ * - the **streaming** gate (disabled by a non-object root or by tools),
6477
+ * - **native structured output** the resolved provider can't honor (R10).
6478
+ *
6479
+ * The structured event always fires (persisted + streamed). For the genuinely
6480
+ * surprising, low-frequency cliffs it ALSO fires a one-time deduped
6481
+ * `console.warn` so the median consumer — who never wires up `ctx.events` and
6482
+ * runs with the trace console off — still sees it. `oversized` is event-only
6483
+ * (O3: it's tunable/noisy); the `tools` streaming cause is event-only too
6484
+ * (expected behavior for any tool-using agent, not a surprise).
6485
+ */
6486
+ emitSchemaDiagnostics(agent2, options, toolDefs, ctx) {
6487
+ const silent = this.config.diagnostics?.silent;
6488
+ const threshold = this.config.diagnostics?.schemaOversizedTokens ?? DEFAULT_SCHEMA_OVERSIZED_TOKENS;
6489
+ const schema = options?.schema;
6490
+ if (schema) {
6491
+ if (ctx.mode === "none") {
6492
+ this.emitEvent({
6493
+ type: "schema_diagnostic",
6494
+ agent: agent2._name,
6495
+ data: { kind: "schema_prompt_none_no_guidance" }
6496
+ });
6497
+ warnSchemaDiagnosticOnce(
6498
+ `${agent2._name}\0schema_prompt_none`,
6499
+ `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'.`,
6500
+ silent
6501
+ );
6502
+ } else {
6503
+ const promptTokens = estimateTokens(ctx.appendedSchemaText ?? "");
6504
+ if (promptTokens > threshold) {
6505
+ this.emitEvent({
6506
+ type: "schema_diagnostic",
6507
+ agent: agent2._name,
6508
+ data: {
6509
+ kind: "prompt_schema_oversized",
6510
+ estimatedTokens: promptTokens,
6511
+ threshold,
6512
+ site: "prompt"
6513
+ }
6514
+ });
6515
+ }
6516
+ if (ctx.mode === "json-schema") {
6517
+ const dropped = detectDroppedRefinements(schema);
6518
+ if (dropped.count > 0) {
6519
+ this.emitEvent({
6520
+ type: "schema_diagnostic",
6521
+ agent: agent2._name,
6522
+ data: {
6523
+ kind: "dropped_refinements",
6524
+ count: dropped.count,
6525
+ paths: dropped.paths,
6526
+ site: "prompt"
6527
+ }
6528
+ });
6529
+ warnSchemaDiagnosticOnce(
6530
+ `${agent2._name}\0dropped_refinements\0prompt\0${dropped.count}\0${dropped.paths.join(",")}`,
6531
+ `${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.`,
6532
+ silent
6533
+ );
6534
+ }
6535
+ }
6536
+ }
6537
+ const nativeOut = options?.nativeStructuredOutput ?? agent2._config.nativeStructuredOutput;
6538
+ if (nativeOut && toolDefs.length === 0) {
6539
+ const support = ctx.provider.nativeStructuredOutputSupport?.(ctx.model) ?? "schema";
6540
+ if (support !== "schema") {
6541
+ this.emitEvent({
6542
+ type: "schema_diagnostic",
6543
+ agent: agent2._name,
6544
+ data: {
6545
+ kind: "native_output_unsupported",
6546
+ ...ctx.provider.name ? { provider: ctx.provider.name } : {},
6547
+ support
6548
+ }
6549
+ });
6550
+ warnSchemaDiagnosticOnce(
6551
+ `${agent2._name}\0native_output_unsupported\0${ctx.provider.name ?? ctx.model}`,
6552
+ `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.`,
6553
+ silent
6554
+ );
6555
+ }
6556
+ }
6557
+ const rootIsObject = schema instanceof import_zod.z.ZodObject;
6558
+ const hasTools = toolDefs.length > 0;
6559
+ if (this._streamingEnabled && (!rootIsObject || hasTools)) {
6560
+ const cause = !rootIsObject ? "non-object" : "tools";
6561
+ const rootType = schema.constructor?.name ?? "unknown";
6562
+ this.emitEvent({
6563
+ type: "schema_diagnostic",
6564
+ agent: agent2._name,
6565
+ data: { kind: "streaming_disabled", rootType, cause }
6566
+ });
6567
+ if (cause === "non-object") {
6568
+ warnSchemaDiagnosticOnce(
6569
+ `${agent2._name}\0streaming_disabled\0${rootType}`,
6570
+ `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.`,
6571
+ silent
6572
+ );
6573
+ }
6574
+ }
6575
+ }
6576
+ for (const tool2 of agent2._config.tools ?? []) {
6577
+ const toolTokens = estimateInlineSchemaTokens(tool2.inputSchema);
6578
+ if (toolTokens > threshold) {
6579
+ this.emitEvent({
6580
+ type: "schema_diagnostic",
6581
+ agent: agent2._name,
6582
+ data: {
6583
+ kind: "prompt_schema_oversized",
6584
+ estimatedTokens: toolTokens,
6585
+ threshold,
6586
+ site: "tool",
6587
+ tool: tool2.name
6588
+ }
6589
+ });
6590
+ }
6591
+ const droppedTool = detectDroppedRefinements(tool2.inputSchema);
6592
+ if (droppedTool.count > 0) {
6593
+ this.emitEvent({
6594
+ type: "schema_diagnostic",
6595
+ agent: agent2._name,
6596
+ data: {
6597
+ kind: "dropped_refinements",
6598
+ count: droppedTool.count,
6599
+ paths: droppedTool.paths,
6600
+ site: "tool",
6601
+ tool: tool2.name
6602
+ }
6603
+ });
6604
+ warnSchemaDiagnosticOnce(
6605
+ `${agent2._name}\0dropped_refinements\0tool\0${tool2.name}\0${droppedTool.count}`,
6606
+ `${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.`,
6607
+ silent
6608
+ );
6609
+ }
6610
+ }
6611
+ }
5536
6612
  buildToolDefs(agent2, resolvedHandoffs) {
5537
6613
  const defs = [];
5538
6614
  if (agent2._config.tools) {
@@ -6025,24 +7101,44 @@ ${summaryResponse.content}`
6025
7101
  limit,
6026
7102
  exceeded: false,
6027
7103
  policy,
6028
- abortController: controller
7104
+ abortController: controller,
7105
+ unpriced: false,
7106
+ unpricedCount: 0,
7107
+ unpricedWarned: false
6029
7108
  };
6030
7109
  const executeBudget = async () => {
6031
7110
  try {
6032
7111
  const value = budgetSignal ? await signalStorage.run(budgetSignal, fn) : await fn();
6033
7112
  const totalCost = this.budgetContext.totalCost;
6034
7113
  const exceeded = this.budgetContext.exceeded;
6035
- return { value, budgetExceeded: exceeded, totalCost };
7114
+ const unpriced = this.budgetContext.unpriced;
7115
+ return { value, budgetExceeded: exceeded, totalCost, unpriced };
6036
7116
  } catch (err) {
6037
7117
  if (this.budgetContext.exceeded) {
6038
- return { value: null, budgetExceeded: true, totalCost: this.budgetContext.totalCost };
7118
+ return {
7119
+ value: null,
7120
+ budgetExceeded: true,
7121
+ totalCost: this.budgetContext.totalCost,
7122
+ unpriced: this.budgetContext.unpriced
7123
+ };
6039
7124
  }
6040
7125
  if (err instanceof DOMException && err.name === "AbortError" && controller) {
6041
- return { value: null, budgetExceeded: true, totalCost: this.budgetContext.totalCost };
7126
+ return {
7127
+ value: null,
7128
+ budgetExceeded: true,
7129
+ totalCost: this.budgetContext.totalCost,
7130
+ unpriced: this.budgetContext.unpriced
7131
+ };
6042
7132
  }
6043
7133
  throw err;
6044
7134
  } finally {
6045
- if (parentBudget) parentBudget.totalCost += this.budgetContext.totalCost;
7135
+ if (parentBudget) {
7136
+ parentBudget.totalCost += this.budgetContext.totalCost;
7137
+ if (this.budgetContext.unpriced) {
7138
+ parentBudget.unpriced = true;
7139
+ parentBudget.unpricedCount += this.budgetContext.unpricedCount;
7140
+ }
7141
+ }
6046
7142
  this.budgetContext = parentBudget;
6047
7143
  }
6048
7144
  };
@@ -6063,13 +7159,22 @@ ${summaryResponse.content}`
6063
7159
  }
6064
7160
  return executeBudget();
6065
7161
  }
6066
- /** Get the current budget status, or null if not inside a budget block. */
7162
+ /**
7163
+ * Get the current budget status, or null if not inside a budget block. Inside
7164
+ * nested budgets this reflects the **innermost active** block (the nested block's
7165
+ * `finally` restores the parent before control returns to it).
7166
+ *
7167
+ * `unpriced` is true when this block ran a model with no usable cost (unpriced /
7168
+ * pricing-table miss): `spent` is then a LOWER BOUND and `remaining` an upper bound —
7169
+ * the limit cannot be enforced on the unmeasured spend. See {@link BudgetResult.unpriced}.
7170
+ */
6067
7171
  getBudgetStatus() {
6068
7172
  if (!this.budgetContext) return null;
6069
7173
  return {
6070
7174
  spent: this.budgetContext.totalCost,
6071
7175
  limit: this.budgetContext.limit,
6072
- remaining: Math.max(0, this.budgetContext.limit - this.budgetContext.totalCost)
7176
+ remaining: Math.max(0, this.budgetContext.limit - this.budgetContext.totalCost),
7177
+ unpriced: this.budgetContext.unpriced
6073
7178
  };
6074
7179
  }
6075
7180
  // ── ctx.race() ────────────────────────────────────────────────────────
@@ -6651,6 +7756,8 @@ ${summaryResponse.content}`
6651
7756
  });
6652
7757
  return this.ask(agents[0], prompt, {
6653
7758
  schema: options?.schema,
7759
+ schemaPrompt: options?.schemaPrompt,
7760
+ nativeStructuredOutput: options?.nativeStructuredOutput,
6654
7761
  retries: options?.retries,
6655
7762
  metadata: options?.metadata,
6656
7763
  validate: options?.validate,
@@ -6695,6 +7802,8 @@ ${summaryResponse.content}`
6695
7802
  });
6696
7803
  return this.ask(routerAgent, prompt, {
6697
7804
  schema: options?.schema,
7805
+ schemaPrompt: options?.schemaPrompt,
7806
+ nativeStructuredOutput: options?.nativeStructuredOutput,
6698
7807
  retries: options?.retries,
6699
7808
  metadata: options?.metadata,
6700
7809
  validate: options?.validate,
@@ -6713,8 +7822,25 @@ ${summaryResponse.content}`
6713
7822
  const frame = askStorage.getStore();
6714
7823
  const step = (frame?.stepRef ?? this.stepRefRoot).value++;
6715
7824
  const cost = partial.cost;
6716
- if (frame && typeof cost === "number" && Number.isFinite(cost) && cost >= 0 && COST_BEARING_LEAF_TYPES.includes(partial.type)) {
6717
- frame.askCost.value += cost;
7825
+ if (COST_BEARING_LEAF_TYPES.includes(partial.type)) {
7826
+ const usable = isUsableCost(cost);
7827
+ const unpriced = !usable && hasPositiveTokens(
7828
+ partial
7829
+ );
7830
+ if (frame) {
7831
+ if (usable) frame.askCost.value += cost;
7832
+ else if (unpriced) frame.askUnpriced = true;
7833
+ }
7834
+ if (unpriced && this.budgetContext) {
7835
+ this.budgetContext.unpriced = true;
7836
+ this.budgetContext.unpricedCount += 1;
7837
+ if (!this.budgetContext.unpricedWarned && Number.isFinite(this.budgetContext.limit)) {
7838
+ this.budgetContext.unpricedWarned = true;
7839
+ console.warn(
7840
+ "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."
7841
+ );
7842
+ }
7843
+ }
6718
7844
  }
6719
7845
  const event = {
6720
7846
  executionId: this.executionId,
@@ -8541,7 +9667,7 @@ var StreamingFlusher = class {
8541
9667
  await Promise.allSettled(pendings);
8542
9668
  }
8543
9669
  };
8544
- function isAbortError(err) {
9670
+ function isAbortError2(err) {
8545
9671
  return typeof err === "object" && err !== null && err.name === "AbortError";
8546
9672
  }
8547
9673
  function pushEventBounded(execInfo, event, cap) {
@@ -8804,7 +9930,7 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
8804
9930
  this.persistExecution(execInfo);
8805
9931
  return result;
8806
9932
  } catch (err) {
8807
- const aborted = isAbortError(err);
9933
+ const aborted = isAbortError2(err);
8808
9934
  execInfo.status = "failed";
8809
9935
  execInfo.completedAt = Date.now();
8810
9936
  execInfo.duration = execInfo.completedAt - execInfo.startedAt;
@@ -9078,7 +10204,10 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
9078
10204
  totalCost: 0,
9079
10205
  limit: budgetLimit,
9080
10206
  exceeded: false,
9081
- policy: "finish_and_stop"
10207
+ policy: "finish_and_stop",
10208
+ unpriced: false,
10209
+ unpricedCount: 0,
10210
+ unpricedWarned: false
9082
10211
  }
9083
10212
  });
9084
10213
  }
@@ -9113,6 +10242,7 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
9113
10242
  status: "running",
9114
10243
  events: [],
9115
10244
  totalCost: 0,
10245
+ unpriced: false,
9116
10246
  startedAt: Date.now(),
9117
10247
  duration: 0,
9118
10248
  metadata: liftPersistedMetadata(options?.metadata)
@@ -9131,6 +10261,7 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
9131
10261
  onTrace: (event) => {
9132
10262
  pushEventBounded(execInfo, event, this.maxEventsPerExecution);
9133
10263
  execInfo.totalCost += eventCostContribution(event);
10264
+ if (isUnpricedLeaf(event)) execInfo.unpriced = true;
9134
10265
  if (this.streamableExecutionIds.has(executionId)) {
9135
10266
  this.streamingFlusher?.append(executionId, event);
9136
10267
  }
@@ -9177,7 +10308,10 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
9177
10308
  totalCost: 0,
9178
10309
  limit: Infinity,
9179
10310
  exceeded: false,
9180
- policy: "finish_and_stop"
10311
+ policy: "finish_and_stop",
10312
+ unpriced: false,
10313
+ unpricedCount: 0,
10314
+ unpricedWarned: false
9181
10315
  }
9182
10316
  });
9183
10317
  return this.spanManager.withSpanAsync(
@@ -9221,6 +10355,7 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
9221
10355
  status: "running",
9222
10356
  events: [],
9223
10357
  totalCost: 0,
10358
+ unpriced: false,
9224
10359
  startedAt: Date.now(),
9225
10360
  duration: 0,
9226
10361
  metadata: liftPersistedMetadata(options?.metadata)
@@ -9244,6 +10379,7 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
9244
10379
  onTrace: (event) => {
9245
10380
  pushEventBounded(execInfo, event, this.maxEventsPerExecution);
9246
10381
  execInfo.totalCost += eventCostContribution(event);
10382
+ if (isUnpricedLeaf(event)) execInfo.unpriced = true;
9247
10383
  if (this.streamableExecutionIds.has(executionId)) {
9248
10384
  this.streamingFlusher?.append(executionId, event);
9249
10385
  }
@@ -9291,7 +10427,10 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
9291
10427
  totalCost: 0,
9292
10428
  limit: Infinity,
9293
10429
  exceeded: false,
9294
- policy: "finish_and_stop"
10430
+ policy: "finish_and_stop",
10431
+ unpriced: false,
10432
+ unpricedCount: 0,
10433
+ unpricedWarned: false
9295
10434
  }
9296
10435
  });
9297
10436
  return this.spanManager.withSpanAsync(
@@ -9453,12 +10592,14 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
9453
10592
  const completedAt = lastEvent.timestamp ?? startedAt;
9454
10593
  const workflowName = workflowStartEvent?.workflow ?? RECOVERED_UNKNOWN_WORKFLOW;
9455
10594
  const totalCost = boundedEvents.reduce((sum, e) => sum + eventCostContribution(e), 0);
10595
+ const unpriced = boundedEvents.some(isUnpricedLeaf);
9456
10596
  const synthesized = {
9457
10597
  executionId: id,
9458
10598
  workflow: workflowName,
9459
10599
  status: "failed",
9460
10600
  events: boundedEvents,
9461
10601
  totalCost,
10602
+ unpriced,
9462
10603
  startedAt,
9463
10604
  completedAt,
9464
10605
  duration: completedAt - startedAt,
@@ -9760,8 +10901,8 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
9760
10901
  * Works with both `createContext()` and `execute()` calls inside `fn`.
9761
10902
  */
9762
10903
  async trackCost(fn) {
9763
- const { result, cost } = await this.trackExecution(fn);
9764
- return { result, cost };
10904
+ const { result, cost, unpriced } = await this.trackExecution(fn);
10905
+ return { result, cost, unpriced };
9765
10906
  }
9766
10907
  /**
9767
10908
  * Track cost and execution metadata across any runtime operations within the given function.
@@ -9806,10 +10947,12 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
9806
10947
  const workflowCalls = /* @__PURE__ */ new Map();
9807
10948
  const tokens = { input: 0, output: 0, reasoning: 0 };
9808
10949
  let agentCalls = 0;
10950
+ let unpriced = false;
9809
10951
  const capturedTraces = options?.captureTraces ? [] : void 0;
9810
10952
  const listener = (event) => {
9811
10953
  if (!scope.trackedIds.has(event.executionId)) return;
9812
10954
  scope.totalCost += eventCostContribution(event);
10955
+ if (isUnpricedLeaf(event)) unpriced = true;
9813
10956
  if (event.type === "agent_call_end") {
9814
10957
  if (event.model) modelCalls.set(event.model, (modelCalls.get(event.model) ?? 0) + 1);
9815
10958
  agentCalls++;
@@ -9847,6 +10990,7 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
9847
10990
  return {
9848
10991
  result,
9849
10992
  cost: scope.totalCost,
10993
+ unpriced,
9850
10994
  ...capturedTraces ? { traces: capturedTraces } : {},
9851
10995
  metadata: {
9852
10996
  models: [...modelCalls.keys()],
@@ -10741,22 +11885,40 @@ var OpenAIEmbedder = class {
10741
11885
  this.dimensions = options.dimensions ?? 1536;
10742
11886
  }
10743
11887
  async embed(texts, signal) {
10744
- const response = await fetchWithRetry(`${this.baseUrl}/v1/embeddings`, {
10745
- method: "POST",
10746
- headers: {
10747
- "Content-Type": "application/json",
10748
- Authorization: `Bearer ${this.apiKey}`
11888
+ const response = await fetchWithRetry(
11889
+ `${this.baseUrl}/v1/embeddings`,
11890
+ {
11891
+ method: "POST",
11892
+ headers: {
11893
+ "Content-Type": "application/json",
11894
+ Authorization: `Bearer ${this.apiKey}`
11895
+ },
11896
+ body: JSON.stringify({
11897
+ model: this.model,
11898
+ input: texts,
11899
+ dimensions: this.dimensions
11900
+ }),
11901
+ signal
10749
11902
  },
10750
- body: JSON.stringify({
10751
- model: this.model,
10752
- input: texts,
10753
- dimensions: this.dimensions
10754
- }),
10755
- signal
10756
- });
11903
+ // Participate in the same typed-error contract as the provider adapters: a
11904
+ // thrown network failure normalizes to ProviderError{status:0,provider:'openai'}.
11905
+ { provider: "openai" }
11906
+ );
10757
11907
  if (!response.ok) {
10758
11908
  const body = await response.text();
10759
- throw new Error(`OpenAI embeddings API error (${response.status}): ${body}`);
11909
+ let detail = body;
11910
+ try {
11911
+ const json2 = JSON.parse(body);
11912
+ detail = json2.error?.message ?? json2.message ?? body;
11913
+ } catch {
11914
+ }
11915
+ throw buildProviderError({
11916
+ provider: "openai",
11917
+ status: response.status,
11918
+ headers: response.headers,
11919
+ message: `OpenAI embeddings API error (${response.status}): ${detail}`,
11920
+ body
11921
+ });
10760
11922
  }
10761
11923
  const json = await response.json();
10762
11924
  const vectors = json.data.sort((a, b) => a.index - b.index).map((d) => d.embedding);
@@ -10898,57 +12060,87 @@ function cosineSimilarity2(a, b) {
10898
12060
  // Annotate the CommonJS export names for ESM import in node:
10899
12061
  0 && (module.exports = {
10900
12062
  AXL_EVENT_TYPES,
12063
+ AZURE_PROFILE,
10901
12064
  AnthropicProvider,
10902
12065
  AxlError,
10903
12066
  AxlEventBus,
10904
12067
  AxlRuntime,
10905
12068
  AxlStream,
12069
+ BEDROCK_PROFILE,
12070
+ BUILTIN_PROFILES,
10906
12071
  BudgetExceededError,
10907
12072
  CONFIG_CANDIDATES,
10908
12073
  COST_BEARING_LEAF_TYPES,
12074
+ DEEPSEEK_PROFILE,
10909
12075
  EventStreamOverflowError,
12076
+ GROQ_PROFILE,
10910
12077
  GeminiProvider,
10911
12078
  GuardrailError,
10912
12079
  InMemoryVectorStore,
12080
+ LLAMACPP_PROFILE,
12081
+ LMSTUDIO_PROFILE,
12082
+ MISTRAL_PROFILE,
10913
12083
  MaxTurnsError,
10914
12084
  McpManager,
10915
12085
  MemoryManager,
10916
12086
  MemoryStore,
10917
12087
  NoConsensus,
10918
12088
  NoopSpanManager,
12089
+ OLLAMA_PROFILE,
12090
+ OPENAI_PRICING,
12091
+ OPENAI_PROFILE,
12092
+ OPENROUTER_PROFILE,
12093
+ OpenAICompatibleProvider,
10919
12094
  OpenAIEmbedder,
10920
12095
  OpenAIProvider,
10921
12096
  OpenAIResponsesProvider,
12097
+ ProviderError,
10922
12098
  ProviderRegistry,
10923
12099
  QuorumNotMet,
10924
12100
  REDACTED,
10925
12101
  REDACTION_RULES,
10926
12102
  RateLimiter,
10927
12103
  RedisStore,
12104
+ SGLANG_PROFILE,
10928
12105
  SQLiteStore,
10929
12106
  Session,
10930
12107
  SqliteVectorStore,
12108
+ ThinkTagScanner,
10931
12109
  TimeoutError,
10932
12110
  ToolDenied,
12111
+ VLLM_PROFILE,
10933
12112
  ValidationError,
10934
12113
  VerifyError,
10935
12114
  WorkflowContext,
12115
+ XAI_PROFILE,
10936
12116
  agent,
10937
12117
  createSpanManager,
10938
12118
  defineConfig,
12119
+ estimateOpenAICost,
10939
12120
  eventCostContribution,
10940
12121
  expandGlob,
10941
12122
  extractJson,
12123
+ extractThinkTags,
10942
12124
  findConfig,
12125
+ hasPositiveTokens,
10943
12126
  importModule,
10944
12127
  isCostBearingLeaf,
12128
+ isRetryableStatus,
10945
12129
  isRootLevel,
12130
+ isUnpricedLeaf,
12131
+ isUsableCost,
10946
12132
  needsTsxLoader,
12133
+ openaiReasoningEmit,
10947
12134
  parsePartialJson,
10948
12135
  pickDefault,
10949
12136
  pickExport,
12137
+ priceFromTable,
12138
+ reasoningEffortEmit,
12139
+ reasoningObjectEmit,
10950
12140
  redactEvent,
10951
12141
  registerConditions,
12142
+ resolveApiKey,
12143
+ resolvePerModel,
10952
12144
  resolveRuntime,
10953
12145
  resolveThinkingOptions,
10954
12146
  stringStreamFromEvents,