@odla-ai/cli 0.27.4 → 0.27.5

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/bin.js CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  exitCodeFor,
4
4
  redactSecrets,
5
5
  runCli
6
- } from "./chunk-GENTB3VO.js";
6
+ } from "./chunk-DQOJ4S6H.js";
7
7
 
8
8
  // src/bin.ts
9
9
  runCli().catch((err) => {
@@ -894,6 +894,32 @@ import { dirname as dirname3, isAbsolute as isAbsolute2, resolve as resolve2 } f
894
894
  import { pathToFileURL } from "url";
895
895
  import { appServiceDefinition, appServiceIds } from "@odla-ai/apps";
896
896
 
897
+ // src/ai-config-validation.ts
898
+ function validateAiConfig(cfg, path) {
899
+ if (cfg.ai === void 0) return;
900
+ if (!isRecord4(cfg.ai)) throw new Error(`${path}: ai must be an object`);
901
+ assertOnly(cfg.ai, ["mode", "provider", "model", "keyEnv", "secretName"], `${path}: ai`);
902
+ if (cfg.ai.mode !== void 0 && cfg.ai.mode !== "hosted" && cfg.ai.mode !== "byok") {
903
+ throw new Error(`${path}: ai.mode must be hosted or byok`);
904
+ }
905
+ if (cfg.ai.mode === "byok" && !safeText(cfg.ai.provider, 100)) {
906
+ throw new Error(`${path}: ai.provider is required when ai.mode is byok`);
907
+ }
908
+ if (cfg.ai.mode === "hosted" && (cfg.ai.provider || cfg.ai.keyEnv || cfg.ai.secretName)) {
909
+ throw new Error(`${path}: hosted ai cannot configure a provider, keyEnv, or secretName`);
910
+ }
911
+ }
912
+ function assertOnly(value2, allowed, label) {
913
+ const extra = Object.keys(value2).find((key) => !allowed.includes(key));
914
+ if (extra) throw new Error(`${label}.${extra} is not supported`);
915
+ }
916
+ function isRecord4(value2) {
917
+ return value2 !== null && typeof value2 === "object" && !Array.isArray(value2);
918
+ }
919
+ function safeText(value2, max) {
920
+ return typeof value2 === "string" && value2.trim().length > 0 && value2.length <= max && !/[\u0000-\u001f\u007f]/.test(value2);
921
+ }
922
+
897
923
  // src/integration-validation.ts
898
924
  function validateIntegrations(cfg, path, defaultServices) {
899
925
  if (cfg.integrations === void 0) return;
@@ -901,16 +927,16 @@ function validateIntegrations(cfg, path, defaultServices) {
901
927
  const ids = /* @__PURE__ */ new Set();
902
928
  for (const [index, integration] of cfg.integrations.entries()) {
903
929
  const at = `${path}: integrations[${index}]`;
904
- if (!isRecord4(integration)) throw new Error(`${at} must be an object`);
930
+ if (!isRecord5(integration)) throw new Error(`${at} must be an object`);
905
931
  if (!validId(integration.id)) throw new Error(`${at}.id must be lowercase letters, numbers, and hyphens`);
906
932
  if (ids.has(integration.id)) throw new Error(`${path}: duplicate integration id "${integration.id}"`);
907
933
  ids.add(integration.id);
908
- if (!safeText(integration.title, 200)) throw new Error(`${at}.title is required`);
909
- if (!safeText(integration.npm, 200)) throw new Error(`${at}.npm is required`);
910
- if (integration.schema !== void 0 && (!isRecord4(integration.schema) || !isRecord4(integration.schema.entities))) {
934
+ if (!safeText2(integration.title, 200)) throw new Error(`${at}.title is required`);
935
+ if (!safeText2(integration.npm, 200)) throw new Error(`${at}.npm is required`);
936
+ if (integration.schema !== void 0 && (!isRecord5(integration.schema) || !isRecord5(integration.schema.entities))) {
911
937
  throw new Error(`${at}.schema must contain an entities object`);
912
938
  }
913
- if (integration.rules !== void 0 && !isRecord4(integration.rules)) throw new Error(`${at}.rules must be an object`);
939
+ if (integration.rules !== void 0 && !isRecord5(integration.rules)) throw new Error(`${at}.rules must be an object`);
914
940
  validateSeeds(integration, at);
915
941
  validateProbes(integration, at);
916
942
  }
@@ -924,13 +950,13 @@ function validateSeeds(integration, at) {
924
950
  const ids = /* @__PURE__ */ new Set();
925
951
  for (const [index, seed] of integration.seeds.entries()) {
926
952
  const sat = `${at}.seeds[${index}]`;
927
- if (!isRecord4(seed) || !safeText(seed.id, 200) || !safeText(seed.ns, 200)) throw new Error(`${sat} requires id and ns`);
953
+ if (!isRecord5(seed) || !safeText2(seed.id, 200) || !safeText2(seed.ns, 200)) throw new Error(`${sat} requires id and ns`);
928
954
  if (ids.has(seed.id)) throw new Error(`${at} has duplicate seed id "${seed.id}"`);
929
955
  ids.add(seed.id);
930
- if (!isRecord4(seed.key) || !safeText(seed.key.attr, 200) || !safeText(seed.key.value, 2048)) {
956
+ if (!isRecord5(seed.key) || !safeText2(seed.key.attr, 200) || !safeText2(seed.key.value, 2048)) {
931
957
  throw new Error(`${sat}.key requires string attr and value`);
932
958
  }
933
- if (!isRecord4(seed.attrs)) throw new Error(`${sat}.attrs must be an object`);
959
+ if (!isRecord5(seed.attrs)) throw new Error(`${sat}.attrs must be an object`);
934
960
  if (Object.hasOwn(seed.attrs, seed.key.attr) && seed.attrs[seed.key.attr] !== seed.key.value) {
935
961
  throw new Error(`${sat}.attrs.${seed.key.attr} conflicts with its natural key`);
936
962
  }
@@ -941,16 +967,16 @@ function validateProbes(integration, at) {
941
967
  if (!Array.isArray(integration.probes)) throw new Error(`${at}.probes must be an array`);
942
968
  for (const [index, probe] of integration.probes.entries()) {
943
969
  const pat = `${at}.probes[${index}]`;
944
- if (!isRecord4(probe) || !safeProbePath(probe.path)) throw new Error(`${pat}.path must be an absolute path without query or fragment`);
970
+ if (!isRecord5(probe) || !safeProbePath(probe.path)) throw new Error(`${pat}.path must be an absolute path without query or fragment`);
945
971
  if (!Number.isInteger(probe.expectedStatus) || probe.expectedStatus < 100 || probe.expectedStatus > 599) {
946
972
  throw new Error(`${pat}.expectedStatus must be an HTTP status`);
947
973
  }
948
974
  }
949
975
  }
950
- function isRecord4(value2) {
976
+ function isRecord5(value2) {
951
977
  return value2 !== null && typeof value2 === "object" && !Array.isArray(value2);
952
978
  }
953
- function safeText(value2, max) {
979
+ function safeText2(value2, max) {
954
980
  return typeof value2 === "string" && value2.trim().length > 0 && value2.length <= max && !/[\u0000-\u001f\u007f]/.test(value2);
955
981
  }
956
982
  function safeProbePath(value2) {
@@ -1080,6 +1106,7 @@ function validateRawConfig(raw, path) {
1080
1106
  if (cfg.services !== void 0 && (!Array.isArray(cfg.services) || cfg.services.some((service) => typeof service !== "string" || !service.trim()))) {
1081
1107
  throw new Error(`${path}: services must be an array of non-empty names`);
1082
1108
  }
1109
+ validateAiConfig(cfg, path);
1083
1110
  validateIntegrations(cfg, path, DEFAULT_SERVICES);
1084
1111
  }
1085
1112
  function validateCalendarConfig(cfg, envs, services, path) {
@@ -1088,11 +1115,11 @@ function validateCalendarConfig(cfg, envs, services, path) {
1088
1115
  if (enabled) throw new Error(`${path}: calendar.google is required when services includes "calendar"`);
1089
1116
  return;
1090
1117
  }
1091
- if (!isRecord5(cfg.calendar)) throw new Error(`${path}: calendar must be an object`);
1092
- assertOnly(cfg.calendar, ["google"], `${path}: calendar`);
1093
- if (!isRecord5(cfg.calendar.google)) throw new Error(`${path}: calendar.google must be an object`);
1118
+ if (!isRecord6(cfg.calendar)) throw new Error(`${path}: calendar must be an object`);
1119
+ assertOnly2(cfg.calendar, ["google"], `${path}: calendar`);
1120
+ if (!isRecord6(cfg.calendar.google)) throw new Error(`${path}: calendar.google must be an object`);
1094
1121
  const google = cfg.calendar.google;
1095
- assertOnly(
1122
+ assertOnly2(
1096
1123
  google,
1097
1124
  ["availabilityCalendars", "calendars", "bookingCalendar", "bookingPageUrl"],
1098
1125
  `${path}: calendar.google`
@@ -1102,7 +1129,7 @@ function validateCalendarConfig(cfg, envs, services, path) {
1102
1129
  throw new Error(`${path}: calendar.google requires exactly one of availabilityCalendars or calendars (legacy)`);
1103
1130
  }
1104
1131
  const availability = google[availabilityKey];
1105
- if (!isRecord5(availability)) throw new Error(`${path}: calendar.google.${availabilityKey} must map env names to calendar ids`);
1132
+ if (!isRecord6(availability)) throw new Error(`${path}: calendar.google.${availabilityKey} must map env names to calendar ids`);
1106
1133
  const unknownEnv = Object.keys(availability).find((env) => !envs.includes(env));
1107
1134
  if (unknownEnv) throw new Error(`${path}: calendar.google.${availabilityKey}.${unknownEnv} is not in config envs`);
1108
1135
  for (const env of envs) {
@@ -1113,22 +1140,22 @@ function validateCalendarConfig(cfg, envs, services, path) {
1113
1140
  if (ids.length > 10) {
1114
1141
  throw new Error(`${path}: calendar.google.${availabilityKey}.${env} must contain at most 10 calendar ids`);
1115
1142
  }
1116
- if (ids.some((id) => !safeText2(id, 1024))) {
1143
+ if (ids.some((id) => !safeText3(id, 1024))) {
1117
1144
  throw new Error(`${path}: calendar.google.${availabilityKey}.${env} contains an invalid calendar id`);
1118
1145
  }
1119
1146
  }
1120
1147
  if (google.bookingCalendar !== void 0) {
1121
- if (!isRecord5(google.bookingCalendar)) throw new Error(`${path}: calendar.google.bookingCalendar must map env names to one calendar id`);
1148
+ if (!isRecord6(google.bookingCalendar)) throw new Error(`${path}: calendar.google.bookingCalendar must map env names to one calendar id`);
1122
1149
  const unknownBookingEnv = Object.keys(google.bookingCalendar).find((env) => !envs.includes(env));
1123
1150
  if (unknownBookingEnv) throw new Error(`${path}: calendar.google.bookingCalendar.${unknownBookingEnv} is not in config envs`);
1124
1151
  for (const [env, value2] of Object.entries(google.bookingCalendar)) {
1125
- if (!safeText2(value2, 1024)) {
1152
+ if (!safeText3(value2, 1024)) {
1126
1153
  throw new Error(`${path}: calendar.google.bookingCalendar.${env} must be a calendar id`);
1127
1154
  }
1128
1155
  }
1129
1156
  }
1130
1157
  if (google.bookingPageUrl !== void 0) {
1131
- if (!isRecord5(google.bookingPageUrl)) throw new Error(`${path}: calendar.google.bookingPageUrl must map env names to HTTPS URLs or null`);
1158
+ if (!isRecord6(google.bookingPageUrl)) throw new Error(`${path}: calendar.google.bookingPageUrl must map env names to HTTPS URLs or null`);
1132
1159
  const unknownBookingEnv = Object.keys(google.bookingPageUrl).find((env) => !envs.includes(env));
1133
1160
  if (unknownBookingEnv) throw new Error(`${path}: calendar.google.bookingPageUrl.${unknownBookingEnv} is not in config envs`);
1134
1161
  for (const [env, value2] of Object.entries(google.bookingPageUrl)) {
@@ -1151,14 +1178,14 @@ function validateServices(services, path) {
1151
1178
  }
1152
1179
  }
1153
1180
  }
1154
- function assertOnly(value2, allowed, label) {
1181
+ function assertOnly2(value2, allowed, label) {
1155
1182
  const extra = Object.keys(value2).find((key) => !allowed.includes(key));
1156
1183
  if (extra) throw new Error(`${label}.${extra} is not supported`);
1157
1184
  }
1158
- function isRecord5(value2) {
1185
+ function isRecord6(value2) {
1159
1186
  return value2 !== null && typeof value2 === "object" && !Array.isArray(value2);
1160
1187
  }
1161
- function safeText2(value2, max) {
1188
+ function safeText3(value2, max) {
1162
1189
  return typeof value2 === "string" && value2.trim().length > 0 && value2.length <= max && !/[\u0000-\u001f\u007f]/.test(value2);
1163
1190
  }
1164
1191
  function safeHttpsUrl(value2) {
@@ -1745,7 +1772,7 @@ async function assertTenantAdminAccess(doFetch, cfg, env, token) {
1745
1772
  `${env}: you are not an owner of "${cfg.app.id}" (tenant ${tenantId}) \u2014 nothing was minted or written; ask an existing owner to run "odla-ai app owners add <your-email>", then re-run provision`
1746
1773
  );
1747
1774
  }
1748
- throw new Error(`${env}: tenant access preflight (${tenantId}) failed: ${res.status} ${await safeText3(res)}`);
1775
+ throw new Error(`${env}: tenant access preflight (${tenantId}) failed: ${res.status} ${await safeText4(res)}`);
1749
1776
  }
1750
1777
  async function postJson(doFetch, url, bearer, body) {
1751
1778
  const res = await doFetch(url, {
@@ -1753,7 +1780,7 @@ async function postJson(doFetch, url, bearer, body) {
1753
1780
  headers: { authorization: `Bearer ${bearer}`, "content-type": "application/json" },
1754
1781
  body: JSON.stringify(body)
1755
1782
  });
1756
- if (!res.ok) throw new Error(`${new URL(url).pathname} failed: ${res.status} ${await safeText3(res)}`);
1783
+ if (!res.ok) throw new Error(`${new URL(url).pathname} failed: ${res.status} ${await safeText4(res)}`);
1757
1784
  }
1758
1785
  function normalizeClerkConfig(value2) {
1759
1786
  if (!value2) return null;
@@ -1766,7 +1793,7 @@ function normalizeClerkConfig(value2) {
1766
1793
  const publishableKey = envValue(cfg.publishableKey);
1767
1794
  return publishableKey ? { publishableKey, ...cfg.audience ? { audience: cfg.audience } : {}, ...cfg.mode ? { mode: cfg.mode } : {} } : null;
1768
1795
  }
1769
- async function safeText3(res) {
1796
+ async function safeText4(res) {
1770
1797
  try {
1771
1798
  return redactSecrets((await res.text()).slice(0, 500));
1772
1799
  } catch {
@@ -2917,18 +2944,18 @@ function assertSeedContracts(integrations, schema) {
2917
2944
  }
2918
2945
  }
2919
2946
  function isUniqueAttr(schema, ns, attr) {
2920
- if (!isRecord6(schema) || !isRecord6(schema.entities)) return false;
2947
+ if (!isRecord7(schema) || !isRecord7(schema.entities)) return false;
2921
2948
  const entity = schema.entities[ns];
2922
- if (!isRecord6(entity) || !isRecord6(entity.attrs)) return false;
2949
+ if (!isRecord7(entity) || !isRecord7(entity.attrs)) return false;
2923
2950
  const definition = entity.attrs[attr];
2924
- return isRecord6(definition) && definition.unique === true;
2951
+ return isRecord7(definition) && definition.unique === true;
2925
2952
  }
2926
2953
  function normalizeSchema(value2) {
2927
2954
  if (value2 === void 0 || value2 === null) return { entities: {}, links: {} };
2928
- if (!isRecord6(value2) || !isRecord6(value2.entities)) {
2955
+ if (!isRecord7(value2) || !isRecord7(value2.entities)) {
2929
2956
  throw new Error("db schema must be a serialized schema object with an entities map");
2930
2957
  }
2931
- if (value2.links !== void 0 && !isRecord6(value2.links)) throw new Error("db schema links must be an object");
2958
+ if (value2.links !== void 0 && !isRecord7(value2.links)) throw new Error("db schema links must be an object");
2932
2959
  return {
2933
2960
  entities: { ...value2.entities },
2934
2961
  links: { ...value2.links ?? {} }
@@ -2942,7 +2969,7 @@ function mergeMap(target, fragment, label) {
2942
2969
  target[name] = value2;
2943
2970
  }
2944
2971
  }
2945
- function isRecord6(value2) {
2972
+ function isRecord7(value2) {
2946
2973
  return value2 !== null && typeof value2 === "object" && !Array.isArray(value2);
2947
2974
  }
2948
2975
 
@@ -2961,7 +2988,7 @@ async function doctor(options) {
2961
2988
  out.log(`integrations: ${plan.integrations.length ? plan.integrations.join(", ") : "none"}`);
2962
2989
  out.log(`schema: ${schema ? `${entities.length} entities` : "none"}`);
2963
2990
  out.log(`rules: ${rules ? `${Object.keys(rules).length} namespaces` : "none"}`);
2964
- out.log(`ai: ${cfg.services.includes("ai") ? cfg.ai?.provider ?? "not configured" : "not enabled"}`);
2991
+ out.log(`ai: ${cfg.services.includes("ai") ? cfg.ai?.provider ? `byok/${cfg.ai.provider}` : "hosted" : "not enabled"}`);
2965
2992
  if (cfg.services.includes("calendar")) {
2966
2993
  const calendar = cfg.envs.map((env) => {
2967
2994
  const resolved = calendarServiceConfig(cfg, env);
@@ -2982,7 +3009,9 @@ async function doctor(options) {
2982
3009
  }
2983
3010
  }
2984
3011
  warnings.push(...integrationWarnings(database.integrations, schema, rules));
2985
- if (cfg.services.includes("ai") && !cfg.ai?.provider) warnings.push("ai service is enabled but ai.provider is not set");
3012
+ if (cfg.services.includes("ai") && cfg.ai?.mode === "byok" && !cfg.ai.provider) {
3013
+ warnings.push("ai.mode is byok but ai.provider is not set");
3014
+ }
2986
3015
  if (cfg.auth?.clerk) {
2987
3016
  for (const [env, value2] of Object.entries(cfg.auth.clerk)) {
2988
3017
  if (typeof value2 === "string" && value2.startsWith("$") && !process.env[value2.slice(1)]) {
@@ -3043,7 +3072,7 @@ function initProject(options) {
3043
3072
  if (!services.includes(dependency)) throw new Error(`--services ${service} requires ${dependency}`);
3044
3073
  }
3045
3074
  }
3046
- const aiProvider = options.aiProvider ?? "anthropic";
3075
+ const aiProvider = options.aiProvider;
3047
3076
  mkdirSync2(dirname4(configPath), { recursive: true });
3048
3077
  mkdirSync2(resolve4(rootDir, "src/odla"), { recursive: true });
3049
3078
  mkdirSync2(resolve4(rootDir, ".odla"), { recursive: true });
@@ -3070,6 +3099,15 @@ function configTemplate(input) {
3070
3099
  },
3071
3100
  },
3072
3101
  ` : "";
3102
+ const ai = input.aiProvider ? ` ai: {
3103
+ mode: "byok",
3104
+ provider: process.env.ODLA_AI_PROVIDER ?? "${input.aiProvider}",
3105
+ // Optional: set this env var while running provision to store the provider
3106
+ // key in the app vault for each tenant.
3107
+ keyEnv: "${defaultKeyEnv(input.aiProvider)}",
3108
+ },` : ` // Hosted AI uses admin-approved models and central cost tracking.
3109
+ // Pass --ai-provider during init only when this app must use BYOK.
3110
+ ai: { mode: "hosted" },`;
3073
3111
  return `export default {
3074
3112
  platformUrl: process.env.ODLA_PLATFORM_URL ?? "https://odla.ai",
3075
3113
  dbEndpoint: process.env.ODLA_ENDPOINT ?? process.env.ODLA_DB_ENDPOINT ?? "https://db.odla.ai",
@@ -3088,12 +3126,7 @@ function configTemplate(input) {
3088
3126
  // When rules is omitted, the CLI generates deny-all rules from schema.
3089
3127
  defaultRules: "deny",
3090
3128
  },
3091
- ai: {
3092
- provider: process.env.ODLA_AI_PROVIDER ?? "${input.aiProvider}",
3093
- // Optional: set this env var while running provision to store the provider
3094
- // key in the platform vault for each tenant.
3095
- keyEnv: "${defaultKeyEnv(input.aiProvider)}",
3096
- },
3129
+ ${ai}
3097
3130
  ${calendar}
3098
3131
  auth: {
3099
3132
  clerk: {
@@ -3589,12 +3622,16 @@ async function smoke(options) {
3589
3622
  out.log(` tenant: ${entry.tenantId}`);
3590
3623
  const publicConfig = await getJson(doFetch, publicConfigUrl(cfg.platformUrl, cfg.app.id, env), void 0);
3591
3624
  out.log(` public-config: ok`);
3592
- if (cfg.ai?.provider) {
3625
+ if (cfg.services.includes("ai") && cfg.ai?.provider) {
3593
3626
  const provider = publicConfig.ai?.provider ?? null;
3594
3627
  if (provider !== cfg.ai.provider) {
3595
3628
  throw new Error(`ai provider mismatch: expected "${cfg.ai.provider}", public-config has "${provider ?? "none"}"`);
3596
3629
  }
3597
- out.log(` ai: ${provider}`);
3630
+ out.log(` ai: byok/${provider}`);
3631
+ } else if (cfg.services.includes("ai")) {
3632
+ const mode = publicConfig.ai?.mode;
3633
+ if (mode !== "hosted") throw new Error(`ai mode mismatch: expected "hosted", public-config has "${String(mode ?? "none")}"`);
3634
+ out.log(" ai: hosted");
3598
3635
  }
3599
3636
  if (hasO11y) out.log(` o11y: credentials present`);
3600
3637
  if (cfg.services.includes("calendar")) {
@@ -3672,7 +3709,7 @@ async function getJson(doFetch, url, bearer) {
3672
3709
  const res = await doFetch(url, {
3673
3710
  headers: bearer ? { authorization: `Bearer ${bearer}` } : void 0
3674
3711
  });
3675
- if (!res.ok) throw new Error(`${new URL(url).pathname} returned ${res.status}: ${await safeText4(res)}`);
3712
+ if (!res.ok) throw new Error(`${new URL(url).pathname} returned ${res.status}: ${await safeText5(res)}`);
3676
3713
  return res.json();
3677
3714
  }
3678
3715
  async function postJson2(doFetch, url, bearer, body) {
@@ -3681,7 +3718,7 @@ async function postJson2(doFetch, url, bearer, body) {
3681
3718
  headers: { authorization: `Bearer ${bearer}`, "content-type": "application/json" },
3682
3719
  body: JSON.stringify(body)
3683
3720
  });
3684
- if (!res.ok) throw new Error(`${new URL(url).pathname} returned ${res.status}: ${await safeText4(res)}`);
3721
+ if (!res.ok) throw new Error(`${new URL(url).pathname} returned ${res.status}: ${await safeText5(res)}`);
3685
3722
  return res.json();
3686
3723
  }
3687
3724
  function publicConfigUrl(platformUrl, appId, env) {
@@ -3689,7 +3726,7 @@ function publicConfigUrl(platformUrl, appId, env) {
3689
3726
  url.searchParams.set("env", env);
3690
3727
  return url.toString();
3691
3728
  }
3692
- async function safeText4(res) {
3729
+ async function safeText5(res) {
3693
3730
  try {
3694
3731
  return redactSecrets((await res.text()).slice(0, 500));
3695
3732
  } catch {
@@ -7160,7 +7197,7 @@ async function provisionIntegrationSeeds(doFetch, endpoint, tenantId, dbKey, int
7160
7197
  const payload = await postJson3(doFetch, `${base}/query`, dbKey, {
7161
7198
  query: { [seed.ns]: { $: { where: { [seed.key.attr]: seed.key.value }, limit: 1 } } }
7162
7199
  });
7163
- const rows = isRecord7(payload) && isRecord7(payload.result) ? payload.result[seed.ns] : void 0;
7200
+ const rows = isRecord8(payload) && isRecord8(payload.result) ? payload.result[seed.ns] : void 0;
7164
7201
  if (!Array.isArray(rows)) {
7165
7202
  throw new Error(`${env}: integration ${integration.id} seed ${seed.id} query returned an invalid response`);
7166
7203
  }
@@ -7192,7 +7229,7 @@ async function postJson3(doFetch, url, bearer, body) {
7192
7229
  if (!res.ok) throw new Error(`${new URL(url).pathname} failed: ${res.status} ${await responseText(res)}`);
7193
7230
  return res.json().catch(() => ({}));
7194
7231
  }
7195
- function isRecord7(value2) {
7232
+ function isRecord8(value2) {
7196
7233
  return value2 !== null && typeof value2 === "object" && !Array.isArray(value2);
7197
7234
  }
7198
7235
  async function responseText(res) {
@@ -7262,14 +7299,14 @@ async function mintDbKey(opts, tenantId) {
7262
7299
  appId: tenantId
7263
7300
  })
7264
7301
  });
7265
- if (!created.ok) throw new Error(`db app create (${tenantId}) failed: ${created.status} ${await safeText5(created)}`);
7302
+ if (!created.ok) throw new Error(`db app create (${tenantId}) failed: ${created.status} ${await safeText6(created)}`);
7266
7303
  res = await opts.fetch(`${opts.cfg.dbEndpoint}/admin/apps/${encodeURIComponent(tenantId)}/keys`, {
7267
7304
  method: "POST",
7268
7305
  headers,
7269
7306
  body: "{}"
7270
7307
  });
7271
7308
  }
7272
- if (!res.ok) throw new Error(`db key mint (${tenantId}) failed: ${res.status} ${await safeText5(res)}`);
7309
+ if (!res.ok) throw new Error(`db key mint (${tenantId}) failed: ${res.status} ${await safeText6(res)}`);
7273
7310
  const body = await res.json();
7274
7311
  if (!body.key) throw new Error(`db key mint (${tenantId}) returned no key`);
7275
7312
  return body.key;
@@ -7285,12 +7322,12 @@ async function issueO11yToken(opts) {
7285
7322
  `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`
7286
7323
  );
7287
7324
  }
7288
- if (!res.ok) throw new Error(`o11y token ${opts.rotateO11y ? "rotation" : "issue"} (${opts.env}) failed: ${res.status} ${await safeText5(res)}`);
7325
+ if (!res.ok) throw new Error(`o11y token ${opts.rotateO11y ? "rotation" : "issue"} (${opts.env}) failed: ${res.status} ${await safeText6(res)}`);
7289
7326
  const body = await res.json();
7290
7327
  if (!body.token) throw new Error(`o11y token ${opts.rotateO11y ? "rotation" : "issue"} (${opts.env}) returned no token`);
7291
7328
  return body.token;
7292
7329
  }
7293
- async function safeText5(res) {
7330
+ async function safeText6(res) {
7294
7331
  try {
7295
7332
  return redactSecrets((await res.text()).slice(0, 500));
7296
7333
  } catch {
@@ -7333,7 +7370,7 @@ async function provision(options) {
7333
7370
  const namespaces = Object.keys(integration.schema?.entities ?? {}).length;
7334
7371
  out.log(` integration.${integration.id}: ${namespaces} namespaces, ${integration.seeds?.length ?? 0} seeds, ${integration.probes?.length ?? 0} smoke probes`);
7335
7372
  }
7336
- out.log(` ai: ${cfg.services.includes("ai") ? cfg.ai?.provider ?? "not configured" : "not enabled"}`);
7373
+ out.log(` ai: ${cfg.services.includes("ai") ? cfg.ai?.provider ?? "hosted" : "not enabled"}`);
7337
7374
  if (cfg.services.includes("calendar")) {
7338
7375
  for (const env of cfg.envs) {
7339
7376
  const calendar = calendarServiceConfig(cfg, env);
@@ -7384,11 +7421,11 @@ async function provision(options) {
7384
7421
  for (const service of serviceOrder) {
7385
7422
  if (service === "ai") {
7386
7423
  if (cfg.ai?.provider) {
7387
- await apps.setAi(cfg.app.id, env, { provider: cfg.ai.provider, ...cfg.ai.model ? { model: cfg.ai.model } : {} });
7388
- out.log(`${env}: ai configured (${cfg.ai.provider}${cfg.ai.model ? `/${cfg.ai.model}` : ""})`);
7424
+ await apps.setAi(cfg.app.id, env, { mode: "byok", provider: cfg.ai.provider, ...cfg.ai.model ? { model: cfg.ai.model } : {} });
7425
+ out.log(`${env}: ai configured (byok ${cfg.ai.provider}${cfg.ai.model ? `/${cfg.ai.model}` : ""})`);
7389
7426
  } else {
7390
- await apps.setService(cfg.app.id, "ai", true, { env });
7391
- out.log(`${env}: ai enabled`);
7427
+ await apps.setAi(cfg.app.id, env, { mode: "hosted", ...cfg.ai?.model ? { model: cfg.ai.model } : {} });
7428
+ out.log(`${env}: ai configured (hosted${cfg.ai?.model ? `/${cfg.ai.model}` : ""})`);
7392
7429
  }
7393
7430
  } else {
7394
7431
  const config = service === "calendar" ? calendarServiceConfig(cfg, env) : void 0;
@@ -9461,7 +9498,7 @@ Start here:
9461
9498
 
9462
9499
  Usage:
9463
9500
  odla-ai setup [--dir <project>] [--agent <name>] [--global] [--force]
9464
- odla-ai init --app-id <id> --name <name> [--services db,ai,o11y,calendar] [--env dev --env prod]
9501
+ odla-ai init --app-id <id> --name <name> [--services db,ai,o11y,calendar] [--env dev --env prod] [--ai-provider <byok-provider>]
9465
9502
  odla-ai doctor [--config odla.config.mjs]
9466
9503
  odla-ai config <diff|plan> [--config odla.config.mjs] [--email <odla-account>] [--json]
9467
9504
  odla-ai config apply --plan <plan.json> [--idempotency-key <key>] [--email <odla-account>] [--json]
@@ -13124,4 +13161,4 @@ export {
13124
13161
  exitCodeFor,
13125
13162
  runCli
13126
13163
  };
13127
- //# sourceMappingURL=chunk-GENTB3VO.js.map
13164
+ //# sourceMappingURL=chunk-DQOJ4S6H.js.map