@odla-ai/ai 0.1.2 → 0.2.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/README.md CHANGED
@@ -252,6 +252,33 @@ is a *privileged* action (operator or `odla_dev_` token, via `provisionAgentApp`
252
252
  never returned to browser clients. `OdlaDbMemory` persists conversation turns and
253
253
  `persistRun` writes run traces, both as natural-key upserts (idempotent on retry).
254
254
 
255
+ ### Platform apps (odla.ai)
256
+
257
+ For apps registered on the odla platform, LLM access is an **app setting**:
258
+ provider + default model live in the registry per environment (Studio's AI
259
+ card, or `@odla-ai/apps` `setAi`), and the API key lives in the platform
260
+ vault — the env's odla-db tenant secrets. `initFromPlatform` reads both:
261
+
262
+ ```ts
263
+ import { initFromPlatform } from "@odla-ai/ai";
264
+ import { init as odlaInit } from "@odla-ai/db";
265
+
266
+ const db = odlaInit({ appId: TENANT, adminToken: env.ODLA_API_KEY, endpoint: "https://db.odla.ai" });
267
+ const { ai, provider, model } = await initFromPlatform({
268
+ platform: "https://odla.ai",
269
+ appId: APP_ID, // the registry app id, not the tenant
270
+ env: "prod",
271
+ db,
272
+ });
273
+ ```
274
+
275
+ The config comes from the anonymous `public-config` endpoint and is cached
276
+ (~60s by default), so switching provider or model in Studio takes effect
277
+ without a redeploy; the key resolves from the vault at call time, so rotation
278
+ is just writing the secret again. **Never put provider keys in an app
279
+ Worker's wrangler vars/secrets** — the worker's only secret is its odla-db
280
+ app key.
281
+
255
282
  ## Models & capabilities
256
283
 
257
284
  The catalog maps a canonical model id → provider, native id, and capabilities.
package/dist/index.cjs CHANGED
@@ -5,8 +5,13 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
6
  var __getProtoOf = Object.getPrototypeOf;
7
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
8
- var __esm = (fn, res) => function __init() {
9
- return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
8
+ var __esm = (fn, res, err) => function __init() {
9
+ if (err) throw err[0];
10
+ try {
11
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
12
+ } catch (e) {
13
+ throw err = [e], e;
14
+ }
10
15
  };
11
16
  var __export = (target, all) => {
12
17
  for (var name in all)
@@ -438,6 +443,7 @@ function buildParams2(spec, req, stream) {
438
443
  json_schema: { name: req.responseFormat.name ?? "response", schema: req.responseFormat.schema }
439
444
  };
440
445
  }
446
+ if (req.webSearch) params.web_search_options = { search_context_size: "medium" };
441
447
  if (stream) {
442
448
  params.stream = true;
443
449
  params.stream_options = { include_usage: true };
@@ -903,6 +909,7 @@ __export(index_exports, {
903
909
  blocksOf: () => blocksOf,
904
910
  buildCatalog: () => buildCatalog,
905
911
  chatCapabilities: () => chatCapabilities,
912
+ clearPlatformAiCache: () => clearPlatformAiCache,
906
913
  clearProviderCache: () => clearProviderCache,
907
914
  composeSkills: () => composeSkills,
908
915
  createApp: () => createApp,
@@ -916,6 +923,7 @@ __export(index_exports, {
916
923
  getProvider: () => getProvider,
917
924
  includes: () => includes,
918
925
  init: () => init,
926
+ initFromPlatform: () => initFromPlatform,
919
927
  isAudioBlock: () => isAudioBlock,
920
928
  isDocumentBlock: () => isDocumentBlock,
921
929
  isImageBlock: () => isImageBlock,
@@ -1069,6 +1077,22 @@ var OPENAI_MODELS = [
1069
1077
  capabilities: chatCapabilities({ effort: true }),
1070
1078
  contextWindow: 4e5
1071
1079
  },
1080
+ {
1081
+ id: "gpt-5-nano",
1082
+ provider: "openai",
1083
+ nativeId: "gpt-5-nano",
1084
+ capabilities: chatCapabilities({ effort: true }),
1085
+ contextWindow: 4e5
1086
+ },
1087
+ {
1088
+ // Search-tuned chat model: the adapter sends web_search_options when a
1089
+ // request sets webSearch. No reasoning-effort knob.
1090
+ id: "gpt-5-search-api",
1091
+ provider: "openai",
1092
+ nativeId: "gpt-5-search-api",
1093
+ capabilities: chatCapabilities({ webSearch: true }),
1094
+ contextWindow: 4e5
1095
+ },
1072
1096
  {
1073
1097
  // Non-reasoning multimodal chat (image input, no effort knob).
1074
1098
  id: "gpt-4o",
@@ -1390,6 +1414,27 @@ Secrets are read-only from the app key (odlaDbKeyResolver \u2192 db.secrets.get)
1390
1414
  are WRITTEN only with the operator/dev token via provisionAgentApp/putSecret. Keys
1391
1415
  are AES-GCM-encrypted at rest in odla-db and never returned to browser clients.
1392
1416
 
1417
+ ## Platform wiring (odla.ai apps registry)
1418
+
1419
+ Apps registered on the odla platform get LLM access as an APP SETTING \u2014
1420
+ never put provider keys in your Worker's wrangler vars/secrets. Provider +
1421
+ default model are per-environment registry config (set in Studio's AI card
1422
+ or \`@odla-ai/apps\` setAi); the API key lives in the platform vault (the
1423
+ env's odla-db tenant secrets, write-only for operators). One call reads both:
1424
+
1425
+ import { initFromPlatform } from "@odla-ai/ai";
1426
+ import { init as odlaInit } from "@odla-ai/db";
1427
+
1428
+ const db = odlaInit({ appId: TENANT, adminToken: env.ODLA_API_KEY, endpoint: "https://db.odla.ai" });
1429
+ const { ai, provider, model } = await initFromPlatform({
1430
+ platform: "https://odla.ai", appId: APP_ID, env: "prod", db });
1431
+ // ai.chat / ai.stream / ai.extract \u2014 provider/model from public-config
1432
+ // (cached ~60s, so Studio changes apply without redeploys); the key is
1433
+ // fetched from the vault at call time via odlaDbKeyResolver.
1434
+
1435
+ 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.
1437
+
1393
1438
  ## Errors
1394
1439
 
1395
1440
  Every error is an OdlaAIError subclass with a stable \`code\` \u2014 branch on err.code:
@@ -1826,15 +1871,15 @@ var DEFAULT_SECRET_NAMES = {
1826
1871
  function odlaDbKeyResolver(db, opts = {}) {
1827
1872
  const nameFor = opts.secretName ?? ((p) => DEFAULT_SECRET_NAMES[p]);
1828
1873
  const useCache = opts.cache ?? true;
1829
- const cache2 = /* @__PURE__ */ new Map();
1874
+ const cache3 = /* @__PURE__ */ new Map();
1830
1875
  return async (provider) => {
1831
1876
  if (useCache) {
1832
- const hit = cache2.get(provider);
1877
+ const hit = cache3.get(provider);
1833
1878
  if (hit !== void 0) return hit;
1834
1879
  }
1835
1880
  try {
1836
1881
  const key = await db.secrets.get(nameFor(provider));
1837
- if (useCache) cache2.set(provider, key);
1882
+ if (useCache) cache3.set(provider, key);
1838
1883
  return key;
1839
1884
  } catch {
1840
1885
  return void 0;
@@ -1842,6 +1887,52 @@ function odlaDbKeyResolver(db, opts = {}) {
1842
1887
  };
1843
1888
  }
1844
1889
 
1890
+ // src/store/platform.ts
1891
+ init_shape();
1892
+ var cache2 = /* @__PURE__ */ new Map();
1893
+ function clearPlatformAiCache() {
1894
+ cache2.clear();
1895
+ }
1896
+ async function fetchAiConfig(opts) {
1897
+ const doFetch = opts.fetch ?? fetch;
1898
+ const base = opts.platform.replace(/\/$/, "");
1899
+ const url = `${base}/registry/apps/${encodeURIComponent(opts.appId)}/public-config?env=${encodeURIComponent(opts.env)}`;
1900
+ const res = await doFetch(url);
1901
+ if (!res.ok) throw new ProviderError(`platform public-config fetch failed: ${res.status} for ${opts.appId}/${opts.env}`);
1902
+ const cfg = await res.json();
1903
+ const ai = cfg.ai;
1904
+ if (!ai || typeof ai.provider !== "string") {
1905
+ throw new ConfigError(
1906
+ `ai service is not enabled/configured for "${opts.appId}" (${opts.env}) \u2014 enable it in Studio or via @odla-ai/apps setAi(appId, env, { provider }).`
1907
+ );
1908
+ }
1909
+ if (!(ai.provider in DEFAULT_SECRET_NAMES)) {
1910
+ throw new ConfigError(`platform ai config names unknown provider "${ai.provider}" for "${opts.appId}" (${opts.env}).`);
1911
+ }
1912
+ const model = typeof ai.model === "string" && ai.model ? ai.model : void 0;
1913
+ return { provider: ai.provider, ...model ? { model } : {} };
1914
+ }
1915
+ async function initFromPlatform(opts) {
1916
+ const ttl = opts.ttlMs ?? 6e4;
1917
+ const key = `${opts.platform}|${opts.appId}|${opts.env}`;
1918
+ const now = Date.now();
1919
+ 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
+ }
1926
+ const ai = init({
1927
+ ...opts.initOptions,
1928
+ resolveKey: odlaDbKeyResolver(opts.db),
1929
+ defaultModel: model ?? opts.initOptions?.defaultModel
1930
+ });
1931
+ const value = { ai, provider, ...model ? { model } : {} };
1932
+ if (ttl > 0) cache2.set(key, { value, expiresAt: now + ttl });
1933
+ return value;
1934
+ }
1935
+
1845
1936
  // src/store/provision.ts
1846
1937
  init_shape();
1847
1938
  async function post(ctx, path, body) {
@@ -1885,6 +1976,9 @@ async function pushAgentSchema(ctx, appId, appKey) {
1885
1976
  async function putSecret(ctx, appId, name, value) {
1886
1977
  await post(ctx, `/admin/apps/${encodeURIComponent(appId)}/secrets`, { name, value });
1887
1978
  }
1979
+ async function putRules(ctx, appId, rules) {
1980
+ await post(ctx, `/app/${encodeURIComponent(appId)}/admin/rules`, rules);
1981
+ }
1888
1982
  async function provisionAgentApp(opts) {
1889
1983
  const ctx = { endpoint: opts.endpoint, token: opts.token, fetch: opts.fetch };
1890
1984
  const appId = await createApp(ctx, { appId: opts.appId, name: opts.appName });
@@ -1892,6 +1986,9 @@ async function provisionAgentApp(opts) {
1892
1986
  if (opts.pushSchema !== false) {
1893
1987
  await pushAgentSchema({ endpoint: opts.endpoint, fetch: opts.fetch }, appId, appKey);
1894
1988
  }
1989
+ if (opts.rules) {
1990
+ await putRules(ctx, appId, opts.rules);
1991
+ }
1895
1992
  const nameFor = opts.secretName ?? ((p) => DEFAULT_SECRET_NAMES[p]);
1896
1993
  for (const [provider, value] of Object.entries(opts.providerKeys ?? {})) {
1897
1994
  if (!value) continue;