@odla-ai/ai 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -35,7 +35,14 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
35
35
  ));
36
36
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
37
37
 
38
- // src/shape/index.ts
38
+ // src/shape/blocks.ts
39
+ var init_blocks = __esm({
40
+ "src/shape/blocks.ts"() {
41
+ "use strict";
42
+ }
43
+ });
44
+
45
+ // src/shape/usage.ts
39
46
  function emptyUsage() {
40
47
  return { inputTokens: 0, outputTokens: 0 };
41
48
  }
@@ -49,39 +56,30 @@ function addUsage(into, more) {
49
56
  into.cacheReadTokens = (into.cacheReadTokens ?? 0) + more.cacheReadTokens;
50
57
  }
51
58
  }
52
- function isTextBlock(b) {
53
- return b.type === "text";
54
- }
55
- function isImageBlock(b) {
56
- return b.type === "image";
57
- }
58
- function isAudioBlock(b) {
59
- return b.type === "audio";
60
- }
61
- function isDocumentBlock(b) {
62
- return b.type === "document";
63
- }
64
- function isToolUseBlock(b) {
65
- return b.type === "tool_use";
66
- }
67
- function isToolResultBlock(b) {
68
- return b.type === "tool_result";
69
- }
70
- function isThinkingBlock(b) {
71
- return b.type === "thinking";
72
- }
73
- function blocksOf(content) {
74
- return typeof content === "string" ? [{ type: "text", text: content }] : content;
75
- }
76
- function extractText(content) {
77
- return content.filter(isTextBlock).map((b) => b.text).join("");
78
- }
79
- function extractToolUses(content) {
80
- return content.filter(isToolUseBlock);
81
- }
82
- var OdlaAIError, ConfigError, AuthError, RateLimitError, CapabilityError, ContextWindowError, InvalidRequestError, ProviderError;
83
- var init_shape = __esm({
84
- "src/shape/index.ts"() {
59
+ var init_usage = __esm({
60
+ "src/shape/usage.ts"() {
61
+ "use strict";
62
+ }
63
+ });
64
+
65
+ // src/shape/request.ts
66
+ var init_request = __esm({
67
+ "src/shape/request.ts"() {
68
+ "use strict";
69
+ }
70
+ });
71
+
72
+ // src/shape/events.ts
73
+ var init_events = __esm({
74
+ "src/shape/events.ts"() {
75
+ "use strict";
76
+ }
77
+ });
78
+
79
+ // src/shape/errors.ts
80
+ var OdlaAIError, ConfigError, AuthError, RateLimitError, CapabilityError, ContextWindowError, InvalidRequestError, ToolInputError, CancelledError, DeadlineExceededError, ProviderError;
81
+ var init_errors = __esm({
82
+ "src/shape/errors.ts"() {
85
83
  "use strict";
86
84
  OdlaAIError = class extends Error {
87
85
  code;
@@ -133,6 +131,23 @@ var init_shape = __esm({
133
131
  super("invalid_request", message, opts);
134
132
  }
135
133
  };
134
+ ToolInputError = class extends OdlaAIError {
135
+ tool;
136
+ constructor(message, opts) {
137
+ super("tool_input_invalid", message, { cause: opts?.cause });
138
+ this.tool = opts?.tool;
139
+ }
140
+ };
141
+ CancelledError = class extends OdlaAIError {
142
+ constructor(message = "AI request was cancelled.", opts) {
143
+ super("cancelled", message, opts);
144
+ }
145
+ };
146
+ DeadlineExceededError = class extends OdlaAIError {
147
+ constructor(message = "AI request deadline was exceeded.", opts) {
148
+ super("deadline_exceeded", message, opts);
149
+ }
150
+ };
136
151
  ProviderError = class extends OdlaAIError {
137
152
  constructor(message, opts) {
138
153
  super("provider_error", message, opts);
@@ -141,6 +156,423 @@ var init_shape = __esm({
141
156
  }
142
157
  });
143
158
 
159
+ // src/shape/helpers.ts
160
+ function isTextBlock(b) {
161
+ return b.type === "text";
162
+ }
163
+ function isImageBlock(b) {
164
+ return b.type === "image";
165
+ }
166
+ function isAudioBlock(b) {
167
+ return b.type === "audio";
168
+ }
169
+ function isDocumentBlock(b) {
170
+ return b.type === "document";
171
+ }
172
+ function isToolUseBlock(b) {
173
+ return b.type === "tool_use";
174
+ }
175
+ function isToolResultBlock(b) {
176
+ return b.type === "tool_result";
177
+ }
178
+ function isThinkingBlock(b) {
179
+ return b.type === "thinking";
180
+ }
181
+ function blocksOf(content) {
182
+ return typeof content === "string" ? [{ type: "text", text: content }] : content;
183
+ }
184
+ function extractText(content) {
185
+ return content.filter(isTextBlock).map((b) => b.text).join("");
186
+ }
187
+ function extractToolUses(content) {
188
+ return content.filter(isToolUseBlock);
189
+ }
190
+ var init_helpers = __esm({
191
+ "src/shape/helpers.ts"() {
192
+ "use strict";
193
+ }
194
+ });
195
+
196
+ // src/shape/json-schema-shape.ts
197
+ function validateSchemaShape(schema, path, root, issues) {
198
+ if (typeof schema === "boolean") return;
199
+ if (!isRecord(schema)) {
200
+ issue(issues, path, "schema must be an object or boolean");
201
+ return;
202
+ }
203
+ for (const key of Object.keys(schema)) {
204
+ if (!SUPPORTED_KEYS.has(key)) issue(issues, path, `schema uses unsupported keyword "${key}"`);
205
+ }
206
+ if (typeof schema.$ref === "string" && resolveLocalRef(root, schema.$ref) === void 0) {
207
+ issue(issues, path, `schema reference "${schema.$ref}" could not be resolved`);
208
+ } else if (schema.$ref !== void 0 && typeof schema.$ref !== "string") {
209
+ issue(issues, path, "schema $ref must be a string");
210
+ }
211
+ const types = typeof schema.type === "string" ? [schema.type] : Array.isArray(schema.type) ? schema.type : [];
212
+ if (schema.type !== void 0 && (types.length === 0 || !types.every((type) => typeof type === "string" && ALLOWED_TYPES.has(type)))) {
213
+ issue(issues, path, "schema type contains an unsupported JSON type");
214
+ }
215
+ if (schema.enum !== void 0 && !Array.isArray(schema.enum)) {
216
+ issue(issues, path, "schema enum must be an array");
217
+ }
218
+ if (schema.required !== void 0 && (!Array.isArray(schema.required) || !schema.required.every((key) => typeof key === "string"))) {
219
+ issue(issues, path, "schema required must be a string array");
220
+ }
221
+ validateConstraintShapes(schema, path, issues);
222
+ validateChildSchemas(schema, path, root, issues);
223
+ }
224
+ function validateChildSchemas(schema, path, root, issues) {
225
+ for (const key of ["properties", "$defs", "definitions"]) {
226
+ const group = schema[key];
227
+ if (group === void 0) continue;
228
+ if (!isRecord(group)) {
229
+ issue(issues, path, `schema ${key} must be an object`);
230
+ continue;
231
+ }
232
+ for (const [name, child] of Object.entries(group)) {
233
+ validateSchemaShape(child, `${path}.${key}.${name}`, root, issues);
234
+ }
235
+ }
236
+ for (const key of ["allOf", "anyOf", "oneOf"]) {
237
+ const group = schema[key];
238
+ if (group === void 0) continue;
239
+ if (!Array.isArray(group) || group.length === 0) {
240
+ issue(issues, path, `schema ${key} must be a non-empty array`);
241
+ continue;
242
+ }
243
+ group.forEach((child, index2) => validateSchemaShape(child, `${path}.${key}[${index2}]`, root, issues));
244
+ }
245
+ for (const key of ["items", "not", "additionalProperties"]) {
246
+ const child = schema[key];
247
+ if (child !== void 0) validateSchemaShape(child, `${path}.${key}`, root, issues);
248
+ }
249
+ }
250
+ function validateConstraintShapes(schema, path, issues) {
251
+ for (const key of ["minLength", "maxLength", "minItems", "maxItems", "minProperties", "maxProperties"]) {
252
+ const value = schema[key];
253
+ if (value !== void 0 && (!Number.isInteger(value) || value < 0)) {
254
+ issue(issues, path, `schema ${key} must be a non-negative integer`);
255
+ }
256
+ }
257
+ for (const key of ["minimum", "maximum", "exclusiveMinimum", "exclusiveMaximum"]) {
258
+ const value = schema[key];
259
+ if (value !== void 0 && !isFiniteNumber(value)) {
260
+ issue(issues, path, `schema ${key} must be a finite number`);
261
+ }
262
+ }
263
+ if (schema.multipleOf !== void 0 && (!isFiniteNumber(schema.multipleOf) || schema.multipleOf <= 0)) {
264
+ issue(issues, path, "schema multipleOf must be a finite number greater than zero");
265
+ }
266
+ if (schema.uniqueItems !== void 0 && typeof schema.uniqueItems !== "boolean") {
267
+ issue(issues, path, "schema uniqueItems must be a boolean");
268
+ }
269
+ validatePatternShape(schema.pattern, path, issues);
270
+ }
271
+ function validatePatternShape(value, path, issues) {
272
+ if (value === void 0) return;
273
+ if (typeof value !== "string") {
274
+ issue(issues, path, "schema pattern must be a string");
275
+ return;
276
+ }
277
+ try {
278
+ new RegExp(value, "u");
279
+ } catch {
280
+ issue(issues, path, `schema pattern "${value}" is invalid`);
281
+ }
282
+ }
283
+ function resolveLocalRef(root, ref) {
284
+ if (ref === "#") return root;
285
+ if (!ref.startsWith("#/")) return void 0;
286
+ let current = root;
287
+ for (const raw of ref.slice(2).split("/")) {
288
+ const key = raw.replace(/~1/g, "/").replace(/~0/g, "~");
289
+ if (!isRecord(current) || !Object.hasOwn(current, key)) return void 0;
290
+ current = current[key];
291
+ }
292
+ return current;
293
+ }
294
+ function isRecord(value) {
295
+ return value !== null && typeof value === "object" && !Array.isArray(value);
296
+ }
297
+ function isFiniteNumber(value) {
298
+ return typeof value === "number" && Number.isFinite(value);
299
+ }
300
+ function issue(issues, path, message) {
301
+ issues.push({ path, message });
302
+ }
303
+ var ANNOTATION_KEYS, SUPPORTED_KEYS, ALLOWED_TYPES;
304
+ var init_json_schema_shape = __esm({
305
+ "src/shape/json-schema-shape.ts"() {
306
+ "use strict";
307
+ ANNOTATION_KEYS = /* @__PURE__ */ new Set([
308
+ "$schema",
309
+ "$id",
310
+ "$anchor",
311
+ "title",
312
+ "description",
313
+ "default",
314
+ "examples",
315
+ "deprecated",
316
+ "readOnly",
317
+ "writeOnly",
318
+ "format"
319
+ ]);
320
+ SUPPORTED_KEYS = /* @__PURE__ */ new Set([
321
+ ...ANNOTATION_KEYS,
322
+ "$ref",
323
+ "$defs",
324
+ "definitions",
325
+ "type",
326
+ "enum",
327
+ "const",
328
+ "allOf",
329
+ "anyOf",
330
+ "oneOf",
331
+ "not",
332
+ "properties",
333
+ "required",
334
+ "additionalProperties",
335
+ "minProperties",
336
+ "maxProperties",
337
+ "items",
338
+ "minItems",
339
+ "maxItems",
340
+ "uniqueItems",
341
+ "minLength",
342
+ "maxLength",
343
+ "pattern",
344
+ "minimum",
345
+ "maximum",
346
+ "exclusiveMinimum",
347
+ "exclusiveMaximum",
348
+ "multipleOf"
349
+ ]);
350
+ ALLOWED_TYPES = /* @__PURE__ */ new Set([
351
+ "null",
352
+ "boolean",
353
+ "object",
354
+ "array",
355
+ "number",
356
+ "integer",
357
+ "string"
358
+ ]);
359
+ }
360
+ });
361
+
362
+ // src/shape/json-schema.ts
363
+ function validateJsonSchema(schema, value) {
364
+ const issues = [];
365
+ validateSchemaShape(schema, "$", schema, issues);
366
+ if (issues.length > 0) return { valid: false, issues };
367
+ visit(schema, value, "$", schema, issues, /* @__PURE__ */ new Set());
368
+ return { valid: issues.length === 0, issues };
369
+ }
370
+ function visit(schema, value, path, root, issues, refs) {
371
+ if (schema === true) return;
372
+ if (schema === false) {
373
+ issue2(issues, path, "is rejected by the schema");
374
+ return;
375
+ }
376
+ if (!isRecord2(schema)) {
377
+ issue2(issues, path, "schema must be an object or boolean");
378
+ return;
379
+ }
380
+ for (const key of Object.keys(schema)) {
381
+ if (!SUPPORTED_KEYS.has(key)) {
382
+ issue2(issues, path, `schema uses unsupported keyword "${key}"`);
383
+ return;
384
+ }
385
+ }
386
+ if (typeof schema.$ref === "string") {
387
+ const target = resolveLocalRef2(root, schema.$ref);
388
+ if (target === void 0) {
389
+ issue2(issues, path, `schema reference "${schema.$ref}" could not be resolved`);
390
+ return;
391
+ }
392
+ if (refs.has(schema.$ref)) {
393
+ issue2(issues, path, `cyclic schema reference "${schema.$ref}" is unsupported`);
394
+ return;
395
+ }
396
+ const nextRefs = new Set(refs);
397
+ nextRefs.add(schema.$ref);
398
+ visit(target, value, path, root, issues, nextRefs);
399
+ }
400
+ if (Array.isArray(schema.enum) && !schema.enum.some((candidate) => jsonEqual(candidate, value))) {
401
+ issue2(issues, path, "must equal one of the allowed enum values");
402
+ }
403
+ if (Object.hasOwn(schema, "const") && !jsonEqual(schema.const, value)) {
404
+ issue2(issues, path, "must equal the constant value");
405
+ }
406
+ if (Array.isArray(schema.allOf)) {
407
+ for (const child of schema.allOf) visit(child, value, path, root, issues, refs);
408
+ }
409
+ if (Array.isArray(schema.anyOf) && !schema.anyOf.some((child) => matches(child, value, root, refs))) {
410
+ issue2(issues, path, "must match at least one anyOf schema");
411
+ }
412
+ if (Array.isArray(schema.oneOf)) {
413
+ const matchesCount = schema.oneOf.filter((child) => matches(child, value, root, refs)).length;
414
+ if (matchesCount !== 1) issue2(issues, path, "must match exactly one oneOf schema");
415
+ }
416
+ if (schema.not !== void 0 && matches(schema.not, value, root, refs)) {
417
+ issue2(issues, path, "must not match the forbidden schema");
418
+ }
419
+ const types = typeof schema.type === "string" ? [schema.type] : Array.isArray(schema.type) && schema.type.every((t) => typeof t === "string") ? schema.type : void 0;
420
+ if (schema.type !== void 0 && !types) {
421
+ issue2(issues, path, "schema type must be a string or string array");
422
+ return;
423
+ }
424
+ if (types && !types.some((type) => isType(value, type))) {
425
+ issue2(issues, path, `must be ${types.join(" or ")}`);
426
+ return;
427
+ }
428
+ if (typeof value === "string") validateString(schema, value, path, issues);
429
+ if (typeof value === "number") validateNumber(schema, value, path, issues);
430
+ if (Array.isArray(value)) validateArray(schema, value, path, root, issues, refs);
431
+ if (isRecord2(value)) validateObject(schema, value, path, root, issues, refs);
432
+ }
433
+ function validateString(schema, value, path, issues) {
434
+ if (isFiniteNumber2(schema.minLength) && [...value].length < schema.minLength) issue2(issues, path, `must have at least ${schema.minLength} characters`);
435
+ if (isFiniteNumber2(schema.maxLength) && [...value].length > schema.maxLength) issue2(issues, path, `must have at most ${schema.maxLength} characters`);
436
+ if (typeof schema.pattern === "string") {
437
+ try {
438
+ if (!new RegExp(schema.pattern, "u").test(value)) issue2(issues, path, `must match pattern ${schema.pattern}`);
439
+ } catch {
440
+ issue2(issues, path, `schema pattern "${schema.pattern}" is invalid`);
441
+ }
442
+ }
443
+ }
444
+ function validateNumber(schema, value, path, issues) {
445
+ if (!Number.isFinite(value)) {
446
+ issue2(issues, path, "must be a finite number");
447
+ return;
448
+ }
449
+ if (isFiniteNumber2(schema.minimum) && value < schema.minimum) issue2(issues, path, `must be >= ${schema.minimum}`);
450
+ if (isFiniteNumber2(schema.maximum) && value > schema.maximum) issue2(issues, path, `must be <= ${schema.maximum}`);
451
+ if (isFiniteNumber2(schema.exclusiveMinimum) && value <= schema.exclusiveMinimum) issue2(issues, path, `must be > ${schema.exclusiveMinimum}`);
452
+ if (isFiniteNumber2(schema.exclusiveMaximum) && value >= schema.exclusiveMaximum) issue2(issues, path, `must be < ${schema.exclusiveMaximum}`);
453
+ if (isFiniteNumber2(schema.multipleOf) && schema.multipleOf > 0) {
454
+ const quotient = value / schema.multipleOf;
455
+ if (Math.abs(quotient - Math.round(quotient)) > Number.EPSILON * Math.max(1, Math.abs(quotient))) {
456
+ issue2(issues, path, `must be a multiple of ${schema.multipleOf}`);
457
+ }
458
+ }
459
+ }
460
+ function validateArray(schema, value, path, root, issues, refs) {
461
+ if (isFiniteNumber2(schema.minItems) && value.length < schema.minItems) issue2(issues, path, `must contain at least ${schema.minItems} items`);
462
+ if (isFiniteNumber2(schema.maxItems) && value.length > schema.maxItems) issue2(issues, path, `must contain at most ${schema.maxItems} items`);
463
+ if (schema.uniqueItems === true) {
464
+ for (let i = 0; i < value.length; i++) {
465
+ if (value.slice(0, i).some((other) => jsonEqual(other, value[i]))) {
466
+ issue2(issues, `${path}[${i}]`, "must be unique");
467
+ break;
468
+ }
469
+ }
470
+ }
471
+ if (schema.items !== void 0) {
472
+ for (let i = 0; i < value.length; i++) visit(schema.items, value[i], `${path}[${i}]`, root, issues, refs);
473
+ }
474
+ }
475
+ function validateObject(schema, value, path, root, issues, refs) {
476
+ const keys = Object.keys(value);
477
+ if (isFiniteNumber2(schema.minProperties) && keys.length < schema.minProperties) issue2(issues, path, `must contain at least ${schema.minProperties} properties`);
478
+ if (isFiniteNumber2(schema.maxProperties) && keys.length > schema.maxProperties) issue2(issues, path, `must contain at most ${schema.maxProperties} properties`);
479
+ if (schema.required !== void 0 && (!Array.isArray(schema.required) || !schema.required.every((key) => typeof key === "string"))) {
480
+ issue2(issues, path, "schema required must be a string array");
481
+ return;
482
+ }
483
+ for (const key of schema.required ?? []) {
484
+ if (!Object.hasOwn(value, key)) issue2(issues, childPath(path, key), "is required");
485
+ }
486
+ const properties = isRecord2(schema.properties) ? schema.properties : {};
487
+ for (const [key, child] of Object.entries(properties)) {
488
+ if (Object.hasOwn(value, key)) visit(child, value[key], childPath(path, key), root, issues, refs);
489
+ }
490
+ const extras = keys.filter((key) => !Object.hasOwn(properties, key));
491
+ if (schema.additionalProperties === false) {
492
+ for (const key of extras) issue2(issues, childPath(path, key), "is not an allowed property");
493
+ } else if (schema.additionalProperties !== void 0 && schema.additionalProperties !== true) {
494
+ for (const key of extras) visit(schema.additionalProperties, value[key], childPath(path, key), root, issues, refs);
495
+ }
496
+ }
497
+ function matches(schema, value, root, refs) {
498
+ const nested = [];
499
+ visit(schema, value, "$", root, nested, refs);
500
+ return nested.length === 0;
501
+ }
502
+ function resolveLocalRef2(root, ref) {
503
+ if (ref === "#") return root;
504
+ if (!ref.startsWith("#/")) return void 0;
505
+ let current = root;
506
+ for (const raw of ref.slice(2).split("/")) {
507
+ const key = raw.replace(/~1/g, "/").replace(/~0/g, "~");
508
+ if (!isRecord2(current) || !Object.hasOwn(current, key)) return void 0;
509
+ current = current[key];
510
+ }
511
+ return current;
512
+ }
513
+ function isRecord2(value) {
514
+ return value !== null && typeof value === "object" && !Array.isArray(value);
515
+ }
516
+ function isType(value, type) {
517
+ switch (type) {
518
+ case "null":
519
+ return value === null;
520
+ case "boolean":
521
+ return typeof value === "boolean";
522
+ case "object":
523
+ return isRecord2(value);
524
+ case "array":
525
+ return Array.isArray(value);
526
+ case "number":
527
+ return typeof value === "number" && Number.isFinite(value);
528
+ case "integer":
529
+ return typeof value === "number" && Number.isInteger(value);
530
+ case "string":
531
+ return typeof value === "string";
532
+ default:
533
+ return false;
534
+ }
535
+ }
536
+ function isFiniteNumber2(value) {
537
+ return typeof value === "number" && Number.isFinite(value);
538
+ }
539
+ function jsonEqual(a, b) {
540
+ if (Object.is(a, b)) return true;
541
+ if (Array.isArray(a) && Array.isArray(b)) return a.length === b.length && a.every((v, i) => jsonEqual(v, b[i]));
542
+ if (isRecord2(a) && isRecord2(b)) {
543
+ const aKeys = Object.keys(a);
544
+ const bKeys = Object.keys(b);
545
+ return aKeys.length === bKeys.length && aKeys.every((key) => Object.hasOwn(b, key) && jsonEqual(a[key], b[key]));
546
+ }
547
+ return false;
548
+ }
549
+ function childPath(path, key) {
550
+ return /^[A-Za-z_$][\w$]*$/.test(key) ? `${path}.${key}` : `${path}[${JSON.stringify(key)}]`;
551
+ }
552
+ function issue2(issues, path, message) {
553
+ issues.push({ path, message });
554
+ }
555
+ var init_json_schema = __esm({
556
+ "src/shape/json-schema.ts"() {
557
+ "use strict";
558
+ init_json_schema_shape();
559
+ }
560
+ });
561
+
562
+ // src/shape/index.ts
563
+ var init_shape = __esm({
564
+ "src/shape/index.ts"() {
565
+ "use strict";
566
+ init_blocks();
567
+ init_usage();
568
+ init_request();
569
+ init_events();
570
+ init_errors();
571
+ init_helpers();
572
+ init_json_schema();
573
+ }
574
+ });
575
+
144
576
  // src/providers/errors.ts
145
577
  function statusOf(err) {
146
578
  if (err && typeof err === "object") {
@@ -168,8 +600,10 @@ function messageOf(err) {
168
600
  if (typeof err === "string") return err;
169
601
  return "unknown provider error";
170
602
  }
171
- function normalizeError(err, provider) {
603
+ function normalizeError(err, provider, signal) {
172
604
  if (err instanceof OdlaAIError) return err;
605
+ const cancellation = cancellationError(signal?.aborted ? signal.reason : err);
606
+ if (signal?.aborted || cancellation) return cancellation ?? new CancelledError(void 0, { cause: err });
173
607
  const status = statusOf(err);
174
608
  const message = messageOf(err);
175
609
  const tagged = `[${provider}] ${message}`;
@@ -185,21 +619,27 @@ function normalizeError(err, provider) {
185
619
  }
186
620
  return new ProviderError(tagged, { providerStatus: status, cause: err });
187
621
  }
188
- function mapProviderError(err, provider) {
189
- throw normalizeError(err, provider);
622
+ function mapProviderError(err, provider, signal) {
623
+ throw normalizeError(err, provider, signal);
190
624
  }
191
- var init_errors = __esm({
625
+ function cancellationError(reason) {
626
+ if (reason instanceof DeadlineExceededError || reason instanceof CancelledError) return reason;
627
+ if (!reason || typeof reason !== "object") return void 0;
628
+ const rec = reason;
629
+ if (rec.name === "TimeoutError") return new DeadlineExceededError(void 0, { cause: reason });
630
+ if (rec.name === "AbortError" || rec.name === "APIUserAbortError" || rec.code === "ABORT_ERR") {
631
+ return new CancelledError(void 0, { cause: reason });
632
+ }
633
+ return void 0;
634
+ }
635
+ var init_errors2 = __esm({
192
636
  "src/providers/errors.ts"() {
193
637
  "use strict";
194
638
  init_shape();
195
639
  }
196
640
  });
197
641
 
198
- // src/providers/anthropic.ts
199
- var anthropic_exports = {};
200
- __export(anthropic_exports, {
201
- AnthropicProvider: () => AnthropicProvider
202
- });
642
+ // src/providers/anthropic/request.ts
203
643
  function buildParams(spec, req, messages) {
204
644
  const params = {
205
645
  model: spec.nativeId,
@@ -287,12 +727,33 @@ function mapToolChoice(tc) {
287
727
  if (tc === "none") return { type: "none" };
288
728
  return { type: "tool", name: tc.name };
289
729
  }
730
+ var init_request2 = __esm({
731
+ "src/providers/anthropic/request.ts"() {
732
+ "use strict";
733
+ }
734
+ });
735
+
736
+ // src/providers/tool-input.ts
737
+ function toolInput(value, provider, name) {
738
+ if (value !== null && typeof value === "object" && !Array.isArray(value)) {
739
+ return value;
740
+ }
741
+ throw new ToolInputError(`[${provider}] tool "${name}" returned arguments that are not a JSON object.`, { tool: name });
742
+ }
743
+ var init_tool_input = __esm({
744
+ "src/providers/tool-input.ts"() {
745
+ "use strict";
746
+ init_shape();
747
+ }
748
+ });
749
+
750
+ // src/providers/anthropic/response.ts
290
751
  function mapContent(blocks) {
291
752
  const out = [];
292
753
  for (const b of blocks) {
293
754
  if (b.type === "text") out.push({ type: "text", text: b.text });
294
755
  else if (b.type === "tool_use") {
295
- out.push({ type: "tool_use", id: b.id, name: b.name, input: b.input ?? {} });
756
+ out.push({ type: "tool_use", id: b.id, name: b.name, input: toolInput(b.input ?? {}, "anthropic", b.name) });
296
757
  } else if (b.type === "thinking") {
297
758
  out.push({ type: "thinking", thinking: b.thinking, signature: b.signature });
298
759
  }
@@ -318,6 +779,14 @@ function mapStop(reason) {
318
779
  return "end_turn";
319
780
  }
320
781
  }
782
+ var init_response = __esm({
783
+ "src/providers/anthropic/response.ts"() {
784
+ "use strict";
785
+ init_tool_input();
786
+ }
787
+ });
788
+
789
+ // src/providers/anthropic/stream.ts
321
790
  function mapStreamEvent(raw, req) {
322
791
  switch (raw.type) {
323
792
  case "message_start":
@@ -359,13 +828,58 @@ function mapStartBlock(cb) {
359
828
  if (cb.type === "thinking") return { type: "thinking", thinking: cb.thinking, signature: cb.signature };
360
829
  return void 0;
361
830
  }
831
+ var init_stream = __esm({
832
+ "src/providers/anthropic/stream.ts"() {
833
+ "use strict";
834
+ init_response();
835
+ }
836
+ });
837
+
838
+ // src/client/abort.ts
839
+ function signalWithDeadline(signal, deadline) {
840
+ if (deadline === void 0) return signal;
841
+ if (!Number.isSafeInteger(deadline)) throw new ConfigError("deadline must be a finite integer Unix timestamp in milliseconds.");
842
+ const remaining = deadline - Date.now();
843
+ if (remaining <= 0) {
844
+ const expired = new AbortController();
845
+ expired.abort(new DeadlineExceededError());
846
+ return signal ? AbortSignal.any([signal, expired.signal]) : expired.signal;
847
+ }
848
+ const timeout = AbortSignal.timeout(remaining);
849
+ return signal ? AbortSignal.any([signal, timeout]) : timeout;
850
+ }
851
+ function throwIfAborted(signal) {
852
+ if (!signal?.aborted) return;
853
+ const reason = signal.reason;
854
+ if (reason instanceof DeadlineExceededError || reason instanceof CancelledError) throw reason;
855
+ if (reason && typeof reason === "object" && reason.name === "TimeoutError") {
856
+ throw new DeadlineExceededError(void 0, { cause: reason });
857
+ }
858
+ throw new CancelledError(void 0, { cause: reason });
859
+ }
860
+ var init_abort = __esm({
861
+ "src/client/abort.ts"() {
862
+ "use strict";
863
+ init_errors();
864
+ }
865
+ });
866
+
867
+ // src/providers/anthropic.ts
868
+ var anthropic_exports = {};
869
+ __export(anthropic_exports, {
870
+ AnthropicProvider: () => AnthropicProvider
871
+ });
362
872
  var import_sdk, AnthropicProvider;
363
873
  var init_anthropic = __esm({
364
874
  "src/providers/anthropic.ts"() {
365
875
  "use strict";
366
876
  import_sdk = __toESM(require("@anthropic-ai/sdk"), 1);
367
- init_errors();
877
+ init_errors2();
368
878
  init_shape();
879
+ init_request2();
880
+ init_response();
881
+ init_stream();
882
+ init_abort();
369
883
  AnthropicProvider = class {
370
884
  id = "anthropic";
371
885
  client;
@@ -374,6 +888,7 @@ var init_anthropic = __esm({
374
888
  this.client = client ?? new import_sdk.default({ apiKey });
375
889
  }
376
890
  async create(spec, req) {
891
+ const signal = signalWithDeadline(req.signal, req.deadline);
377
892
  try {
378
893
  let messages = toAnthropicMessages(req);
379
894
  const content = [];
@@ -382,7 +897,8 @@ var init_anthropic = __esm({
382
897
  let id = "";
383
898
  for (let i = 0; i < 8; i++) {
384
899
  const res = await this.client.messages.create(
385
- buildParams(spec, req, messages)
900
+ buildParams(spec, req, messages),
901
+ { signal }
386
902
  );
387
903
  id = res.id;
388
904
  addUsage(usage, mapUsage(res.usage));
@@ -396,34 +912,39 @@ var init_anthropic = __esm({
396
912
  }
397
913
  return { id, model: req.model, provider: "anthropic", role: "assistant", content, stopReason, usage };
398
914
  } catch (err) {
399
- mapProviderError(err, "anthropic");
915
+ mapProviderError(err, "anthropic", signal);
400
916
  }
401
917
  }
402
918
  async *stream(spec, req) {
919
+ const signal = signalWithDeadline(req.signal, req.deadline);
403
920
  try {
404
921
  const events = this.client.messages.stream(
405
- buildParams(spec, req, toAnthropicMessages(req))
922
+ buildParams(spec, req, toAnthropicMessages(req)),
923
+ { signal }
406
924
  );
407
925
  for await (const raw of events) {
408
926
  const ev = mapStreamEvent(raw, req);
409
927
  if (ev) yield ev;
410
928
  }
411
929
  } catch (err) {
412
- yield { type: "error", error: normalizeError(err, "anthropic").toShape() };
930
+ yield { type: "error", error: normalizeError(err, "anthropic", signal).toShape() };
413
931
  }
414
932
  }
415
933
  };
416
934
  }
417
935
  });
418
936
 
419
- // src/providers/openai.ts
420
- var openai_exports = {};
421
- __export(openai_exports, {
422
- OpenAIProvider: () => OpenAIProvider,
423
- mapResponse: () => mapResponse,
424
- parseArgs: () => parseArgs,
425
- toOpenAIMessages: () => toOpenAIMessages
937
+ // src/providers/blocks.ts
938
+ function stringifyBlocks(blocks) {
939
+ return blocks.map((b) => b.type === "text" ? b.text : `[${b.type}]`).join("");
940
+ }
941
+ var init_blocks2 = __esm({
942
+ "src/providers/blocks.ts"() {
943
+ "use strict";
944
+ }
426
945
  });
946
+
947
+ // src/providers/openai/request.ts
427
948
  function buildParams2(spec, req, stream) {
428
949
  const params = {
429
950
  model: spec.nativeId,
@@ -520,9 +1041,6 @@ function audioFormat(m) {
520
1041
  if (m === "audio/mp3" || m === "audio/mpeg") return "mp3";
521
1042
  throw new CapabilityError(`OpenAI audio input supports wav and mp3, not "${m}".`);
522
1043
  }
523
- function stringifyBlocks(blocks) {
524
- return blocks.map((b) => b.type === "text" ? b.text : `[${b.type}]`).join("");
525
- }
526
1044
  function buildTools2(req) {
527
1045
  if (!req.tools || req.tools.length === 0) return void 0;
528
1046
  return req.tools.map((t) => ({
@@ -542,6 +1060,15 @@ function reasoningEffort(e) {
542
1060
  if (e === "medium") return "medium";
543
1061
  return "high";
544
1062
  }
1063
+ var init_request3 = __esm({
1064
+ "src/providers/openai/request.ts"() {
1065
+ "use strict";
1066
+ init_shape();
1067
+ init_blocks2();
1068
+ }
1069
+ });
1070
+
1071
+ // src/providers/openai/response.ts
545
1072
  function mapResponse(res, req) {
546
1073
  const choice = res.choices[0];
547
1074
  const content = [];
@@ -565,9 +1092,10 @@ function parseArgs(raw) {
565
1092
  if (!raw) return {};
566
1093
  try {
567
1094
  const parsed = JSON.parse(raw);
568
- return parsed && typeof parsed === "object" ? parsed : {};
569
- } catch {
570
- return {};
1095
+ return toolInput(parsed, "openai", "function");
1096
+ } catch (error) {
1097
+ if (error instanceof ToolInputError) throw error;
1098
+ throw new ToolInputError("[openai] tool returned malformed JSON arguments.", { cause: error });
571
1099
  }
572
1100
  }
573
1101
  function mapFinish(reason) {
@@ -591,6 +1119,15 @@ function mapUsage2(u) {
591
1119
  if (cached != null) usage.cacheReadTokens = cached;
592
1120
  return usage;
593
1121
  }
1122
+ var init_response2 = __esm({
1123
+ "src/providers/openai/response.ts"() {
1124
+ "use strict";
1125
+ init_shape();
1126
+ init_tool_input();
1127
+ }
1128
+ });
1129
+
1130
+ // src/providers/openai/stream.ts
594
1131
  async function* mapStream(stream, req) {
595
1132
  let started = false;
596
1133
  let textOpen = false;
@@ -638,13 +1175,32 @@ async function* mapStream(stream, req) {
638
1175
  yield { type: "message_delta", delta: { stopReason }, usage };
639
1176
  yield { type: "message_stop" };
640
1177
  }
1178
+ var init_stream2 = __esm({
1179
+ "src/providers/openai/stream.ts"() {
1180
+ "use strict";
1181
+ init_shape();
1182
+ init_response2();
1183
+ }
1184
+ });
1185
+
1186
+ // src/providers/openai.ts
1187
+ var openai_exports = {};
1188
+ __export(openai_exports, {
1189
+ OpenAIProvider: () => OpenAIProvider,
1190
+ mapResponse: () => mapResponse,
1191
+ parseArgs: () => parseArgs,
1192
+ toOpenAIMessages: () => toOpenAIMessages
1193
+ });
641
1194
  var import_openai2, OpenAIProvider;
642
1195
  var init_openai = __esm({
643
1196
  "src/providers/openai.ts"() {
644
1197
  "use strict";
645
1198
  import_openai2 = __toESM(require("openai"), 1);
646
- init_errors();
647
- init_shape();
1199
+ init_errors2();
1200
+ init_request3();
1201
+ init_response2();
1202
+ init_stream2();
1203
+ init_abort();
648
1204
  OpenAIProvider = class {
649
1205
  id = "openai";
650
1206
  client;
@@ -652,36 +1208,34 @@ var init_openai = __esm({
652
1208
  this.client = client ?? new import_openai2.default({ apiKey });
653
1209
  }
654
1210
  async create(spec, req) {
1211
+ const signal = signalWithDeadline(req.signal, req.deadline);
655
1212
  try {
656
1213
  const res = await this.client.chat.completions.create(
657
- buildParams2(spec, req, false)
1214
+ buildParams2(spec, req, false),
1215
+ { signal }
658
1216
  );
659
1217
  return mapResponse(res, req);
660
1218
  } catch (err) {
661
- mapProviderError(err, "openai");
1219
+ mapProviderError(err, "openai", signal);
662
1220
  }
663
1221
  }
664
1222
  async *stream(spec, req) {
1223
+ const signal = signalWithDeadline(req.signal, req.deadline);
665
1224
  try {
666
1225
  const stream = await this.client.chat.completions.create(
667
- buildParams2(spec, req, true)
1226
+ buildParams2(spec, req, true),
1227
+ { signal }
668
1228
  );
669
1229
  yield* mapStream(stream, req);
670
1230
  } catch (err) {
671
- yield { type: "error", error: normalizeError(err, "openai").toShape() };
1231
+ yield { type: "error", error: normalizeError(err, "openai", signal).toShape() };
672
1232
  }
673
1233
  }
674
1234
  };
675
1235
  }
676
1236
  });
677
1237
 
678
- // src/providers/google.ts
679
- var google_exports = {};
680
- __export(google_exports, {
681
- GoogleProvider: () => GoogleProvider,
682
- mapResponse: () => mapResponse2,
683
- toContents: () => toContents
684
- });
1238
+ // src/providers/google/request.ts
685
1239
  function buildParams3(spec, req) {
686
1240
  const config = { maxOutputTokens: req.maxTokens };
687
1241
  if (req.system !== void 0) {
@@ -729,16 +1283,13 @@ function toPart(b) {
729
1283
  functionResponse: {
730
1284
  // Gemini correlates by name; the canonical tool_use id is keyed to it.
731
1285
  name: b.toolUseId,
732
- response: { result: typeof b.content === "string" ? b.content : stringifyBlocks2(b.content) }
1286
+ response: { result: typeof b.content === "string" ? b.content : stringifyBlocks(b.content) }
733
1287
  }
734
1288
  };
735
1289
  case "thinking":
736
1290
  return { text: "" };
737
1291
  }
738
1292
  }
739
- function stringifyBlocks2(blocks) {
740
- return blocks.map((b) => b.type === "text" ? b.text : `[${b.type}]`).join("");
741
- }
742
1293
  function buildTools3(req) {
743
1294
  const tools = [];
744
1295
  if (req.tools && req.tools.length > 0) {
@@ -759,6 +1310,17 @@ function mapToolChoice3(tc) {
759
1310
  if (tc === "any") return { functionCallingConfig: { mode: import_genai.FunctionCallingConfigMode.ANY } };
760
1311
  return { functionCallingConfig: { mode: import_genai.FunctionCallingConfigMode.ANY, allowedFunctionNames: [tc.name] } };
761
1312
  }
1313
+ var import_genai;
1314
+ var init_request4 = __esm({
1315
+ "src/providers/google/request.ts"() {
1316
+ "use strict";
1317
+ import_genai = require("@google/genai");
1318
+ init_shape();
1319
+ init_blocks2();
1320
+ }
1321
+ });
1322
+
1323
+ // src/providers/google/response.ts
762
1324
  function mapResponse2(res, req) {
763
1325
  const candidate = res.candidates?.[0];
764
1326
  const parts = candidate?.content?.parts ?? [];
@@ -769,7 +1331,7 @@ function mapResponse2(res, req) {
769
1331
  else if (p.functionCall) {
770
1332
  sawToolUse = true;
771
1333
  const name = p.functionCall.name ?? "";
772
- content.push({ type: "tool_use", id: p.functionCall.id ?? name, name, input: p.functionCall.args ?? {} });
1334
+ content.push({ type: "tool_use", id: p.functionCall.id ?? name, name, input: toolInput(p.functionCall.args ?? {}, "google", name) });
773
1335
  }
774
1336
  }
775
1337
  return {
@@ -807,6 +1369,14 @@ function mapUsage3(res) {
807
1369
  if (u?.cachedContentTokenCount != null) usage.cacheReadTokens = u.cachedContentTokenCount;
808
1370
  return usage;
809
1371
  }
1372
+ var init_response3 = __esm({
1373
+ "src/providers/google/response.ts"() {
1374
+ "use strict";
1375
+ init_tool_input();
1376
+ }
1377
+ });
1378
+
1379
+ // src/providers/google/stream.ts
810
1380
  async function* mapStream2(iter, req) {
811
1381
  let started = false;
812
1382
  let textOpen = false;
@@ -850,33 +1420,57 @@ async function* mapStream2(iter, req) {
850
1420
  yield { type: "message_delta", delta: { stopReason }, usage };
851
1421
  yield { type: "message_stop" };
852
1422
  }
853
- var import_genai, GoogleProvider;
1423
+ var init_stream3 = __esm({
1424
+ "src/providers/google/stream.ts"() {
1425
+ "use strict";
1426
+ init_shape();
1427
+ init_response3();
1428
+ }
1429
+ });
1430
+
1431
+ // src/providers/google.ts
1432
+ var google_exports = {};
1433
+ __export(google_exports, {
1434
+ GoogleProvider: () => GoogleProvider,
1435
+ mapResponse: () => mapResponse2,
1436
+ toContents: () => toContents
1437
+ });
1438
+ var import_genai2, GoogleProvider;
854
1439
  var init_google = __esm({
855
1440
  "src/providers/google.ts"() {
856
1441
  "use strict";
857
- import_genai = require("@google/genai");
858
- init_errors();
859
- init_shape();
1442
+ import_genai2 = require("@google/genai");
1443
+ init_errors2();
1444
+ init_request4();
1445
+ init_response3();
1446
+ init_stream3();
1447
+ init_abort();
860
1448
  GoogleProvider = class {
861
1449
  id = "google";
862
1450
  client;
863
1451
  constructor(apiKey, client) {
864
- this.client = client ?? new import_genai.GoogleGenAI({ apiKey });
1452
+ this.client = client ?? new import_genai2.GoogleGenAI({ apiKey });
865
1453
  }
866
1454
  async create(spec, req) {
1455
+ const signal = signalWithDeadline(req.signal, req.deadline);
867
1456
  try {
868
- const res = await this.client.models.generateContent(buildParams3(spec, req));
1457
+ const params = buildParams3(spec, req);
1458
+ params.config = { ...params.config, abortSignal: signal };
1459
+ const res = await this.client.models.generateContent(params);
869
1460
  return mapResponse2(res, req);
870
1461
  } catch (err) {
871
- mapProviderError(err, "google");
1462
+ mapProviderError(err, "google", signal);
872
1463
  }
873
1464
  }
874
1465
  async *stream(spec, req) {
1466
+ const signal = signalWithDeadline(req.signal, req.deadline);
875
1467
  try {
876
- const iter = await this.client.models.generateContentStream(buildParams3(spec, req));
1468
+ const params = buildParams3(spec, req);
1469
+ params.config = { ...params.config, abortSignal: signal };
1470
+ const iter = await this.client.models.generateContentStream(params);
877
1471
  yield* mapStream2(iter, req);
878
1472
  } catch (err) {
879
- yield { type: "error", error: normalizeError(err, "google").toShape() };
1473
+ yield { type: "error", error: normalizeError(err, "google", signal).toShape() };
880
1474
  }
881
1475
  }
882
1476
  };
@@ -889,11 +1483,13 @@ __export(index_exports, {
889
1483
  AGENT_SCHEMA: () => AGENT_SCHEMA,
890
1484
  ANTHROPIC_MODELS: () => ANTHROPIC_MODELS,
891
1485
  AuthError: () => AuthError,
1486
+ CancelledError: () => CancelledError,
892
1487
  CapabilityError: () => CapabilityError,
893
1488
  ConfigError: () => ConfigError,
894
1489
  ContextWindowError: () => ContextWindowError,
895
1490
  DEFAULT_CATALOG: () => DEFAULT_CATALOG,
896
1491
  DEFAULT_SECRET_NAMES: () => DEFAULT_SECRET_NAMES,
1492
+ DeadlineExceededError: () => DeadlineExceededError,
897
1493
  GOOGLE_MODELS: () => GOOGLE_MODELS,
898
1494
  InMemoryScope: () => InMemoryScope,
899
1495
  InvalidRequestError: () => InvalidRequestError,
@@ -904,6 +1500,7 @@ __export(index_exports, {
904
1500
  ProviderError: () => ProviderError,
905
1501
  RateLimitError: () => RateLimitError,
906
1502
  TaintError: () => TaintError,
1503
+ ToolInputError: () => ToolInputError,
907
1504
  addUsage: () => addUsage,
908
1505
  assertSinkAcceptsTaint: () => assertSinkAcceptsTaint,
909
1506
  blocksOf: () => blocksOf,
@@ -949,6 +1546,7 @@ __export(index_exports, {
949
1546
  structuredMatch: () => structuredMatch,
950
1547
  taint: () => taint,
951
1548
  toOracleTool: () => toOracleTool,
1549
+ validateJsonSchema: () => validateJsonSchema,
952
1550
  validateRequest: () => validateRequest
953
1551
  });
954
1552
  module.exports = __toCommonJS(index_exports);
@@ -958,7 +1556,7 @@ init_shape();
958
1556
 
959
1557
  // src/shape/capabilities.ts
960
1558
  init_shape();
961
- function chatCapabilities(overrides = {}) {
1559
+ function chatCapabilities(overrides2 = {}) {
962
1560
  return {
963
1561
  kind: "chat",
964
1562
  textIn: true,
@@ -971,7 +1569,7 @@ function chatCapabilities(overrides = {}) {
971
1569
  streaming: true,
972
1570
  structuredOutput: true,
973
1571
  webSearch: false,
974
- ...overrides
1572
+ ...overrides2
975
1573
  };
976
1574
  }
977
1575
  function validateRequest(spec, req) {
@@ -1063,6 +1661,15 @@ var OPENAI_MODELS = [
1063
1661
  capabilities: chatCapabilities({ effort: true }),
1064
1662
  contextWindow: 4e5
1065
1663
  },
1664
+ {
1665
+ // Compact GPT-5.4 reasoning model (effort knob), same 400K context as the
1666
+ // rest of the GPT-5 line. The default for odla-o11y AI triage.
1667
+ id: "gpt-5.4-mini",
1668
+ provider: "openai",
1669
+ nativeId: "gpt-5.4-mini",
1670
+ capabilities: chatCapabilities({ effort: true }),
1671
+ contextWindow: 4e5
1672
+ },
1066
1673
  {
1067
1674
  id: "gpt-5",
1068
1675
  provider: "openai",
@@ -1180,11 +1787,29 @@ function providersInCatalog(catalog) {
1180
1787
  }
1181
1788
 
1182
1789
  // src/providers/registry.ts
1790
+ var CLIENT_TTL_MS = 6e4;
1183
1791
  var cache = /* @__PURE__ */ new Map();
1792
+ var inflight = /* @__PURE__ */ new Map();
1793
+ var overrides = /* @__PURE__ */ new Map();
1184
1794
  async function getProvider(id, apiKey) {
1185
- const cacheKey = `${id}:${apiKey}`;
1186
- const existing = cache.get(cacheKey);
1187
- if (existing) return existing;
1795
+ const override = overrides.get(id);
1796
+ if (override?.apiKey === apiKey) return override.provider;
1797
+ const fingerprint = await fingerprintKey(apiKey);
1798
+ const existing = cache.get(id);
1799
+ if (existing?.fingerprint === fingerprint && existing.expiresAt > Date.now()) return existing.provider;
1800
+ if (existing) cache.delete(id);
1801
+ const pending = inflight.get(id);
1802
+ if (pending?.fingerprint === fingerprint) return pending.promise;
1803
+ const promise = createProvider(id, apiKey).then((provider) => {
1804
+ cache.set(id, { fingerprint, provider, expiresAt: Date.now() + CLIENT_TTL_MS });
1805
+ return provider;
1806
+ }).finally(() => {
1807
+ if (inflight.get(id)?.promise === promise) inflight.delete(id);
1808
+ });
1809
+ inflight.set(id, { fingerprint, promise });
1810
+ return promise;
1811
+ }
1812
+ async function createProvider(id, apiKey) {
1188
1813
  let provider;
1189
1814
  switch (id) {
1190
1815
  case "anthropic": {
@@ -1203,14 +1828,19 @@ async function getProvider(id, apiKey) {
1203
1828
  break;
1204
1829
  }
1205
1830
  }
1206
- cache.set(cacheKey, provider);
1207
1831
  return provider;
1208
1832
  }
1833
+ async function fingerprintKey(apiKey) {
1834
+ const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(apiKey));
1835
+ return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
1836
+ }
1209
1837
  function registerProvider(id, apiKey, provider) {
1210
- cache.set(`${id}:${apiKey}`, provider);
1838
+ overrides.set(id, { apiKey, provider });
1211
1839
  }
1212
1840
  function clearProviderCache() {
1213
1841
  cache.clear();
1842
+ inflight.clear();
1843
+ overrides.clear();
1214
1844
  }
1215
1845
 
1216
1846
  // src/client/keys.ts
@@ -1281,7 +1911,9 @@ function init(opts = {}) {
1281
1911
  messages: [{ role: "user", content: c.user }],
1282
1912
  tools: [{ name: c.tool.name, description: c.tool.description ?? "", inputSchema: c.tool.parameters }],
1283
1913
  toolChoice: { type: "tool", name: c.tool.name },
1284
- maxTokens: c.maxTokens ?? 4096
1914
+ maxTokens: c.maxTokens ?? 4096,
1915
+ signal: c.signal,
1916
+ deadline: c.deadline
1285
1917
  };
1286
1918
  const { spec, provider } = await resolveProviderFor(model);
1287
1919
  validateRequest(spec, req);
@@ -1290,6 +1922,11 @@ function init(opts = {}) {
1290
1922
  if (!block || block.type !== "tool_use") {
1291
1923
  throw new ProviderError(`Model "${model}" did not return the forced tool call "${c.tool.name}".`);
1292
1924
  }
1925
+ const validation = validateJsonSchema(c.tool.parameters, block.input);
1926
+ if (!validation.valid) {
1927
+ const detail = validation.issues.slice(0, 5).map((i) => `${i.path} ${i.message}`).join("; ");
1928
+ throw new ToolInputError(`Tool "${c.tool.name}" returned invalid input: ${detail}`, { tool: c.tool.name });
1929
+ }
1293
1930
  return { value: block.input, usage: res.usage };
1294
1931
  };
1295
1932
  const search = async (c) => {
@@ -1299,7 +1936,9 @@ function init(opts = {}) {
1299
1936
  system: c.system,
1300
1937
  messages: [{ role: "user", content: c.user }],
1301
1938
  webSearch: true,
1302
- maxTokens: c.maxTokens ?? 8192
1939
+ maxTokens: c.maxTokens ?? 8192,
1940
+ signal: c.signal,
1941
+ deadline: c.deadline
1303
1942
  };
1304
1943
  const { spec, provider } = await resolveProviderFor(model);
1305
1944
  validateRequest(spec, req);
@@ -1328,16 +1967,20 @@ function generateLlmsTxt(catalog = DEFAULT_CATALOG) {
1328
1967
  const models = Object.values(catalog).map((s) => `- \`${s.id}\` (${s.provider}) \u2014 ${capsSummary(s.capabilities)}`).join("\n");
1329
1968
  return `# @odla-ai/ai \u2014 LLM context
1330
1969
 
1970
+ > EARLY ACCESS \u2014 pre-1.0. Agents work from bounded runbooks; humans approve
1971
+ > credentials, production changes, releases, and merges. APIs and exact package
1972
+ > availability can change. Review documented guarantees and limitations; this
1973
+ > software is MIT-licensed and provided without warranty.
1974
+
1331
1975
  One general-purpose interface for AI inference across Anthropic (Claude), OpenAI
1332
1976
  (GPT), and Google (Gemini) \u2014 text, image, and audio \u2014 plus a tool-use agent
1333
1977
  engine and an eval harness. Library-only: import in-process, bring your own keys.
1334
- Isomorphic (Node 20+, Cloudflare Workers, browser). Provider SDKs are lazy-loaded.
1978
+ Targets Node 20+ and Cloudflare Workers. Provider adapters are runtime-lazy, but
1979
+ all provider SDKs are package dependencies. Never expose long-lived keys in a browser.
1335
1980
 
1336
1981
  ## Install
1337
1982
 
1338
1983
  npm i @odla-ai/ai
1339
- # provider SDKs are peer-lazy-loaded; install those you use:
1340
- # @anthropic-ai/sdk openai @google/genai
1341
1984
  # optional, for odla-db-backed storage + secrets:
1342
1985
  # @odla-ai/db
1343
1986
 
@@ -1351,7 +1994,8 @@ Isomorphic (Node 20+, Cloudflare Workers, browser). Provider SDKs are lazy-loade
1351
1994
  - \`ai.stream(input)\` \u2192 async iterable of normalized events: message_start \u2192
1352
1995
  content_block_start \u2192 content_block_delta {text_delta|thinking_delta|input_json_delta}
1353
1996
  \u2192 content_block_stop \u2192 message_delta \u2192 message_stop (plus \`error\`).
1354
- - \`ai.extract<T>({ model, user, tool })\` \u2192 forced tool call; returns a parsed object.
1997
+ - \`ai.extract<T>({ model, user, tool })\` \u2192 forced tool call; returns an object
1998
+ validated against the declared JSON Schema. Unsupported assertions fail closed.
1355
1999
  - \`ai.search({ model, user })\` \u2192 web-search-grounded prose.
1356
2000
 
1357
2001
  ## Content blocks (multimodal)
@@ -1370,7 +2014,15 @@ audio block to a Claude model throws CapabilityError before any network call.
1370
2014
  // run.finalText, run.toolCalls, run.usage, run.steps, run.stoppedReason
1371
2015
 
1372
2016
  Tools may carry taint labels (outputTaint / acceptsTaint) for a CaMeL-style
1373
- prompt-injection gate. Personas may carry a MemoryScope.
2017
+ prompt-injection gate. Personas may carry a MemoryScope. Tool inputs are schema-
2018
+ validated before handlers run. Pass signal/deadline and a run budget
2019
+ ({maxInputTokens,maxOutputTokens,maxTotalTokens,maxToolCalls}) to bound follow-up
2020
+ effects and requested output. Input and total limits are post-turn ceilings:
2021
+ provider usage is unavailable before the first response, so they cannot
2022
+ tokenizer-bound the first prompt. Output and remaining-total limits cap the next
2023
+ request's maxTokens. Token limits must be positive integers; maxToolCalls may be
2024
+ zero. Budget exits pair every requested tool_use with an error tool_result before
2025
+ memory is persisted.
1374
2026
 
1375
2027
  ## Skills
1376
2028
 
@@ -1397,14 +2049,17 @@ device-authorization handshake for a scoped, revocable token, then provisions.
1397
2049
  import { requestToken, init as odlaInit } from "@odla-ai/db";
1398
2050
  import { provisionAgentApp, odlaDbKeyResolver, OdlaDbMemory, persistRun, init } from "@odla-ai/ai";
1399
2051
 
1400
- // 1. Handshake: the human supplies the endpoint and approves a short code in odla-db Studio.
1401
- const { token } = await requestToken({ endpoint: ODLA_URL, onCode: ({ userCode }) => show(userCode) });
2052
+ const ODLA_PLATFORM = "https://odla.ai";
2053
+ const ODLA_DB_URL = "https://db.odla.ai";
2054
+
2055
+ // 1. Handshake: the human approves a short code at odla.ai.
2056
+ const { token } = await requestToken({ endpoint: ODLA_PLATFORM, onCode: ({ userCode }) => show(userCode) });
1402
2057
 
1403
2058
  // 2. Provision: create app, mint app key, push agent schema, store BYO provider keys as secrets.
1404
- const { appId, appKey } = await provisionAgentApp({ endpoint: ODLA_URL, token, providerKeys: { openai: OPENAI_KEY } });
2059
+ const { appId, appKey } = await provisionAgentApp({ endpoint: ODLA_PLATFORM, token, providerKeys: { openai: OPENAI_KEY } });
1405
2060
 
1406
2061
  // 3. Runtime: read keys from odla-db secrets; persist memory + run traces.
1407
- const db = odlaInit({ appId, adminToken: appKey, endpoint: ODLA_URL });
2062
+ const db = odlaInit({ appId, adminToken: appKey, endpoint: ODLA_DB_URL });
1408
2063
  const ai = init({ resolveKey: odlaDbKeyResolver(db) });
1409
2064
  const persona = { name: "assistant", model: "gpt-5", memory: new OdlaDbMemory({ db, sessionId: "user-42" }) };
1410
2065
  const run = await runAgent(ai, persona, { input: "hello" });
@@ -1430,16 +2085,22 @@ env's odla-db tenant secrets, write-only for operators). One call reads both:
1430
2085
  platform: "https://odla.ai", appId: APP_ID, env: "prod", db });
1431
2086
  // ai.chat / ai.stream / ai.extract \u2014 provider/model from public-config
1432
2087
  // (cached ~60s, so Studio changes apply without redeploys); the key is
1433
- // fetched from the vault at call time via odlaDbKeyResolver.
2088
+ // fetched from the vault via a finite-TTL odlaDbKeyResolver.
1434
2089
 
1435
2090
  The only secret the Worker carries is its odla-db app key. Rotating the LLM
1436
- key = writing the secret again; switching provider/model = a Studio edit.
2091
+ key = writing the secret again; the default warm-isolate key TTL is 60 seconds
2092
+ and cacheVersion provides immediate invalidation. Provider SDK clients are
2093
+ fingerprinted, bounded to one generation per provider, and expire after 60
2094
+ seconds. The platform provider is enforced: models from other providers are
2095
+ absent from the facade catalog. If no default model is configured, each call
2096
+ must name a model for that provider. Only public config is shared; tenant-bound
2097
+ Ai facades are not cached globally.
1437
2098
 
1438
2099
  ## Errors
1439
2100
 
1440
2101
  Every error is an OdlaAIError subclass with a stable \`code\` \u2014 branch on err.code:
1441
2102
  config, auth, rate_limit, capability_unsupported, context_window, invalid_request,
1442
- tool_input_invalid, provider_error.
2103
+ tool_input_invalid, cancelled, deadline_exceeded, provider_error.
1443
2104
 
1444
2105
  ## Models
1445
2106
 
@@ -1451,10 +2112,11 @@ via buildCatalog / init({ catalog }).
1451
2112
  }
1452
2113
 
1453
2114
  // src/index.ts
1454
- init_errors();
2115
+ init_errors2();
1455
2116
 
1456
2117
  // src/agent/loop.ts
1457
2118
  init_shape();
2119
+ init_abort();
1458
2120
 
1459
2121
  // src/agent/tools.ts
1460
2122
  function toOracleTool(t) {
@@ -1496,6 +2158,7 @@ function assertSinkAcceptsTaint(sink, allowed, actual) {
1496
2158
 
1497
2159
  // src/agent/loop.ts
1498
2160
  async function runAgent(inference, persona, input) {
2161
+ validateRunLimits(persona.maxSteps, input.budget);
1499
2162
  const { system, tools } = composeSkills(persona.system, persona.tools, persona.skills);
1500
2163
  const handlerByName = /* @__PURE__ */ new Map();
1501
2164
  for (const t of tools) if (t.handler) handlerByName.set(t.name, t);
@@ -1507,21 +2170,26 @@ async function runAgent(inference, persona, input) {
1507
2170
  const toolCalls = [];
1508
2171
  const accumulatedTaint = /* @__PURE__ */ new Set(["llm_inherited"]);
1509
2172
  const maxSteps = persona.maxSteps ?? 8;
2173
+ const runSignal = signalWithDeadline(input.signal, input.deadline);
1510
2174
  let response;
1511
2175
  let stoppedReason = "max_steps";
1512
2176
  let steps = 0;
2177
+ let attemptedToolCalls = 0;
1513
2178
  while (steps < maxSteps) {
1514
2179
  steps++;
2180
+ const remainingOutput = remainingOutputTokens(usage, input.budget);
1515
2181
  response = await inference.chat({
1516
2182
  model: persona.model,
1517
2183
  system,
1518
2184
  messages,
1519
2185
  tools: tools.length > 0 ? tools.map(toOracleTool) : void 0,
1520
- maxTokens: persona.maxTokens ?? 4096,
2186
+ maxTokens: Math.min(persona.maxTokens ?? 4096, remainingOutput ?? Number.POSITIVE_INFINITY),
1521
2187
  effort: persona.effort,
1522
2188
  thinking: persona.thinking,
1523
2189
  temperature: persona.temperature,
1524
- webSearch: persona.webSearch
2190
+ webSearch: persona.webSearch,
2191
+ signal: runSignal,
2192
+ deadline: input.deadline
1525
2193
  });
1526
2194
  addUsage(usage, response.usage);
1527
2195
  messages.push({ role: "assistant", content: response.content });
@@ -1534,8 +2202,22 @@ async function runAgent(inference, persona, input) {
1534
2202
  stoppedReason = "end_turn";
1535
2203
  break;
1536
2204
  }
2205
+ if (budgetExceeded(usage, input.budget)) {
2206
+ stoppedReason = "budget_exhausted";
2207
+ messages.push({
2208
+ role: "user",
2209
+ content: toolUses.map((toolUse) => errorResult(toolUse.id, "Run budget exhausted before tool execution."))
2210
+ });
2211
+ break;
2212
+ }
1537
2213
  const resultBlocks = [];
1538
2214
  for (const toolUse of toolUses) {
2215
+ if (input.budget?.maxToolCalls !== void 0 && attemptedToolCalls >= input.budget.maxToolCalls) {
2216
+ stoppedReason = "budget_exhausted";
2217
+ resultBlocks.push(errorResult(toolUse.id, "Run tool-call budget exhausted before execution."));
2218
+ continue;
2219
+ }
2220
+ attemptedToolCalls++;
1539
2221
  const def = handlerByName.get(toolUse.name);
1540
2222
  if (!def || !def.handler) {
1541
2223
  resultBlocks.push(errorResult(toolUse.id, `No local handler for tool "${toolUse.name}".`));
@@ -1543,16 +2225,26 @@ async function runAgent(inference, persona, input) {
1543
2225
  }
1544
2226
  if (def.acceptsTaint) assertSinkAcceptsTaint(def.name, def.acceptsTaint, accumulatedTaint);
1545
2227
  let output;
1546
- try {
1547
- output = await def.handler(toolUse.input, { taint: new Set(accumulatedTaint), signal: input.signal });
1548
- } catch (err) {
1549
- output = { content: `Tool "${toolUse.name}" threw: ${err.message}`, isError: true };
2228
+ const validation = validateJsonSchema(def.inputSchema, toolUse.input);
2229
+ if (!validation.valid) {
2230
+ const detail = validation.issues.slice(0, 5).map((i) => `${i.path} ${i.message}`).join("; ");
2231
+ output = { content: `Tool "${toolUse.name}" input is invalid: ${detail}`, isError: true };
2232
+ } else {
2233
+ throwIfAborted(runSignal);
2234
+ try {
2235
+ output = await def.handler(toolUse.input, { taint: new Set(accumulatedTaint), signal: runSignal });
2236
+ } catch (error) {
2237
+ if (error instanceof CancelledError || error instanceof DeadlineExceededError) throw error;
2238
+ throwIfAborted(runSignal);
2239
+ output = { content: `Tool "${toolUse.name}" failed.`, isError: true };
2240
+ }
1550
2241
  }
1551
2242
  toolCalls.push({ toolUse, output });
1552
2243
  resultBlocks.push({ type: "tool_result", toolUseId: toolUse.id, content: output.content, isError: output.isError });
1553
2244
  for (const label of def.outputTaint ?? []) accumulatedTaint.add(label);
1554
2245
  }
1555
2246
  messages.push({ role: "user", content: resultBlocks });
2247
+ if (stoppedReason === "budget_exhausted") break;
1556
2248
  }
1557
2249
  if (!response) throw new Error("runAgent: maxSteps must be at least 1");
1558
2250
  if (persona.memory) await persona.memory.append(messages.slice(runStartIndex));
@@ -1566,6 +2258,36 @@ async function runAgent(inference, persona, input) {
1566
2258
  stoppedReason
1567
2259
  };
1568
2260
  }
2261
+ function budgetExceeded(usage, budget) {
2262
+ if (!budget) return false;
2263
+ if (budget.maxInputTokens !== void 0 && usage.inputTokens >= budget.maxInputTokens) return true;
2264
+ if (budget.maxOutputTokens !== void 0 && usage.outputTokens >= budget.maxOutputTokens) return true;
2265
+ if (budget.maxTotalTokens !== void 0 && usage.inputTokens + usage.outputTokens >= budget.maxTotalTokens) return true;
2266
+ return false;
2267
+ }
2268
+ function remainingOutputTokens(usage, budget) {
2269
+ if (!budget) return void 0;
2270
+ const remaining = [
2271
+ budget.maxOutputTokens === void 0 ? void 0 : budget.maxOutputTokens - usage.outputTokens,
2272
+ budget.maxTotalTokens === void 0 ? void 0 : budget.maxTotalTokens - usage.inputTokens - usage.outputTokens
2273
+ ].filter((value) => value !== void 0);
2274
+ return remaining.length === 0 ? void 0 : Math.max(1, Math.min(...remaining));
2275
+ }
2276
+ function validateRunLimits(maxSteps, budget) {
2277
+ if (maxSteps !== void 0 && (!Number.isInteger(maxSteps) || maxSteps < 1)) {
2278
+ throw new ConfigError("persona.maxSteps must be a positive integer.");
2279
+ }
2280
+ if (!budget) return;
2281
+ for (const key of ["maxInputTokens", "maxOutputTokens", "maxTotalTokens"]) {
2282
+ const value = budget[key];
2283
+ if (value !== void 0 && (!Number.isInteger(value) || value < 1)) {
2284
+ throw new ConfigError(`budget.${key} must be a positive integer.`);
2285
+ }
2286
+ }
2287
+ if (budget.maxToolCalls !== void 0 && (!Number.isInteger(budget.maxToolCalls) || budget.maxToolCalls < 0)) {
2288
+ throw new ConfigError("budget.maxToolCalls must be a non-negative integer.");
2289
+ }
2290
+ }
1569
2291
  function errorResult(toolUseId, message) {
1570
2292
  return { type: "tool_result", toolUseId, content: message, isError: true };
1571
2293
  }
@@ -1870,22 +2592,32 @@ var DEFAULT_SECRET_NAMES = {
1870
2592
  };
1871
2593
  function odlaDbKeyResolver(db, opts = {}) {
1872
2594
  const nameFor = opts.secretName ?? ((p) => DEFAULT_SECRET_NAMES[p]);
1873
- const useCache = opts.cache ?? true;
2595
+ const ttlMs = opts.cache === false ? 0 : Math.max(0, opts.ttlMs ?? 6e4);
1874
2596
  const cache3 = /* @__PURE__ */ new Map();
1875
2597
  return async (provider) => {
1876
- if (useCache) {
2598
+ const version = typeof opts.cacheVersion === "function" ? opts.cacheVersion(provider) : opts.cacheVersion ?? "";
2599
+ if (ttlMs > 0) {
1877
2600
  const hit = cache3.get(provider);
1878
- if (hit !== void 0) return hit;
2601
+ if (hit?.version === version && hit.expiresAt > Date.now()) return hit.value;
2602
+ cache3.delete(provider);
1879
2603
  }
1880
2604
  try {
1881
2605
  const key = await db.secrets.get(nameFor(provider));
1882
- if (useCache) cache3.set(provider, key);
2606
+ if (ttlMs > 0) cache3.set(provider, { version, value: key, expiresAt: Date.now() + ttlMs });
1883
2607
  return key;
1884
- } catch {
1885
- return void 0;
2608
+ } catch (error) {
2609
+ if (isMissingSecret(error)) return void 0;
2610
+ throw error;
1886
2611
  }
1887
2612
  };
1888
2613
  }
2614
+ function isMissingSecret(error) {
2615
+ if (!error || typeof error !== "object") return false;
2616
+ const rec = error;
2617
+ if (rec.status === 404 || rec.statusCode === 404 || rec.code === "not_found") return true;
2618
+ const message = error instanceof Error ? error.message : "";
2619
+ return /\b404\b/.test(message);
2620
+ }
1889
2621
 
1890
2622
  // src/store/platform.ts
1891
2623
  init_shape();
@@ -1917,19 +2649,22 @@ async function initFromPlatform(opts) {
1917
2649
  const key = `${opts.platform}|${opts.appId}|${opts.env}`;
1918
2650
  const now = Date.now();
1919
2651
  const hit = ttl > 0 ? cache2.get(key) : void 0;
1920
- if (hit && hit.expiresAt > now) return hit.value;
1921
- const { provider, model } = await fetchAiConfig(opts);
1922
- if (hit && hit.value.provider === provider && hit.value.model === model) {
1923
- hit.expiresAt = now + ttl;
1924
- return hit.value;
1925
- }
2652
+ const config = hit && hit.expiresAt > now ? hit.value : await fetchAiConfig(opts);
2653
+ const { provider, model } = config;
2654
+ if (ttl > 0 && (!hit || hit.expiresAt <= now)) cache2.set(key, { value: config, expiresAt: now + ttl });
2655
+ const sourceCatalog = opts.initOptions?.catalog ?? DEFAULT_CATALOG;
2656
+ const catalog = Object.fromEntries(
2657
+ Object.entries(sourceCatalog).filter(([, spec]) => spec.provider === provider)
2658
+ );
2659
+ const defaultModel = model ?? opts.initOptions?.defaultModel;
2660
+ if (defaultModel) resolveModel(catalog, defaultModel);
1926
2661
  const ai = init({
1927
2662
  ...opts.initOptions,
1928
- resolveKey: odlaDbKeyResolver(opts.db),
1929
- defaultModel: model ?? opts.initOptions?.defaultModel
2663
+ catalog,
2664
+ resolveKey: odlaDbKeyResolver(opts.db, opts.keyResolverOptions),
2665
+ defaultModel
1930
2666
  });
1931
2667
  const value = { ai, provider, ...model ? { model } : {} };
1932
- if (ttl > 0) cache2.set(key, { value, expiresAt: now + ttl });
1933
2668
  return value;
1934
2669
  }
1935
2670