@odla-ai/cli 0.27.15 → 0.27.17

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
@@ -344,6 +344,7 @@ npx odla-ai whoami
344
344
  npx odla-ai setup
345
345
  npx odla-ai init --app-id my-app --name "My App"
346
346
  npx odla-ai doctor
347
+ npx odla-ai ai models --env dev # exact models this app may select; no admin grant
347
348
  npx odla-ai config diff --json
348
349
  npx odla-ai config plan --json > .odla/config-plan.json
349
350
  npx odla-ai config apply --plan .odla/config-plan.json --json
@@ -753,7 +754,10 @@ otherwise. It never follows an arbitrary shell working directory. Studio display
753
754
  exact scope, only an admin can approve it, and it expires after ~15 minutes;
754
755
  policy, credential, and usage capabilities are separate and cannot act as the
755
756
  approver on ordinary app/db/o11y routes. `admin ai models` reads the server
756
- catalog. `admin ai set security` updates discovery and independent validation
757
+ catalog. For app code and provisioning, `ai models [--env dev] [--json]` reads
758
+ the app-safe public configuration and lists only the hosted models that exact
759
+ app may select (or the configured provider's local catalog in BYOK mode), with
760
+ the default and generic capabilities. `admin ai set security` updates discovery and independent validation
757
761
  together with expected revisions; a concurrent edit returns 409 and reloads
758
762
  instead of being overwritten. `admin ai usage` requests its own read-only
759
763
  capability and lists metadata-only events and status aggregates; narrow it with
package/dist/bin.cjs CHANGED
@@ -3035,6 +3035,71 @@ var init_capabilities = __esm({
3035
3035
  }
3036
3036
  });
3037
3037
 
3038
+ // src/ai-models.ts
3039
+ async function aiModels(options = {}) {
3040
+ const cfg = await loadProjectConfig(options.configPath ?? "odla.config.mjs");
3041
+ const env = options.env ?? cfg.envs[0] ?? "dev";
3042
+ if (!cfg.envs.includes(env)) throw new Error(`ai models env "${env}" is not declared in config envs`);
3043
+ const url = new URL(`/registry/apps/${encodeURIComponent(cfg.app.id)}/public-config`, cfg.platformUrl);
3044
+ url.searchParams.set("env", env);
3045
+ const response2 = await (options.fetch ?? fetch)(url);
3046
+ if (!response2.ok) throw new Error(`read app AI models failed (${response2.status}): ${await safeText4(response2)}`);
3047
+ const body = await response2.json();
3048
+ if (!body.ai) throw new Error(`ai is not configured for ${cfg.app.id}/${env}`);
3049
+ const mode = body.ai.mode === "hosted" ? "hosted" : "byok";
3050
+ const defaultModel = typeof body.ai.model === "string" ? body.ai.model : void 0;
3051
+ let models;
3052
+ if (mode === "hosted") {
3053
+ if (body.ai.enabled !== true) throw new Error(`hosted ai is disabled for ${cfg.app.id}/${env}`);
3054
+ if (!Array.isArray(body.ai.models) || !body.ai.models.every(isModelSpec)) {
3055
+ throw new Error("platform returned an invalid hosted AI model catalog");
3056
+ }
3057
+ models = body.ai.models;
3058
+ } else {
3059
+ const provider = typeof body.ai.provider === "string" ? body.ai.provider : cfg.ai?.provider;
3060
+ if (!provider) throw new Error(`BYOK ai has no provider for ${cfg.app.id}/${env}`);
3061
+ models = Object.values(import_ai.DEFAULT_CATALOG).filter((model) => model.provider === provider);
3062
+ }
3063
+ if (options.provider) models = models.filter((model) => model.provider === options.provider);
3064
+ models.sort((a, b) => a.provider.localeCompare(b.provider) || a.id.localeCompare(b.id));
3065
+ const out = options.stdout ?? console;
3066
+ if (options.json) {
3067
+ out.log(JSON.stringify({ appId: cfg.app.id, env, mode, defaultModel: defaultModel ?? null, models }, null, 2));
3068
+ return;
3069
+ }
3070
+ out.log("provider model default capabilities");
3071
+ for (const model of models) {
3072
+ out.log([model.provider, model.id, model.id === defaultModel ? "yes" : "", capabilityList(model)].join(" "));
3073
+ }
3074
+ }
3075
+ function capabilityList(model) {
3076
+ return [
3077
+ model.capabilities.imageIn ? "image" : "",
3078
+ model.capabilities.audioIn ? "audio" : "",
3079
+ model.capabilities.documentIn ? "document" : "",
3080
+ model.capabilities.toolUse ? "tools" : "",
3081
+ model.capabilities.thinking || model.capabilities.effort ? "reasoning" : "",
3082
+ model.capabilities.webSearch || model.superpowers?.webSearch ? "web-search" : ""
3083
+ ].filter(Boolean).join(",");
3084
+ }
3085
+ function isModelSpec(value2) {
3086
+ if (!value2 || typeof value2 !== "object" || Array.isArray(value2)) return false;
3087
+ const model = value2;
3088
+ return typeof model.id === "string" && typeof model.nativeId === "string" && (model.provider === "anthropic" || model.provider === "openai" || model.provider === "google") && Boolean(model.capabilities) && typeof model.capabilities === "object";
3089
+ }
3090
+ async function safeText4(response2) {
3091
+ return (await response2.text().catch(() => "request failed")).slice(0, 300);
3092
+ }
3093
+ var import_ai;
3094
+ var init_ai_models = __esm({
3095
+ "src/ai-models.ts"() {
3096
+ "use strict";
3097
+ init_cjs_shims();
3098
+ import_ai = require("@odla-ai/ai");
3099
+ init_config();
3100
+ }
3101
+ });
3102
+
3038
3103
  // src/config-operation-error.ts
