@odla-ai/ai 0.2.2 → 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,16 +1,21 @@
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
14
  emptyUsage,
12
- normalizeError
13
- } from "./chunk-QFT2DU2X.js";
15
+ normalizeError,
16
+ signalWithDeadline,
17
+ throwIfAborted
18
+ } from "./chunk-PXXCN2EU.js";
14
19
 
15
20
  // src/shape/helpers.ts
16
21
  function isTextBlock(b) {
@@ -44,8 +49,362 @@ function extractToolUses(content) {
44
49
  return content.filter(isToolUseBlock);
45
50
  }
46
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
+ }
405
+
47
406
  // src/shape/capabilities.ts
48
- function chatCapabilities(overrides = {}) {
407
+ function chatCapabilities(overrides2 = {}) {
49
408
  return {
50
409
  kind: "chat",
51
410
  textIn: true,
@@ -58,7 +417,7 @@ function chatCapabilities(overrides = {}) {
58
417
  streaming: true,
59
418
  structuredOutput: true,
60
419
  webSearch: false,
61
- ...overrides
420
+ ...overrides2
62
421
  };
63
422
  }
64
423
  function validateRequest(spec, req) {
@@ -147,6 +506,15 @@ var OPENAI_MODELS = [
147
506
  capabilities: chatCapabilities({ effort: true }),
148
507
  contextWindow: 4e5
149
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
+ },
150
518
  {
151
519
  id: "gpt-5",
152
520
  provider: "openai",
@@ -264,37 +632,60 @@ function providersInCatalog(catalog) {
264
632
  }
265
633
 
266
634
  // src/providers/registry.ts
635
+ var CLIENT_TTL_MS = 6e4;
267
636
  var cache = /* @__PURE__ */ new Map();
637
+ var inflight = /* @__PURE__ */ new Map();
638
+ var overrides = /* @__PURE__ */ new Map();
268
639
  async function getProvider(id, apiKey) {
269
- const cacheKey = `${id}:${apiKey}`;
270
- const existing = cache.get(cacheKey);
271
- 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) {
272
658
  let provider;
273
659
  switch (id) {
274
660
  case "anthropic": {
275
- const { AnthropicProvider } = await import("./anthropic-BIGCVQE4.js");
661
+ const { AnthropicProvider } = await import("./anthropic-2NPMHJZT.js");
276
662
  provider = new AnthropicProvider(apiKey);
277
663
  break;
278
664
  }
279
665
  case "openai": {
280
- const { OpenAIProvider } = await import("./openai-PTZPJYTT.js");
666
+ const { OpenAIProvider } = await import("./openai-NTQCF577.js");
281
667
  provider = new OpenAIProvider(apiKey);
282
668
  break;
283
669
  }
284
670
  case "google": {
285
- const { GoogleProvider } = await import("./google-QY5B4T5O.js");
671
+ const { GoogleProvider } = await import("./google-3XV2VWVB.js");
286
672
  provider = new GoogleProvider(apiKey);
287
673
  break;
288
674
  }
289
675
  }
290
- cache.set(cacheKey, provider);
291
676
  return provider;
292
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
+ }
293
682
  function registerProvider(id, apiKey, provider) {
294
- cache.set(`${id}:${apiKey}`, provider);
683
+ overrides.set(id, { apiKey, provider });
295
684
  }
296
685
  function clearProviderCache() {
297
686
  cache.clear();
687
+ inflight.clear();
688
+ overrides.clear();
298
689
  }
299
690
 
300
691
  // src/client/keys.ts
@@ -364,7 +755,9 @@ function init(opts = {}) {
364
755
  messages: [{ role: "user", content: c.user }],
365
756
  tools: [{ name: c.tool.name, description: c.tool.description ?? "", inputSchema: c.tool.parameters }],
366
757
  toolChoice: { type: "tool", name: c.tool.name },
367
- maxTokens: c.maxTokens ?? 4096
758
+ maxTokens: c.maxTokens ?? 4096,
759
+ signal: c.signal,
760
+ deadline: c.deadline
368
761
  };
369
762
  const { spec, provider } = await resolveProviderFor(model);
370
763
  validateRequest(spec, req);
@@ -373,6 +766,11 @@ function init(opts = {}) {
373
766
  if (!block || block.type !== "tool_use") {
374
767
  throw new ProviderError(`Model "${model}" did not return the forced tool call "${c.tool.name}".`);
375
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
+ }
376
774
  return { value: block.input, usage: res.usage };
377
775
  };
378
776
  const search = async (c) => {
@@ -382,7 +780,9 @@ function init(opts = {}) {
382
780
  system: c.system,
383
781
  messages: [{ role: "user", content: c.user }],
384
782
  webSearch: true,
385
- maxTokens: c.maxTokens ?? 8192
783
+ maxTokens: c.maxTokens ?? 8192,
784
+ signal: c.signal,
785
+ deadline: c.deadline
386
786
  };
387
787
  const { spec, provider } = await resolveProviderFor(model);
388
788
  validateRequest(spec, req);
@@ -408,20 +808,20 @@ function generateLlmsTxt(catalog = DEFAULT_CATALOG) {
408
808
  const models = Object.values(catalog).map((s) => `- \`${s.id}\` (${s.provider}) \u2014 ${capsSummary(s.capabilities)}`).join("\n");
409
809
  return `# @odla-ai/ai \u2014 LLM context
410
810
 
411
- > EXPERIMENTAL \u2014 an agentic-coding experiment. This package is built and operated
412
- > by autonomous coding agents as an experiment in agentic loops. APIs change
413
- > without notice and nothing here is production-hardened. Use at your own risk.
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.
414
815
 
415
816
  One general-purpose interface for AI inference across Anthropic (Claude), OpenAI
416
817
  (GPT), and Google (Gemini) \u2014 text, image, and audio \u2014 plus a tool-use agent
417
818
  engine and an eval harness. Library-only: import in-process, bring your own keys.
418
- 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.
419
821
 
420
822
  ## Install
421
823
 
422
824
  npm i @odla-ai/ai
423
- # provider SDKs are peer-lazy-loaded; install those you use:
424
- # @anthropic-ai/sdk openai @google/genai
425
825
  # optional, for odla-db-backed storage + secrets:
426
826
  # @odla-ai/db
427
827
 
@@ -435,7 +835,8 @@ Isomorphic (Node 20+, Cloudflare Workers, browser). Provider SDKs are lazy-loade
435
835
  - \`ai.stream(input)\` \u2192 async iterable of normalized events: message_start \u2192
436
836
  content_block_start \u2192 content_block_delta {text_delta|thinking_delta|input_json_delta}
437
837
  \u2192 content_block_stop \u2192 message_delta \u2192 message_stop (plus \`error\`).
438
- - \`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.
439
840
  - \`ai.search({ model, user })\` \u2192 web-search-grounded prose.
440
841
 
441
842
  ## Content blocks (multimodal)
@@ -454,7 +855,15 @@ audio block to a Claude model throws CapabilityError before any network call.
454
855
  // run.finalText, run.toolCalls, run.usage, run.steps, run.stoppedReason
455
856
 
456
857
  Tools may carry taint labels (outputTaint / acceptsTaint) for a CaMeL-style
457
- 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.
458
867
 
459
868
  ## Skills
460
869
 
@@ -481,14 +890,17 @@ device-authorization handshake for a scoped, revocable token, then provisions.
481
890
  import { requestToken, init as odlaInit } from "@odla-ai/db";
482
891
  import { provisionAgentApp, odlaDbKeyResolver, OdlaDbMemory, persistRun, init } from "@odla-ai/ai";
483
892
 
484
- // 1. Handshake: the human supplies the endpoint and approves a short code in odla-db Studio.
485
- 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) });
486
898
 
487
899
  // 2. Provision: create app, mint app key, push agent schema, store BYO provider keys as secrets.
488
- 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 } });
489
901
 
490
902
  // 3. Runtime: read keys from odla-db secrets; persist memory + run traces.
491
- const db = odlaInit({ appId, adminToken: appKey, endpoint: ODLA_URL });
903
+ const db = odlaInit({ appId, adminToken: appKey, endpoint: ODLA_DB_URL });
492
904
  const ai = init({ resolveKey: odlaDbKeyResolver(db) });
493
905
  const persona = { name: "assistant", model: "gpt-5", memory: new OdlaDbMemory({ db, sessionId: "user-42" }) };
494
906
  const run = await runAgent(ai, persona, { input: "hello" });
@@ -514,16 +926,22 @@ env's odla-db tenant secrets, write-only for operators). One call reads both:
514
926
  platform: "https://odla.ai", appId: APP_ID, env: "prod", db });
515
927
  // ai.chat / ai.stream / ai.extract \u2014 provider/model from public-config
516
928
  // (cached ~60s, so Studio changes apply without redeploys); the key is
517
- // fetched from the vault at call time via odlaDbKeyResolver.
929
+ // fetched from the vault via a finite-TTL odlaDbKeyResolver.
518
930
 
519
931
  The only secret the Worker carries is its odla-db app key. Rotating the LLM
520
- 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.
521
939
 
522
940
  ## Errors
523
941
 
524
942
  Every error is an OdlaAIError subclass with a stable \`code\` \u2014 branch on err.code:
525
943
  config, auth, rate_limit, capability_unsupported, context_window, invalid_request,
526
- tool_input_invalid, provider_error.
944
+ tool_input_invalid, cancelled, deadline_exceeded, provider_error.
527
945
 
528
946
  ## Models
529
947
 
@@ -573,6 +991,7 @@ function assertSinkAcceptsTaint(sink, allowed, actual) {
573
991
 
574
992
  // src/agent/loop.ts
575
993
  async function runAgent(inference, persona, input) {
994
+ validateRunLimits(persona.maxSteps, input.budget);
576
995
  const { system, tools } = composeSkills(persona.system, persona.tools, persona.skills);
577
996
  const handlerByName = /* @__PURE__ */ new Map();
578
997
  for (const t of tools) if (t.handler) handlerByName.set(t.name, t);
@@ -584,21 +1003,26 @@ async function runAgent(inference, persona, input) {
584
1003
  const toolCalls = [];
585
1004
  const accumulatedTaint = /* @__PURE__ */ new Set(["llm_inherited"]);
586
1005
  const maxSteps = persona.maxSteps ?? 8;
1006
+ const runSignal = signalWithDeadline(input.signal, input.deadline);
587
1007
  let response;
588
1008
  let stoppedReason = "max_steps";
589
1009
  let steps = 0;
1010
+ let attemptedToolCalls = 0;
590
1011
  while (steps < maxSteps) {
591
1012
  steps++;
1013
+ const remainingOutput = remainingOutputTokens(usage, input.budget);
592
1014
  response = await inference.chat({
593
1015
  model: persona.model,
594
1016
  system,
595
1017
  messages,
596
1018
  tools: tools.length > 0 ? tools.map(toOracleTool) : void 0,
597
- maxTokens: persona.maxTokens ?? 4096,
1019
+ maxTokens: Math.min(persona.maxTokens ?? 4096, remainingOutput ?? Number.POSITIVE_INFINITY),
598
1020
  effort: persona.effort,
599
1021
  thinking: persona.thinking,
600
1022
  temperature: persona.temperature,
601
- webSearch: persona.webSearch
1023
+ webSearch: persona.webSearch,
1024
+ signal: runSignal,
1025
+ deadline: input.deadline
602
1026
  });
603
1027
  addUsage(usage, response.usage);
604
1028
  messages.push({ role: "assistant", content: response.content });
@@ -611,8 +1035,22 @@ async function runAgent(inference, persona, input) {
611
1035
  stoppedReason = "end_turn";
612
1036
  break;
613
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
+ }
614
1046
  const resultBlocks = [];
615
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++;
616
1054
  const def = handlerByName.get(toolUse.name);
617
1055
  if (!def || !def.handler) {
618
1056
  resultBlocks.push(errorResult(toolUse.id, `No local handler for tool "${toolUse.name}".`));
@@ -620,16 +1058,26 @@ async function runAgent(inference, persona, input) {
620
1058
  }
621
1059
  if (def.acceptsTaint) assertSinkAcceptsTaint(def.name, def.acceptsTaint, accumulatedTaint);
622
1060
  let output;
623
- try {
624
- output = await def.handler(toolUse.input, { taint: new Set(accumulatedTaint), signal: input.signal });
625
- } catch (err) {
626
- 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
+ }
627
1074
  }
628
1075
  toolCalls.push({ toolUse, output });
629
1076
  resultBlocks.push({ type: "tool_result", toolUseId: toolUse.id, content: output.content, isError: output.isError });
630
1077
  for (const label of def.outputTaint ?? []) accumulatedTaint.add(label);
631
1078
  }
632
1079
  messages.push({ role: "user", content: resultBlocks });
1080
+ if (stoppedReason === "budget_exhausted") break;
633
1081
  }
634
1082
  if (!response) throw new Error("runAgent: maxSteps must be at least 1");
635
1083
  if (persona.memory) await persona.memory.append(messages.slice(runStartIndex));
@@ -643,6 +1091,36 @@ async function runAgent(inference, persona, input) {
643
1091
  stoppedReason
644
1092
  };
645
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
+ }
646
1124
  function errorResult(toolUseId, message) {
647
1125
  return { type: "tool_result", toolUseId, content: message, isError: true };
648
1126
  }
@@ -945,22 +1423,32 @@ var DEFAULT_SECRET_NAMES = {
945
1423
  };
946
1424
  function odlaDbKeyResolver(db, opts = {}) {
947
1425
  const nameFor = opts.secretName ?? ((p) => DEFAULT_SECRET_NAMES[p]);
948
- const useCache = opts.cache ?? true;
1426
+ const ttlMs = opts.cache === false ? 0 : Math.max(0, opts.ttlMs ?? 6e4);
949
1427
  const cache3 = /* @__PURE__ */ new Map();
950
1428
  return async (provider) => {
951
- if (useCache) {
1429
+ const version = typeof opts.cacheVersion === "function" ? opts.cacheVersion(provider) : opts.cacheVersion ?? "";
1430
+ if (ttlMs > 0) {
952
1431
  const hit = cache3.get(provider);
953
- if (hit !== void 0) return hit;
1432
+ if (hit?.version === version && hit.expiresAt > Date.now()) return hit.value;
1433
+ cache3.delete(provider);
954
1434
  }
955
1435
  try {
956
1436
  const key = await db.secrets.get(nameFor(provider));
957
- if (useCache) cache3.set(provider, key);
1437
+ if (ttlMs > 0) cache3.set(provider, { version, value: key, expiresAt: Date.now() + ttlMs });
958
1438
  return key;
959
- } catch {
960
- return void 0;
1439
+ } catch (error) {
1440
+ if (isMissingSecret(error)) return void 0;
1441
+ throw error;
961
1442
  }
962
1443
  };
963
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
+ }
964
1452
 
965
1453
  // src/store/platform.ts
966
1454
  var cache2 = /* @__PURE__ */ new Map();
@@ -991,19 +1479,22 @@ async function initFromPlatform(opts) {
991
1479
  const key = `${opts.platform}|${opts.appId}|${opts.env}`;
992
1480
  const now = Date.now();
993
1481
  const hit = ttl > 0 ? cache2.get(key) : void 0;
994
- if (hit && hit.expiresAt > now) return hit.value;
995
- const { provider, model } = await fetchAiConfig(opts);
996
- if (hit && hit.value.provider === provider && hit.value.model === model) {
997
- hit.expiresAt = now + ttl;
998
- return hit.value;
999
- }
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);
1000
1491
  const ai = init({
1001
1492
  ...opts.initOptions,
1002
- resolveKey: odlaDbKeyResolver(opts.db),
1003
- defaultModel: model ?? opts.initOptions?.defaultModel
1493
+ catalog,
1494
+ resolveKey: odlaDbKeyResolver(opts.db, opts.keyResolverOptions),
1495
+ defaultModel
1004
1496
  });
1005
1497
  const value = { ai, provider, ...model ? { model } : {} };
1006
- if (ttl > 0) cache2.set(key, { value, expiresAt: now + ttl });
1007
1498
  return value;
1008
1499
  }
1009
1500
 
@@ -1196,11 +1687,13 @@ export {
1196
1687
  AGENT_SCHEMA,
1197
1688
  ANTHROPIC_MODELS,
1198
1689
  AuthError,
1690
+ CancelledError,
1199
1691
  CapabilityError,
1200
1692
  ConfigError,
1201
1693
  ContextWindowError,
1202
1694
  DEFAULT_CATALOG,
1203
1695
  DEFAULT_SECRET_NAMES,
1696
+ DeadlineExceededError,
1204
1697
  GOOGLE_MODELS,
1205
1698
  InMemoryScope,
1206
1699
  InvalidRequestError,
@@ -1211,6 +1704,7 @@ export {
1211
1704
  ProviderError,
1212
1705
  RateLimitError,
1213
1706
  TaintError,
1707
+ ToolInputError,
1214
1708
  addUsage,
1215
1709
  assertSinkAcceptsTaint,
1216
1710
  blocksOf,
@@ -1256,6 +1750,7 @@ export {
1256
1750
  structuredMatch,
1257
1751
  taint,
1258
1752
  toOracleTool,
1753
+ validateJsonSchema,
1259
1754
  validateRequest
1260
1755
  };
1261
1756
  //# sourceMappingURL=index.js.map