@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.js CHANGED
@@ -1,29 +1,410 @@
1
1
  import {
2
2
  AuthError,
3
+ CancelledError,
3
4
  CapabilityError,
4
5
  ConfigError,
5
6
  ContextWindowError,
7
+ DeadlineExceededError,
6
8
  InvalidRequestError,
7
9
  OdlaAIError,
8
10
  ProviderError,
9
11
  RateLimitError,
12
+ ToolInputError,
10
13
  addUsage,
11
- blocksOf,
12
14
  emptyUsage,
13
- extractText,
14
- extractToolUses,
15
- isAudioBlock,
16
- isDocumentBlock,
17
- isImageBlock,
18
- isTextBlock,
19
- isThinkingBlock,
20
- isToolResultBlock,
21
- isToolUseBlock,
22
- normalizeError
23
- } from "./chunk-LANGYP65.js";
15
+ normalizeError,
16
+ signalWithDeadline,
17
+ throwIfAborted
18
+ } from "./chunk-PXXCN2EU.js";
19
+
20
+ // src/shape/helpers.ts
21
+ function isTextBlock(b) {
22
+ return b.type === "text";
23
+ }
24
+ function isImageBlock(b) {
25
+ return b.type === "image";
26
+ }
27
+ function isAudioBlock(b) {
28
+ return b.type === "audio";
29
+ }
30
+ function isDocumentBlock(b) {
31
+ return b.type === "document";
32
+ }
33
+ function isToolUseBlock(b) {
34
+ return b.type === "tool_use";
35
+ }
36
+ function isToolResultBlock(b) {
37
+ return b.type === "tool_result";
38
+ }
39
+ function isThinkingBlock(b) {
40
+ return b.type === "thinking";
41
+ }
42
+ function blocksOf(content) {
43
+ return typeof content === "string" ? [{ type: "text", text: content }] : content;
44
+ }
45
+ function extractText(content) {
46
+ return content.filter(isTextBlock).map((b) => b.text).join("");
47
+ }
48
+ function extractToolUses(content) {
49
+ return content.filter(isToolUseBlock);
50
+ }
51
+
52
+ // src/shape/json-schema-shape.ts
53
+ var ANNOTATION_KEYS = /* @__PURE__ */ new Set([
54
+ "$schema",
55
+ "$id",
56
+ "$anchor",
57
+ "title",
58
+ "description",
59
+ "default",
60
+ "examples",
61
+ "deprecated",
62
+ "readOnly",
63
+ "writeOnly",
64
+ "format"
65
+ ]);
66
+ var SUPPORTED_KEYS = /* @__PURE__ */ new Set([
67
+ ...ANNOTATION_KEYS,
68
+ "$ref",
69
+ "$defs",
70
+ "definitions",
71
+ "type",
72
+ "enum",
73
+ "const",
74
+ "allOf",
75
+ "anyOf",
76
+ "oneOf",
77
+ "not",
78
+ "properties",
79
+ "required",
80
+ "additionalProperties",
81
+ "minProperties",
82
+ "maxProperties",
83
+ "items",
84
+ "minItems",
85
+ "maxItems",
86
+ "uniqueItems",
87
+ "minLength",
88
+ "maxLength",
89
+ "pattern",
90
+ "minimum",
91
+ "maximum",
92
+ "exclusiveMinimum",
93
+ "exclusiveMaximum",
94
+ "multipleOf"
95
+ ]);
96
+ var ALLOWED_TYPES = /* @__PURE__ */ new Set([
97
+ "null",
98
+ "boolean",
99
+ "object",
100
+ "array",
101
+ "number",
102
+ "integer",
103
+ "string"
104
+ ]);
105
+ function validateSchemaShape(schema, path, root, issues) {
106
+ if (typeof schema === "boolean") return;
107
+ if (!isRecord(schema)) {
108
+ issue(issues, path, "schema must be an object or boolean");
109
+ return;
110
+ }
111
+ for (const key of Object.keys(schema)) {
112
+ if (!SUPPORTED_KEYS.has(key)) issue(issues, path, `schema uses unsupported keyword "${key}"`);
113
+ }
114
+ if (typeof schema.$ref === "string" && resolveLocalRef(root, schema.$ref) === void 0) {
115
+ issue(issues, path, `schema reference "${schema.$ref}" could not be resolved`);
116
+ } else if (schema.$ref !== void 0 && typeof schema.$ref !== "string") {
117
+ issue(issues, path, "schema $ref must be a string");
118
+ }
119
+ const types = typeof schema.type === "string" ? [schema.type] : Array.isArray(schema.type) ? schema.type : [];
120
+ if (schema.type !== void 0 && (types.length === 0 || !types.every((type) => typeof type === "string" && ALLOWED_TYPES.has(type)))) {
121
+ issue(issues, path, "schema type contains an unsupported JSON type");
122
+ }
123
+ if (schema.enum !== void 0 && !Array.isArray(schema.enum)) {
124
+ issue(issues, path, "schema enum must be an array");
125
+ }
126
+ if (schema.required !== void 0 && (!Array.isArray(schema.required) || !schema.required.every((key) => typeof key === "string"))) {
127
+ issue(issues, path, "schema required must be a string array");
128
+ }
129
+ validateConstraintShapes(schema, path, issues);
130
+ validateChildSchemas(schema, path, root, issues);
131
+ }
132
+ function validateChildSchemas(schema, path, root, issues) {
133
+ for (const key of ["properties", "$defs", "definitions"]) {
134
+ const group = schema[key];
135
+ if (group === void 0) continue;
136
+ if (!isRecord(group)) {
137
+ issue(issues, path, `schema ${key} must be an object`);
138
+ continue;
139
+ }
140
+ for (const [name, child] of Object.entries(group)) {
141
+ validateSchemaShape(child, `${path}.${key}.${name}`, root, issues);
142
+ }
143
+ }
144
+ for (const key of ["allOf", "anyOf", "oneOf"]) {
145
+ const group = schema[key];
146
+ if (group === void 0) continue;
147
+ if (!Array.isArray(group) || group.length === 0) {
148
+ issue(issues, path, `schema ${key} must be a non-empty array`);
149
+ continue;
150
+ }
151
+ group.forEach((child, index2) => validateSchemaShape(child, `${path}.${key}[${index2}]`, root, issues));
152
+ }
153
+ for (const key of ["items", "not", "additionalProperties"]) {
154
+ const child = schema[key];
155
+ if (child !== void 0) validateSchemaShape(child, `${path}.${key}`, root, issues);
156
+ }
157
+ }
158
+ function validateConstraintShapes(schema, path, issues) {
159
+ for (const key of ["minLength", "maxLength", "minItems", "maxItems", "minProperties", "maxProperties"]) {
160
+ const value = schema[key];
161
+ if (value !== void 0 && (!Number.isInteger(value) || value < 0)) {
162
+ issue(issues, path, `schema ${key} must be a non-negative integer`);
163
+ }
164
+ }
165
+ for (const key of ["minimum", "maximum", "exclusiveMinimum", "exclusiveMaximum"]) {
166
+ const value = schema[key];
167
+ if (value !== void 0 && !isFiniteNumber(value)) {
168
+ issue(issues, path, `schema ${key} must be a finite number`);
169
+ }
170
+ }
171
+ if (schema.multipleOf !== void 0 && (!isFiniteNumber(schema.multipleOf) || schema.multipleOf <= 0)) {
172
+ issue(issues, path, "schema multipleOf must be a finite number greater than zero");
173
+ }
174
+ if (schema.uniqueItems !== void 0 && typeof schema.uniqueItems !== "boolean") {
175
+ issue(issues, path, "schema uniqueItems must be a boolean");
176
+ }
177
+ validatePatternShape(schema.pattern, path, issues);
178
+ }
179
+ function validatePatternShape(value, path, issues) {
180
+ if (value === void 0) return;
181
+ if (typeof value !== "string") {
182
+ issue(issues, path, "schema pattern must be a string");
183
+ return;
184
+ }
185
+ try {
186
+ new RegExp(value, "u");
187
+ } catch {
188
+ issue(issues, path, `schema pattern "${value}" is invalid`);
189
+ }
190
+ }
191
+ function resolveLocalRef(root, ref) {
192
+ if (ref === "#") return root;
193
+ if (!ref.startsWith("#/")) return void 0;
194
+ let current = root;
195
+ for (const raw of ref.slice(2).split("/")) {
196
+ const key = raw.replace(/~1/g, "/").replace(/~0/g, "~");
197
+ if (!isRecord(current) || !Object.hasOwn(current, key)) return void 0;
198
+ current = current[key];
199
+ }
200
+ return current;
201
+ }
202
+ function isRecord(value) {
203
+ return value !== null && typeof value === "object" && !Array.isArray(value);
204
+ }
205
+ function isFiniteNumber(value) {
206
+ return typeof value === "number" && Number.isFinite(value);
207
+ }
208
+ function issue(issues, path, message) {
209
+ issues.push({ path, message });
210
+ }
211
+
212
+ // src/shape/json-schema.ts
213
+ function validateJsonSchema(schema, value) {
214
+ const issues = [];
215
+ validateSchemaShape(schema, "$", schema, issues);
216
+ if (issues.length > 0) return { valid: false, issues };
217
+ visit(schema, value, "$", schema, issues, /* @__PURE__ */ new Set());
218
+ return { valid: issues.length === 0, issues };
219
+ }
220
+ function visit(schema, value, path, root, issues, refs) {
221
+ if (schema === true) return;
222
+ if (schema === false) {
223
+ issue2(issues, path, "is rejected by the schema");
224
+ return;
225
+ }
226
+ if (!isRecord2(schema)) {
227
+ issue2(issues, path, "schema must be an object or boolean");
228
+ return;
229
+ }
230
+ for (const key of Object.keys(schema)) {
231
+ if (!SUPPORTED_KEYS.has(key)) {
232
+ issue2(issues, path, `schema uses unsupported keyword "${key}"`);
233
+ return;
234
+ }
235
+ }
236
+ if (typeof schema.$ref === "string") {
237
+ const target = resolveLocalRef2(root, schema.$ref);
238
+ if (target === void 0) {
239
+ issue2(issues, path, `schema reference "${schema.$ref}" could not be resolved`);
240
+ return;
241
+ }
242
+ if (refs.has(schema.$ref)) {
243
+ issue2(issues, path, `cyclic schema reference "${schema.$ref}" is unsupported`);
244
+ return;
245
+ }
246
+ const nextRefs = new Set(refs);
247
+ nextRefs.add(schema.$ref);
248
+ visit(target, value, path, root, issues, nextRefs);
249
+ }
250
+ if (Array.isArray(schema.enum) && !schema.enum.some((candidate) => jsonEqual(candidate, value))) {
251
+ issue2(issues, path, "must equal one of the allowed enum values");
252
+ }
253
+ if (Object.hasOwn(schema, "const") && !jsonEqual(schema.const, value)) {
254
+ issue2(issues, path, "must equal the constant value");
255
+ }
256
+ if (Array.isArray(schema.allOf)) {
257
+ for (const child of schema.allOf) visit(child, value, path, root, issues, refs);
258
+ }
259
+ if (Array.isArray(schema.anyOf) && !schema.anyOf.some((child) => matches(child, value, root, refs))) {
260
+ issue2(issues, path, "must match at least one anyOf schema");
261
+ }
262
+ if (Array.isArray(schema.oneOf)) {
263
+ const matchesCount = schema.oneOf.filter((child) => matches(child, value, root, refs)).length;
264
+ if (matchesCount !== 1) issue2(issues, path, "must match exactly one oneOf schema");
265
+ }
266
+ if (schema.not !== void 0 && matches(schema.not, value, root, refs)) {
267
+ issue2(issues, path, "must not match the forbidden schema");
268
+ }
269
+ const types = typeof schema.type === "string" ? [schema.type] : Array.isArray(schema.type) && schema.type.every((t) => typeof t === "string") ? schema.type : void 0;
270
+ if (schema.type !== void 0 && !types) {
271
+ issue2(issues, path, "schema type must be a string or string array");
272
+ return;
273
+ }
274
+ if (types && !types.some((type) => isType(value, type))) {
275
+ issue2(issues, path, `must be ${types.join(" or ")}`);
276
+ return;
277
+ }
278
+ if (typeof value === "string") validateString(schema, value, path, issues);
279
+ if (typeof value === "number") validateNumber(schema, value, path, issues);
280
+ if (Array.isArray(value)) validateArray(schema, value, path, root, issues, refs);
281
+ if (isRecord2(value)) validateObject(schema, value, path, root, issues, refs);
282
+ }
283
+ function validateString(schema, value, path, issues) {
284
+ if (isFiniteNumber2(schema.minLength) && [...value].length < schema.minLength) issue2(issues, path, `must have at least ${schema.minLength} characters`);
285
+ if (isFiniteNumber2(schema.maxLength) && [...value].length > schema.maxLength) issue2(issues, path, `must have at most ${schema.maxLength} characters`);
286
+ if (typeof schema.pattern === "string") {
287
+ try {
288
+ if (!new RegExp(schema.pattern, "u").test(value)) issue2(issues, path, `must match pattern ${schema.pattern}`);
289
+ } catch {
290
+ issue2(issues, path, `schema pattern "${schema.pattern}" is invalid`);
291
+ }
292
+ }
293
+ }
294
+ function validateNumber(schema, value, path, issues) {
295
+ if (!Number.isFinite(value)) {
296
+ issue2(issues, path, "must be a finite number");
297
+ return;
298
+ }
299
+ if (isFiniteNumber2(schema.minimum) && value < schema.minimum) issue2(issues, path, `must be >= ${schema.minimum}`);
300
+ if (isFiniteNumber2(schema.maximum) && value > schema.maximum) issue2(issues, path, `must be <= ${schema.maximum}`);
301
+ if (isFiniteNumber2(schema.exclusiveMinimum) && value <= schema.exclusiveMinimum) issue2(issues, path, `must be > ${schema.exclusiveMinimum}`);
302
+ if (isFiniteNumber2(schema.exclusiveMaximum) && value >= schema.exclusiveMaximum) issue2(issues, path, `must be < ${schema.exclusiveMaximum}`);
303
+ if (isFiniteNumber2(schema.multipleOf) && schema.multipleOf > 0) {
304
+ const quotient = value / schema.multipleOf;
305
+ if (Math.abs(quotient - Math.round(quotient)) > Number.EPSILON * Math.max(1, Math.abs(quotient))) {
306
+ issue2(issues, path, `must be a multiple of ${schema.multipleOf}`);
307
+ }
308
+ }
309
+ }
310
+ function validateArray(schema, value, path, root, issues, refs) {
311
+ if (isFiniteNumber2(schema.minItems) && value.length < schema.minItems) issue2(issues, path, `must contain at least ${schema.minItems} items`);
312
+ if (isFiniteNumber2(schema.maxItems) && value.length > schema.maxItems) issue2(issues, path, `must contain at most ${schema.maxItems} items`);
313
+ if (schema.uniqueItems === true) {
314
+ for (let i = 0; i < value.length; i++) {
315
+ if (value.slice(0, i).some((other) => jsonEqual(other, value[i]))) {
316
+ issue2(issues, `${path}[${i}]`, "must be unique");
317
+ break;
318
+ }
319
+ }
320
+ }
321
+ if (schema.items !== void 0) {
322
+ for (let i = 0; i < value.length; i++) visit(schema.items, value[i], `${path}[${i}]`, root, issues, refs);
323
+ }
324
+ }
325
+ function validateObject(schema, value, path, root, issues, refs) {
326
+ const keys = Object.keys(value);
327
+ if (isFiniteNumber2(schema.minProperties) && keys.length < schema.minProperties) issue2(issues, path, `must contain at least ${schema.minProperties} properties`);
328
+ if (isFiniteNumber2(schema.maxProperties) && keys.length > schema.maxProperties) issue2(issues, path, `must contain at most ${schema.maxProperties} properties`);
329
+ if (schema.required !== void 0 && (!Array.isArray(schema.required) || !schema.required.every((key) => typeof key === "string"))) {
330
+ issue2(issues, path, "schema required must be a string array");
331
+ return;
332
+ }
333
+ for (const key of schema.required ?? []) {
334
+ if (!Object.hasOwn(value, key)) issue2(issues, childPath(path, key), "is required");
335
+ }
336
+ const properties = isRecord2(schema.properties) ? schema.properties : {};
337
+ for (const [key, child] of Object.entries(properties)) {
338
+ if (Object.hasOwn(value, key)) visit(child, value[key], childPath(path, key), root, issues, refs);
339
+ }
340
+ const extras = keys.filter((key) => !Object.hasOwn(properties, key));
341
+ if (schema.additionalProperties === false) {
342
+ for (const key of extras) issue2(issues, childPath(path, key), "is not an allowed property");
343
+ } else if (schema.additionalProperties !== void 0 && schema.additionalProperties !== true) {
344
+ for (const key of extras) visit(schema.additionalProperties, value[key], childPath(path, key), root, issues, refs);
345
+ }
346
+ }
347
+ function matches(schema, value, root, refs) {
348
+ const nested = [];
349
+ visit(schema, value, "$", root, nested, refs);
350
+ return nested.length === 0;
351
+ }
352
+ function resolveLocalRef2(root, ref) {
353
+ if (ref === "#") return root;
354
+ if (!ref.startsWith("#/")) return void 0;
355
+ let current = root;
356
+ for (const raw of ref.slice(2).split("/")) {
357
+ const key = raw.replace(/~1/g, "/").replace(/~0/g, "~");
358
+ if (!isRecord2(current) || !Object.hasOwn(current, key)) return void 0;
359
+ current = current[key];
360
+ }
361
+ return current;
362
+ }
363
+ function isRecord2(value) {
364
+ return value !== null && typeof value === "object" && !Array.isArray(value);
365
+ }
366
+ function isType(value, type) {
367
+ switch (type) {
368
+ case "null":
369
+ return value === null;
370
+ case "boolean":
371
+ return typeof value === "boolean";
372
+ case "object":
373
+ return isRecord2(value);
374
+ case "array":
375
+ return Array.isArray(value);
376
+ case "number":
377
+ return typeof value === "number" && Number.isFinite(value);
378
+ case "integer":
379
+ return typeof value === "number" && Number.isInteger(value);
380
+ case "string":
381
+ return typeof value === "string";
382
+ default:
383
+ return false;
384
+ }
385
+ }
386
+ function isFiniteNumber2(value) {
387
+ return typeof value === "number" && Number.isFinite(value);
388
+ }
389
+ function jsonEqual(a, b) {
390
+ if (Object.is(a, b)) return true;
391
+ if (Array.isArray(a) && Array.isArray(b)) return a.length === b.length && a.every((v, i) => jsonEqual(v, b[i]));
392
+ if (isRecord2(a) && isRecord2(b)) {
393
+ const aKeys = Object.keys(a);
394
+ const bKeys = Object.keys(b);
395
+ return aKeys.length === bKeys.length && aKeys.every((key) => Object.hasOwn(b, key) && jsonEqual(a[key], b[key]));
396
+ }
397
+ return false;
398
+ }
399
+ function childPath(path, key) {
400
+ return /^[A-Za-z_$][\w$]*$/.test(key) ? `${path}.${key}` : `${path}[${JSON.stringify(key)}]`;
401
+ }
402
+ function issue2(issues, path, message) {
403
+ issues.push({ path, message });
404
+ }
24
405
 