3039
3104
  var ConfigOperationCommandError;
3040
3105
  var init_config_operation_error = __esm({
@@ -3223,7 +3288,7 @@ var init_config_operation_validate = __esm({
3223
3288
 
3224
3289
  // src/provision-helpers.ts
3225
3290
  function defaultSecretName(provider) {
3226
- const names = import_ai.DEFAULT_SECRET_NAMES;
3291
+ const names = import_ai2.DEFAULT_SECRET_NAMES;
3227
3292
  return names[provider] ?? `${provider}_api_key`;
3228
3293
  }
3229
3294
  async function assertTenantAdminAccess(doFetch, cfg, env, token) {
@@ -3233,7 +3298,7 @@ async function assertTenantAdminAccess(doFetch, cfg, env, token) {
3233
3298
  });
3234
3299
  if (res.ok || res.status === 404) return;
3235
3300
  if (res.status === 403) {
3236
- const detail = await safeText4(res);
3301
+ const detail = await safeText5(res);
3237
3302
  const code = errorCode(detail);
3238
3303
  if (code === "human_session_required") {
3239
3304
  throw new Error(
@@ -3249,7 +3314,7 @@ async function assertTenantAdminAccess(doFetch, cfg, env, token) {
3249
3314
  `${env}: this credential lacks live app.manage authority for "${cfg.app.id}" (tenant ${tenantId}) \u2014 nothing was minted or written; run "odla-ai provision --request-grant --email <odla-account>" to open a fresh owner review. If the human account is not an owner, an existing owner must add it in signed-in Studio; an agent token cannot repair ownership`
3250
3315
  );
3251
3316
  }
3252
- throw new Error(`${env}: tenant access preflight (${tenantId}) failed: ${res.status} ${await safeText4(res)}`);
3317
+ throw new Error(`${env}: tenant access preflight (${tenantId}) failed: ${res.status} ${await safeText5(res)}`);
3253
3318
  }
3254
3319
  function errorCode(text2) {
3255
3320
  try {
@@ -3265,7 +3330,7 @@ async function postJson(doFetch, url, bearer, body) {
3265
3330
  headers: { authorization: `Bearer ${bearer}`, "content-type": "application/json" },
3266
3331
  body: JSON.stringify(body)
3267
3332
  });
3268
- if (!res.ok) throw new Error(`${new URL(url).pathname} failed: ${res.status} ${await safeText4(res)}`);
3333
+ if (!res.ok) throw new Error(`${new URL(url).pathname} failed: ${res.status} ${await safeText5(res)}`);
3269
3334
  }
3270
3335
  function normalizeClerkConfig(value2) {
3271
3336
  if (!value2) return null;
@@ -3278,19 +3343,19 @@ function normalizeClerkConfig(value2) {
3278
3343
  const publishableKey = envValue(cfg.publishableKey);
3279
3344
  return publishableKey ? { publishableKey, ...cfg.audience ? { audience: cfg.audience } : {}, ...cfg.mode ? { mode: cfg.mode } : {} } : null;
3280
3345
  }
3281
- async function safeText4(res) {
3346
+ async function safeText5(res) {
3282
3347
  try {
3283
3348
  return redactSecrets((await res.text()).slice(0, 500));
3284
3349
  } catch {
3285
3350
  return "";
3286
3351
  }
3287
3352
  }
3288
- var import_ai, import_apps4;
3353
+ var import_ai2, import_apps4;
3289
3354
  var init_provision_helpers = __esm({
3290
3355
  "src/provision-helpers.ts"() {
3291
3356
  "use strict";
3292
3357
  init_cjs_shims();
3293
- import_ai = require("@odla-ai/ai");
3358
+ import_ai2 = require("@odla-ai/ai");
3294
3359
  import_apps4 = require("@odla-ai/apps");
3295
3360
  init_config();
3296
3361
  init_redact();
@@ -4745,7 +4810,7 @@ async function secretsSet(options) {
4745
4810
  optionalProjectCapabilities: ["app.manage"]
4746
4811
  });
4747
4812
  try {
4748
- await (0, import_ai2.putSecret)({ endpoint: cfg.dbEndpoint, token, fetch: doFetch }, tenantId, name, value2);
4813
+ await (0, import_ai3.putSecret)({ endpoint: cfg.dbEndpoint, token, fetch: doFetch }, tenantId, name, value2);
4749
4814
  } catch (err) {
4750
4815
  throw new Error(scrubValue(err instanceof Error ? err.message : String(err), value2));
4751
4816
  }
@@ -4790,12 +4855,12 @@ async function resolveVaultWrite(options) {
4790
4855
  const value2 = await secretInputValue(options, "secret");
4791
4856
  return { cfg, tenantId: (0, import_apps10.tenantIdFor)(cfg.app.id, options.env), value: value2, doFetch, out };
4792
4857
  }
4793
- var import_ai2, import_apps10, PROD_ENV_NAMES2;
4858
+ var import_ai3, import_apps10, PROD_ENV_NAMES2;
4794
4859
  var init_secrets_set = __esm({
4795
4860
  "src/secrets-set.ts"() {
4796
4861
  "use strict";
4797
4862
  init_cjs_shims();
4798
- import_ai2 = require("@odla-ai/ai");
4863
+ import_ai3 = require("@odla-ai/ai");
4799
4864
  import_apps10 = require("@odla-ai/apps");
4800
4865
  init_config();
4801
4866
  init_redact();
@@ -5207,7 +5272,7 @@ async function getJson(doFetch, url, bearer) {
5207
5272
  const res = await doFetch(url, {
5208
5273
  headers: bearer ? { authorization: `Bearer ${bearer}` } : void 0
5209
5274
  });
5210
- if (!res.ok) throw new Error(`${new URL(url).pathname} returned ${res.status}: ${await safeText5(res)}`);
5275
+ if (!res.ok) throw new Error(`${new URL(url).pathname} returned ${res.status}: ${await safeText6(res)}`);
5211
5276
  return res.json();
5212
5277
  }
5213
5278
  async function postJson2(doFetch, url, bearer, body) {
@@ -5216,7 +5281,7 @@ async function postJson2(doFetch, url, bearer, body) {
5216
5281
  headers: { authorization: `Bearer ${bearer}`, "content-type": "application/json" },
5217
5282
  body: JSON.stringify(body)
5218
5283
  });
5219
- if (!res.ok) throw new Error(`${new URL(url).pathname} returned ${res.status}: ${await safeText5(res)}`);
5284
+ if (!res.ok) throw new Error(`${new URL(url).pathname} returned ${res.status}: ${await safeText6(res)}`);
5220
5285
  return res.json();
5221
5286
  }
5222
5287
  function publicConfigUrl(platformUrl, appId, env) {
@@ -5224,7 +5289,7 @@ function publicConfigUrl(platformUrl, appId, env) {
5224
5289
  url.searchParams.set("env", env);
5225
5290
  return url.toString();
5226
5291
  }
5227
- async function safeText5(res) {
5292
+ async function safeText6(res) {
5228
5293
  try {
5229
5294
  return redactSecrets((await res.text()).slice(0, 500));
5230
5295
  } catch {
@@ -5291,6 +5356,20 @@ async function secretsCommand(parsed, deps) {
5291
5356
  });
5292
5357
  }
5293
5358
  async function projectCommand(command, parsed, deps) {
5359
+ if (command === "ai") {
5360
+ const sub = parsed.positionals[1];
5361
+ if (sub !== "models") throw new Error(`unknown ai subcommand "${sub ?? ""}". Try "odla-ai ai models --env dev".`);
5362
+ assertArgs(parsed, ["config", "env", "provider", "json"], 2);
5363
+ await aiModels({
5364
+ configPath: stringOpt(parsed.options.config) ?? "odla.config.mjs",
5365
+ env: stringOpt(parsed.options.env),
5366
+ provider: stringOpt(parsed.options.provider),
5367
+ json: parsed.options.json === true,
5368
+ fetch: deps.fetch,
5369
+ stdout: deps.stdout
5370
+ });
5371
+ return true;
5372
+ }
5294
5373
  if (command === "config") {
5295
5374
  const sub = parsed.positionals[1];
5296
5375
  if (sub !== "diff" && sub !== "plan" && sub !== "apply") {
@@ -5413,6 +5492,7 @@ var init_cli_project = __esm({
5413
5492
  init_cjs_shims();
5414
5493
  init_argv();
5415
5494
  init_capabilities();
5495
+ init_ai_models();
5416
5496
  init_config_operation_command();
5417
5497
  init_config_reconcile_command();
5418
5498
  init_doctor();
@@ -9208,6 +9288,7 @@ Usage:
9208
9288
  odla-ai setup [--dir <project>] [--agent <name>] [--global] [--force]
9209
9289
  odla-ai init --app-id <id> --name <name> [--services db,ai,o11y,calendar] [--env dev --env prod] [--ai-provider <byok-provider>]
9210
9290
  odla-ai doctor [--config odla.config.mjs]
9291
+ odla-ai ai models [--config odla.config.mjs] [--env dev] [--provider <id>] [--json]
9211
9292
  odla-ai config <diff|plan> [--config odla.config.mjs] [--email <odla-account>] [--json]
9212
9293
  odla-ai config apply --plan <plan.json> [--idempotency-key <key>] [--email <odla-account>] [--json]
9213
9294
  odla-ai operations get <operation-id> [--json]
@@ -11468,14 +11549,14 @@ async function mintDbKey(opts, tenantId) {
11468
11549
  appId: tenantId
11469
11550
  })
11470
11551
  });
11471
- if (!created.ok) throw new Error(`db app create (${tenantId}) failed: ${created.status} ${await safeText6(created)}`);
11552
+ if (!created.ok) throw new Error(`db app create (${tenantId}) failed: ${created.status} ${await safeText7(created)}`);
11472
11553
  res = await opts.fetch(`${opts.cfg.dbEndpoint}/admin/apps/${encodeURIComponent(tenantId)}/keys`, {
11473
11554
  method: "POST",
11474
11555
  headers,
11475
11556
  body: "{}"
11476
11557
  });
11477
11558
  }
11478
- if (!res.ok) throw new Error(`db key mint (${tenantId}) failed: ${res.status} ${await safeText6(res)}`);
11559
+ if (!res.ok) throw new Error(`db key mint (${tenantId}) failed: ${res.status} ${await safeText7(res)}`);
11479
11560
  const body = await res.json();
11480
11561
  if (!body.key) throw new Error(`db key mint (${tenantId}) returned no key`);
11481
11562
  return body.key;
@@ -11491,12 +11572,12 @@ async function issueO11yToken(opts) {
11491
11572
  `o11y token already exists for env "${opts.env}", but its shown-once value is not in the local credentials file; run "odla-ai provision --rotate-o11y-token --push-secrets" to replace it explicitly`
11492
11573
  );
11493
11574
  }
11494
- if (!res.ok) throw new Error(`o11y token ${opts.rotateO11y ? "rotation" : "issue"} (${opts.env}) failed: ${res.status} ${await safeText6(res)}`);
11575
+ if (!res.ok) throw new Error(`o11y token ${opts.rotateO11y ? "rotation" : "issue"} (${opts.env}) failed: ${res.status} ${await safeText7(res)}`);
11495
11576
  const body = await res.json();
11496
11577
  if (!body.token) throw new Error(`o11y token ${opts.rotateO11y ? "rotation" : "issue"} (${opts.env}) returned no token`);
11497
11578
  return body.token;
11498
11579
  }
11499
- async function safeText6(res) {
11580
+ async function safeText7(res) {
11500
11581
  try {
11501
11582
  return redactSecrets((await res.text()).slice(0, 500));
11502
11583
  } catch {
@@ -11688,7 +11769,7 @@ ${env}: credentials are already saved; retry "odla-ai secrets push --env ${env}$
11688
11769
  const key = import_node_process11.default.env[cfg.ai.keyEnv];
11689
11770
  if (key) {
11690
11771
  const secretName = cfg.ai.secretName ?? defaultSecretName(cfg.ai.provider);
11691
- await (0, import_ai3.putSecret)({ endpoint: cfg.dbEndpoint, token, fetch: doFetch }, tenantId, secretName, key);
11772
+ await (0, import_ai4.putSecret)({ endpoint: cfg.dbEndpoint, token, fetch: doFetch }, tenantId, secretName, key);
11692
11773
  out.log(`${env}: ${cfg.ai.provider} key stored in vault (${secretName})`);
11693
11774
  } else {
11694
11775
  out.log(`${env}: ${cfg.ai.keyEnv} not set; skipped provider key storage`);
@@ -11724,13 +11805,13 @@ ${env}: credentials are already saved; retry "odla-ai secrets push --env ${env}$
11724
11805
  }
11725
11806
  }
11726
11807
  }
11727
- var import_apps12, import_ai3, import_node_process11;
11808
+ var import_apps12, import_ai4, import_node_process11;
11728
11809
  var init_provision = __esm({
11729
11810
  "src/provision.ts"() {
11730
11811
  "use strict";
11731
11812
  init_cjs_shims();
11732
11813
  import_apps12 = require("@odla-ai/apps");
11733
- import_ai3 = require("@odla-ai/ai");
11814
+ import_ai4 = require("@odla-ai/ai");
11734
11815
  import_node_process11 = __toESM(require("process"), 1);
11735
11816
  init_config();
11736
11817
  init_calendar();
@@ -11819,6 +11900,7 @@ var init_surface = __esm({
11819
11900
  };
11820
11901
  COMMAND_SURFACE = {
11821
11902
  agent: { jobs: {}, retry: {} },
11903
+ ai: { models: {} },
11822
11904
  admin: {
11823
11905
  ai: {
11824
11906
  show: {},