@odla-ai/ai 0.1.4 → 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
@@ -909,6 +909,7 @@ __export(index_exports, {
909
909
  blocksOf: () => blocksOf,
910
910
  buildCatalog: () => buildCatalog,
911
911
  chatCapabilities: () => chatCapabilities,
912
+ clearPlatformAiCache: () => clearPlatformAiCache,
912
913
  clearProviderCache: () => clearProviderCache,
913
914
  composeSkills: () => composeSkills,
914
915
  createApp: () => createApp,
@@ -922,6 +923,7 @@ __export(index_exports, {
922
923
  getProvider: () => getProvider,
923
924
  includes: () => includes,
924
925
  init: () => init,
926
+ initFromPlatform: () => initFromPlatform,
925
927
  isAudioBlock: () => isAudioBlock,
926
928
  isDocumentBlock: () => isDocumentBlock,
927
929
  isImageBlock: () => isImageBlock,
@@ -1412,6 +1414,27 @@ Secrets are read-only from the app key (odlaDbKeyResolver \u2192 db.secrets.get)
1412
1414
  are WRITTEN only with the operator/dev token via provisionAgentApp/putSecret. Keys
1413
1415
  are AES-GCM-encrypted at rest in odla-db and never returned to browser clients.
1414
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
+
1415
1438
  ## Errors
1416
1439
 
1417
1440
  Every error is an OdlaAIError subclass with a stable \`code\` \u2014 branch on err.code:
@@ -1848,15 +1871,15 @@ var DEFAULT_SECRET_NAMES = {
1848
1871
  function odlaDbKeyResolver(db, opts = {}) {
1849
1872
  const nameFor = opts.secretName ?? ((p) => DEFAULT_SECRET_NAMES[p]);
1850
1873
  const useCache = opts.cache ?? true;
1851
- const cache2 = /* @__PURE__ */ new Map();
1874
+ const cache3 = /* @__PURE__ */ new Map();
1852
1875
  return async (provider) => {
1853
1876
  if (useCache) {
1854
- const hit = cache2.get(provider);
1877
+ const hit = cache3.get(provider);
1855
1878
  if (hit !== void 0) return hit;
1856
1879
  }
1857
1880
  try {
1858
1881
  const key = await db.secrets.get(nameFor(provider));
1859
- if (useCache) cache2.set(provider, key);
1882
+ if (useCache) cache3.set(provider, key);
1860
1883
  return key;
1861
1884
  } catch {
1862
1885
  return void 0;
@@ -1864,6 +1887,52 @@ function odlaDbKeyResolver(db, opts = {}) {
1864
1887
  };
1865
1888
  }
1866
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
+
1867
1936
  // src/store/provision.ts
1868
1937
  init_shape();
1869
1938
  async function post(ctx, path, body) {