25
406
  // src/shape/capabilities.ts
26
- function chatCapabilities(overrides = {}) {
407
+ function chatCapabilities(overrides2 = {}) {
27
408
  return {
28
409
  kind: "chat",
29
410
  textIn: true,
@@ -36,7 +417,7 @@ function chatCapabilities(overrides = {}) {
36
417
  streaming: true,
37
418
  structuredOutput: true,
38
419
  webSearch: false,
39
- ...overrides
420
+ ...overrides2
40
421
  };
41
422
  }
42
423
  function validateRequest(spec, req) {
@@ -125,6 +506,15 @@ var OPENAI_MODELS = [
125
506
  capabilities: chatCapabilities({ effort: true }),
126
507
  contextWindow: 4e5
127
508
  },
509
+ {
510
+ // Compact GPT-5.4 reasoning model (effort knob), same 400K context as the
511
+ // rest of the GPT-5 line. The default for odla-o11y AI triage.
512
+ id: "gpt-5.4-mini",
513
+ provider: "openai",
514
+ nativeId: "gpt-5.4-mini",
515
+ capabilities: chatCapabilities({ effort: true }),
516
+ contextWindow: 4e5
517
+ },
128
518
  {
129
519
  id: "gpt-5",
130
520
  provider: "openai",
@@ -242,37 +632,60 @@ function providersInCatalog(catalog) {
242
632
  }
243
633
 
244
634
  // src/providers/registry.ts
635
+ var CLIENT_TTL_MS = 6e4;
245
636
  var cache = /* @__PURE__ */ new Map();
637
+ var inflight = /* @__PURE__ */ new Map();
638
+ var overrides = /* @__PURE__ */ new Map();
246
639
  async function getProvider(id, apiKey) {
247
- const cacheKey = `${id}:${apiKey}`;
248
- const existing = cache.get(cacheKey);
249
- if (existing) return existing;
640
+ const override = overrides.get(id);
641
+ if (override?.apiKey === apiKey) return override.provider;
642
+ const fingerprint = await fingerprintKey(apiKey);
643
+ const existing = cache.get(id);
644
+ if (existing?.fingerprint === fingerprint && existing.expiresAt > Date.now()) return existing.provider;
645
+ if (existing) cache.delete(id);
646
+ const pending = inflight.get(id);
647
+ if (pending?.fingerprint === fingerprint) return pending.promise;
648
+ const promise = createProvider(id, apiKey).then((provider) => {
649
+ cache.set(id, { fingerprint, provider, expiresAt: Date.now() + CLIENT_TTL_MS });
650
+ return provider;
651
+ }).finally(() => {
652
+ if (inflight.get(id)?.promise === promise) inflight.delete(id);
653
+ });
654
+ inflight.set(id, { fingerprint, promise });
655
+ return promise;
656
+ }
657
+ async function createProvider(id, apiKey) {
250
658
  let provider;
251
659
  switch (id) {
252
660
  case "anthropic": {
253
- const { AnthropicProvider } = await import("./anthropic-JXDXR7O7.js");
661
+ const { AnthropicProvider } = await import("./anthropic-2NPMHJZT.js");
254
662
  provider = new AnthropicProvider(apiKey);
255
663
  break;
256
664
  }
257
665
  case "openai": {
258
- const { OpenAIProvider } = await import("./openai-XZRXMKCC.js");
666
+ const { OpenAIProvider } = await import("./openai-NTQCF577.js");
259
667
  provider = new OpenAIProvider(apiKey);
260
668
  break;
261
669
  }
262
670
  case "google": {
263
- const { GoogleProvider } = await import("./google-3TFOFIQO.js");
671
+ const { GoogleProvider } = await import("./google-3XV2VWVB.js");
264
672
  provider = new GoogleProvider(apiKey);
265
673
  break;
266
674
  }
267
675
  }
268
- cache.set(cacheKey, provider);
269
676
  return provider;
270
677
  }
678
+ async function fingerprintKey(apiKey) {
679
+ const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(apiKey));
680
+ return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
681
+ }
271
682
  function registerProvider(id, apiKey, provider) {
272
- cache.set(`${id}:${apiKey}`, provider);
683
+ overrides.set(id, { apiKey, provider });
273
684
  }
274
685
  function clearProviderCache() {
275
686
  cache.clear();
687
+ inflight.clear();
688
+ overrides.clear();
276
689
  }
277
690
 
278
691
  // src/client/keys.ts
@@ -342,7 +755,9 @@ function init(opts = {}) {
342
755
  messages: [{ role: "user", content: c.user }],
343
756
  tools: [{ name: c.tool.name, description: c.tool.description ?? "", inputSchema: c.tool.parameters }],
344
757
  toolChoice: { type: "tool", name: c.tool.name },
345
- maxTokens: c.maxTokens ?? 4096
758
+ maxTokens: c.maxTokens ?? 4096,
759
+ signal: c.signal,
760
+ deadline: c.deadline
346
761
  };
347
762
  const { spec, provider } = await resolveProviderFor(model);
348
763
  validateRequest(spec, req);
@@ -351,6 +766,11 @@ function init(opts = {}) {
351
766
  if (!block || block.type !== "tool_use") {
352
767
  throw new ProviderError(`Model "${model}" did not return the forced tool call "${c.tool.name}".`);
353
768
  }
769
+ const validation = validateJsonSchema(c.tool.parameters, block.input);
770
+ if (!validation.valid) {
771
+ const detail = validation.issues.slice(0, 5).map((i) => `${i.path} ${i.message}`).join("; ");
772
+ throw new ToolInputError(`Tool "${c.tool.name}" returned invalid input: ${detail}`, { tool: c.tool.name });
773
+ }
354
774
  return { value: block.input, usage: res.usage };
355
775
  };
356
776
  const search = async (c) => {
@@ -360,7 +780,9 @@ function init(opts = {}) {
360
780
  system: c.system,
361
781
  messages: [{ role: "user", content: c.user }],
362
782
  webSearch: true,
363
- maxTokens: c.maxTokens ?? 8192
783
+ maxTokens: c.maxTokens ?? 8192,
784
+ signal: c.signal,
785
+ deadline: c.deadline
364
786
  };
365
787
  const { spec, provider } = await resolveProviderFor(model);
366
788
  validateRequest(spec, req);
@@ -386,16 +808,20 @@ function generateLlmsTxt(catalog = DEFAULT_CATALOG) {
386
808
  const models = Object.values(catalog).map((s) => `- \`${s.id}\` (${s.provider}) \u2014 ${capsSummary(s.capabilities)}`).join("\n");
387
809
  return `# @odla-ai/ai \u2014 LLM context
388
810
 
811
+ > EARLY ACCESS \u2014 pre-1.0. Agents work from bounded runbooks; humans approve
812
+ > credentials, production changes, releases, and merges. APIs and exact package
813
+ > availability can change. Review documented guarantees and limitations; this
814
+ > software is MIT-licensed and provided without warranty.
815
+
389
816
  One general-purpose interface for AI inference across Anthropic (Claude), OpenAI
390
817
  (GPT), and Google (Gemini) \u2014 text, image, and audio \u2014 plus a tool-use agent
391
818
  engine and an eval harness. Library-only: import in-process, bring your own keys.
392
- Isomorphic (Node 20+, Cloudflare Workers, browser). Provider SDKs are lazy-loaded.
819
+ Targets Node 20+ and Cloudflare Workers. Provider adapters are runtime-lazy, but
820
+ all provider SDKs are package dependencies. Never expose long-lived keys in a browser.
393
821
 
394
822
  ## Install
395
823
 
396
824
  npm i @odla-ai/ai
397
- # provider SDKs are peer-lazy-loaded; install those you use:
398
- # @anthropic-ai/sdk openai @google/genai
399
825
  # optional, for odla-db-backed storage + secrets:
400
826
  # @odla-ai/db
401
827
 
@@ -409,7 +835,8 @@ Isomorphic (Node 20+, Cloudflare Workers, browser). Provider SDKs are lazy-loade
409
835
  - \`ai.stream(input)\` \u2192 async iterable of normalized events: message_start \u2192
410
836
  content_block_start \u2192 content_block_delta {text_delta|thinking_delta|input_json_delta}
411
837
  \u2192 content_block_stop \u2192 message_delta \u2192 message_stop (plus \`error\`).
412
- - \`ai.extract<T>({ model, user, tool })\` \u2192 forced tool call; returns a parsed object.
838
+ - \`ai.extract<T>({ model, user, tool })\` \u2192 forced tool call; returns an object
839
+ validated against the declared JSON Schema. Unsupported assertions fail closed.
413
840
  - \`ai.search({ model, user })\` \u2192 web-search-grounded prose.
414
841
 
415
842
  ## Content blocks (multimodal)
@@ -428,7 +855,15 @@ audio block to a Claude model throws CapabilityError before any network call.
428
855
  // run.finalText, run.toolCalls, run.usage, run.steps, run.stoppedReason
429
856
 
430
857
  Tools may carry taint labels (outputTaint / acceptsTaint) for a CaMeL-style
431
- prompt-injection gate. Personas may carry a MemoryScope.
858
+ prompt-injection gate. Personas may carry a MemoryScope. Tool inputs are schema-
859
+ validated before handlers run. Pass signal/deadline and a run budget
860
+ ({maxInputTokens,maxOutputTokens,maxTotalTokens,maxToolCalls}) to bound follow-up
861
+ effects and requested output. Input and total limits are post-turn ceilings:
862
+ provider usage is unavailable before the first response, so they cannot
863
+ tokenizer-bound the first prompt. Output and remaining-total limits cap the next
864
+ request's maxTokens. Token limits must be positive integers; maxToolCalls may be
865
+ zero. Budget exits pair every requested tool_use with an error tool_result before
866
+ memory is persisted.
432
867
 
433
868
  ## Skills
434
869
 
@@ -455,14 +890,17 @@ device-authorization handshake for a scoped, revocable token, then provisions.
455
890
  import { requestToken, init as odlaInit } from "@odla-ai/db";
456
891
  import { provisionAgentApp, odlaDbKeyResolver, OdlaDbMemory, persistRun, init } from "@odla-ai/ai";
457
892
 
458
- // 1. Handshake: the human supplies the endpoint and approves a short code in odla-db Studio.
459
- const { token } = await requestToken({ endpoint: ODLA_URL, onCode: ({ userCode }) => show(userCode) });
893
+ const ODLA_PLATFORM = "https://odla.ai";
894
+ const ODLA_DB_URL = "https://db.odla.ai";
895
+
896
+ // 1. Handshake: the human approves a short code at odla.ai.
897
+ const { token } = await requestToken({ endpoint: ODLA_PLATFORM, onCode: ({ userCode }) => show(userCode) });
460
898
 
461
899
  // 2. Provision: create app, mint app key, push agent schema, store BYO provider keys as secrets.
462
- const { appId, appKey } = await provisionAgentApp({ endpoint: ODLA_URL, token, providerKeys: { openai: OPENAI_KEY } });
900
+ const { appId, appKey } = await provisionAgentApp({ endpoint: ODLA_PLATFORM, token, providerKeys: { openai: OPENAI_KEY } });
463
901
 
464
902
  // 3. Runtime: read keys from odla-db secrets; persist memory + run traces.
465
- const db = odlaInit({ appId, adminToken: appKey, endpoint: ODLA_URL });
903
+ const db = odlaInit({ appId, adminToken: appKey, endpoint: ODLA_DB_URL });
466
904
  const ai = init({ resolveKey: odlaDbKeyResolver(db) });
467
905
  const persona = { name: "assistant", model: "gpt-5", memory: new OdlaDbMemory({ db, sessionId: "user-42" }) };
468
906
  const run = await runAgent(ai, persona, { input: "hello" });
@@ -488,16 +926,22 @@ env's odla-db tenant secrets, write-only for operators). One call reads both:
488
926
  platform: "https://odla.ai", appId: APP_ID, env: "prod", db });
489
927
  // ai.chat / ai.stream / ai.extract \u2014 provider/model from public-config
490
928
  // (cached ~60s, so Studio changes apply without redeploys); the key is
491
- // fetched from the vault at call time via odlaDbKeyResolver.
929
+ // fetched from the vault via a finite-TTL odlaDbKeyResolver.
492
930
 
493
931
  The only secret the Worker carries is its odla-db app key. Rotating the LLM
494
- key = writing the secret again; switching provider/model = a Studio edit.
932
+ key = writing the secret again; the default warm-isolate key TTL is 60 seconds
933
+ and cacheVersion provides immediate invalidation. Provider SDK clients are
934
+ fingerprinted, bounded to one generation per provider, and expire after 60
935
+ seconds. The platform provider is enforced: models from other providers are
936
+ absent from the facade catalog. If no default model is configured, each call
937
+ must name a model for that provider. Only public config is shared; tenant-bound
938
+ Ai facades are not cached globally.
495
939
 
496
940
  ## Errors
497
941
 
498
942
  Every error is an OdlaAIError subclass with a stable \`code\` \u2014 branch on err.code:
499
943
  config, auth, rate_limit, capability_unsupported, context_window, invalid_request,
500
- tool_input_invalid, provider_error.
944
+ tool_input_invalid, cancelled, deadline_exceeded, provider_error.
501
945
 
502
946
  ## Models
503
947
 
@@ -547,6 +991,7 @@ function assertSinkAcceptsTaint(sink, allowed, actual) {
547
991
 
548
992
  // src/agent/loop.ts
549
993
  async function runAgent(inference, persona, input) {
994
+ validateRunLimits(persona.maxSteps, input.budget);
550
995
  const { system, tools } = composeSkills(persona.system, persona.tools, persona.skills);
551
996
  const handlerByName = /* @__PURE__ */ new Map();
552
997
  for (const t of tools) if (t.handler) handlerByName.set(t.name, t);
@@ -558,21 +1003,26 @@ async function runAgent(inference, persona, input) {
558
1003
  const toolCalls = [];
559
1004
  const accumulatedTaint = /* @__PURE__ */ new Set(["llm_inherited"]);
560
1005
  const maxSteps = persona.maxSteps ?? 8;
1006
+ const runSignal = signalWithDeadline(input.signal, input.deadline);
561
1007
  let response;
562
1008
  let stoppedReason = "max_steps";
563
1009
  let steps = 0;
1010
+ let attemptedToolCalls = 0;
564
1011
  while (steps < maxSteps) {
565
1012
  steps++;
1013
+ const remainingOutput = remainingOutputTokens(usage, input.budget);
566
1014
  response = await inference.chat({
567
1015
  model: persona.model,
568
1016
  system,
569
1017
  messages,
570
1018
  tools: tools.length > 0 ? tools.map(toOracleTool) : void 0,
571
- maxTokens: persona.maxTokens ?? 4096,
1019
+ maxTokens: Math.min(persona.maxTokens ?? 4096, remainingOutput ?? Number.POSITIVE_INFINITY),
572
1020
  effort: persona.effort,
573
1021
  thinking: persona.thinking,
574
1022
  temperature: persona.temperature,
575
- webSearch: persona.webSearch
1023
+ webSearch: persona.webSearch,
1024
+ signal: runSignal,
1025
+ deadline: input.deadline
576
1026
  });
577
1027
  addUsage(usage, response.usage);
578
1028
  messages.push({ role: "assistant", content: response.content });
@@ -585,8 +1035,22 @@ async function runAgent(inference, persona, input) {
585
1035
  stoppedReason = "end_turn";
586
1036
  break;
587
1037
  }
1038
+ if (budgetExceeded(usage, input.budget)) {
1039
+ stoppedReason = "budget_exhausted";
1040
+ messages.push({
1041
+ role: "user",
1042
+ content: toolUses.map((toolUse) => errorResult(toolUse.id, "Run budget exhausted before tool execution."))
1043
+ });
1044
+ break;
1045
+ }
588
1046
  const resultBlocks = [];
589
1047
  for (const toolUse of toolUses) {
1048
+ if (input.budget?.maxToolCalls !== void 0 && attemptedToolCalls >= input.budget.maxToolCalls) {
1049
+ stoppedReason = "budget_exhausted";
1050
+ resultBlocks.push(errorResult(toolUse.id, "Run tool-call budget exhausted before execution."));
1051
+ continue;
1052
+ }
1053
+ attemptedToolCalls++;
590
1054
  const def = handlerByName.get(toolUse.name);
591
1055
  if (!def || !def.handler) {
592
1056
  resultBlocks.push(errorResult(toolUse.id, `No local handler for tool "${toolUse.name}".`));
@@ -594,16 +1058,26 @@ async function runAgent(inference, persona, input) {
594
1058
  }
595
1059
  if (def.acceptsTaint) assertSinkAcceptsTaint(def.name, def.acceptsTaint, accumulatedTaint);
596
1060
  let output;
597
- try {
598
- output = await def.handler(toolUse.input, { taint: new Set(accumulatedTaint), signal: input.signal });
599
- } catch (err) {
600
- output = { content: `Tool "${toolUse.name}" threw: ${err.message}`, isError: true };
1061
+ const validation = validateJsonSchema(def.inputSchema, toolUse.input);
1062
+ if (!validation.valid) {
1063
+ const detail = validation.issues.slice(0, 5).map((i) => `${i.path} ${i.message}`).join("; ");
1064
+ output = { content: `Tool "${toolUse.name}" input is invalid: ${detail}`, isError: true };
1065
+ } else {
1066
+ throwIfAborted(runSignal);
1067
+ try {
1068
+ output = await def.handler(toolUse.input, { taint: new Set(accumulatedTaint), signal: runSignal });
1069
+ } catch (error) {
1070
+ if (error instanceof CancelledError || error instanceof DeadlineExceededError) throw error;
1071
+ throwIfAborted(runSignal);
1072
+ output = { content: `Tool "${toolUse.name}" failed.`, isError: true };
1073
+ }
601
1074
  }
602
1075
  toolCalls.push({ toolUse, output });
603
1076
  resultBlocks.push({ type: "tool_result", toolUseId: toolUse.id, content: output.content, isError: output.isError });
604
1077
  for (const label of def.outputTaint ?? []) accumulatedTaint.add(label);
605
1078
  }
606
1079
  messages.push({ role: "user", content: resultBlocks });
1080
+ if (stoppedReason === "budget_exhausted") break;
607
1081
  }
608
1082
  if (!response) throw new Error("runAgent: maxSteps must be at least 1");
609
1083
  if (persona.memory) await persona.memory.append(messages.slice(runStartIndex));
@@ -617,6 +1091,36 @@ async function runAgent(inference, persona, input) {
617
1091
  stoppedReason
618
1092
  };
619
1093
  }
1094
+ function budgetExceeded(usage, budget) {
1095
+ if (!budget) return false;
1096
+ if (budget.maxInputTokens !== void 0 && usage.inputTokens >= budget.maxInputTokens) return true;
1097
+ if (budget.maxOutputTokens !== void 0 && usage.outputTokens >= budget.maxOutputTokens) return true;
1098
+ if (budget.maxTotalTokens !== void 0 && usage.inputTokens + usage.outputTokens >= budget.maxTotalTokens) return true;
1099
+ return false;
1100
+ }
1101
+ function remainingOutputTokens(usage, budget) {
1102
+ if (!budget) return void 0;
1103
+ const remaining = [
1104
+ budget.maxOutputTokens === void 0 ? void 0 : budget.maxOutputTokens - usage.outputTokens,
1105
+ budget.maxTotalTokens === void 0 ? void 0 : budget.maxTotalTokens - usage.inputTokens - usage.outputTokens
1106
+ ].filter((value) => value !== void 0);
1107
+ return remaining.length === 0 ? void 0 : Math.max(1, Math.min(...remaining));
1108
+ }
1109
+ function validateRunLimits(maxSteps, budget) {
1110
+ if (maxSteps !== void 0 && (!Number.isInteger(maxSteps) || maxSteps < 1)) {
1111
+ throw new ConfigError("persona.maxSteps must be a positive integer.");
1112
+ }
1113
+ if (!budget) return;
1114
+ for (const key of ["maxInputTokens", "maxOutputTokens", "maxTotalTokens"]) {
1115
+ const value = budget[key];
1116
+ if (value !== void 0 && (!Number.isInteger(value) || value < 1)) {
1117
+ throw new ConfigError(`budget.${key} must be a positive integer.`);
1118
+ }
1119
+ }
1120
+ if (budget.maxToolCalls !== void 0 && (!Number.isInteger(budget.maxToolCalls) || budget.maxToolCalls < 0)) {
1121
+ throw new ConfigError("budget.maxToolCalls must be a non-negative integer.");
1122
+ }
1123
+ }
620
1124
  function errorResult(toolUseId, message) {
621
1125
  return { type: "tool_result", toolUseId, content: message, isError: true };
622
1126
  }
@@ -919,22 +1423,32 @@ var DEFAULT_SECRET_NAMES = {
919
1423
  };
920
1424
  function odlaDbKeyResolver(db, opts = {}) {
921
1425
  const nameFor = opts.secretName ?? ((p) => DEFAULT_SECRET_NAMES[p]);
922
- const useCache = opts.cache ?? true;
1426
+ const ttlMs = opts.cache === false ? 0 : Math.max(0, opts.ttlMs ?? 6e4);
923
1427
  const cache3 = /* @__PURE__ */ new Map();
924
1428
  return async (provider) => {
925
- if (useCache) {
1429
+ const version = typeof opts.cacheVersion === "function" ? opts.cacheVersion(provider) : opts.cacheVersion ?? "";
1430
+ if (ttlMs > 0) {
926
1431
  const hit = cache3.get(provider);
927
- if (hit !== void 0) return hit;
1432
+ if (hit?.version === version && hit.expiresAt > Date.now()) return hit.value;
1433
+ cache3.delete(provider);
928
1434
  }
929
1435
  try {
930
1436
  const key = await db.secrets.get(nameFor(provider));
931
- if (useCache) cache3.set(provider, key);
1437
+ if (ttlMs > 0) cache3.set(provider, { version, value: key, expiresAt: Date.now() + ttlMs });
932
1438
  return key;
933
- } catch {
934
- return void 0;
1439
+ } catch (error) {
1440
+ if (isMissingSecret(error)) return void 0;
1441
+ throw error;
935
1442
  }
936
1443
  };
937
1444
  }
1445
+ function isMissingSecret(error) {
1446
+ if (!error || typeof error !== "object") return false;
1447
+ const rec = error;
1448
+ if (rec.status === 404 || rec.statusCode === 404 || rec.code === "not_found") return true;
1449
+ const message = error instanceof Error ? error.message : "";
1450
+ return /\b404\b/.test(message);
1451
+ }
938
1452
 
939
1453
  // src/store/platform.ts
940
1454
  var cache2 = /* @__PURE__ */ new Map();
@@ -965,19 +1479,22 @@ async function initFromPlatform(opts) {
965
1479
  const key = `${opts.platform}|${opts.appId}|${opts.env}`;
966
1480
  const now = Date.now();
967
1481
  const hit = ttl > 0 ? cache2.get(key) : void 0;
968
- if (hit && hit.expiresAt > now) return hit.value;
969
- const { provider, model } = await fetchAiConfig(opts);
970
- if (hit && hit.value.provider === provider && hit.value.model === model) {
971
- hit.expiresAt = now + ttl;
972
- return hit.value;
973
- }
1482
+ const config = hit && hit.expiresAt > now ? hit.value : await fetchAiConfig(opts);
1483
+ const { provider, model } = config;
1484
+ if (ttl > 0 && (!hit || hit.expiresAt <= now)) cache2.set(key, { value: config, expiresAt: now + ttl });
1485
+ const sourceCatalog = opts.initOptions?.catalog ?? DEFAULT_CATALOG;
1486
+ const catalog = Object.fromEntries(
1487
+ Object.entries(sourceCatalog).filter(([, spec]) => spec.provider === provider)
1488
+ );
1489
+ const defaultModel = model ?? opts.initOptions?.defaultModel;
1490
+ if (defaultModel) resolveModel(catalog, defaultModel);
974
1491
  const ai = init({
975
1492
  ...opts.initOptions,
976
- resolveKey: odlaDbKeyResolver(opts.db),
977
- defaultModel: model ?? opts.initOptions?.defaultModel
1493
+ catalog,
1494
+ resolveKey: odlaDbKeyResolver(opts.db, opts.keyResolverOptions),
1495
+ defaultModel
978
1496
  });
979
1497
  const value = { ai, provider, ...model ? { model } : {} };
980
- if (ttl > 0) cache2.set(key, { value, expiresAt: now + ttl });
981
1498
  return value;
982
1499
  }
983
1500
 
@@ -1170,11 +1687,13 @@ export {
1170
1687
  AGENT_SCHEMA,
1171
1688
  ANTHROPIC_MODELS,
1172
1689
  AuthError,
1690
+ CancelledError,
1173
1691
  CapabilityError,
1174
1692
  ConfigError,
1175
1693
  ContextWindowError,
1176
1694
  DEFAULT_CATALOG,
1177
1695
  DEFAULT_SECRET_NAMES,
1696
+ DeadlineExceededError,
1178
1697
  GOOGLE_MODELS,
1179
1698
  InMemoryScope,
1180
1699
  InvalidRequestError,
@@ -1185,6 +1704,7 @@ export {
1185
1704
  ProviderError,
1186
1705
  RateLimitError,
1187
1706
  TaintError,
1707
+ ToolInputError,
1188
1708
  addUsage,
1189
1709
  assertSinkAcceptsTaint,
1190
1710
  blocksOf,
@@ -1230,6 +1750,7 @@ export {
1230
1750
  structuredMatch,
1231
1751
  taint,
1232
1752
  toOracleTool,
1753
+ validateJsonSchema,
1233
1754
  validateRequest
1234
1755
  };
1235
1756
  //# sourceMappingURL=index.js.map