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