@k2b/cloud 0.42.0 → 0.44.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.
Files changed (352) hide show
  1. package/package.json +13 -5
  2. package/scripts/app-favicon.ts +7 -6
  3. package/scripts/build.ts +3 -0
  4. package/scripts/installable-assets.ts +61 -0
  5. package/scripts/preload.ts +3 -0
  6. package/src/_internal/app-access-path.ts +12 -0
  7. package/src/_internal/app-access-request.ts +13 -0
  8. package/src/_internal/app-access-resources.ts +141 -0
  9. package/src/_internal/app-event-catalog.ts +40 -0
  10. package/src/_internal/app-event-schemas.ts +127 -0
  11. package/src/_internal/application-package.ts +40 -0
  12. package/src/_internal/built-in-app-ids.json +27 -0
  13. package/src/_internal/capabilities.ts +185 -12
  14. package/src/_internal/capability-origin.ts +4 -0
  15. package/src/_internal/capability-streams.ts +10 -5
  16. package/src/_internal/define-app.ts +159 -45
  17. package/src/_internal/installable-assets.ts +90 -0
  18. package/src/_internal/installable-head.ts +22 -0
  19. package/src/_internal/object-store-area.ts +9 -0
  20. package/src/_internal/object-store-config.ts +101 -0
  21. package/src/_internal/object-store-key.ts +14 -0
  22. package/src/_internal/page-responses.ts +34 -3
  23. package/src/_internal/process-identity.ts +7 -2
  24. package/src/_internal/registry-validation.ts +15 -1
  25. package/src/_internal/registry.ts +44 -1
  26. package/src/_internal/runtime-context.ts +9 -1
  27. package/src/_internal/runtime-watcher.ts +8 -0
  28. package/src/_internal/status-preserving-ssr.ts +12 -1
  29. package/src/access/AppAccessEditor.browser-fixture.ts +429 -0
  30. package/src/access/AppAccessEditor.browser-frame.tsx +68 -0
  31. package/src/access/AppAccessEditor.browser-harness.tsx +65 -0
  32. package/src/access/AppAccessEditor.tsx +380 -0
  33. package/src/access/PermissionEditor.browser-harness.tsx +22 -2
  34. package/src/access/PermissionEditor.tsx +386 -77
  35. package/src/access/PrincipalAvatar.tsx +63 -0
  36. package/src/access/PrincipalPicker.tsx +29 -8
  37. package/src/access/ResourceApiKeys.tsx +3 -4
  38. package/src/access/app-access-adapter.ts +51 -0
  39. package/src/access/app-access-messages.ts +172 -0
  40. package/src/access/messages.ts +14 -0
  41. package/src/access/ui.ts +4 -1
  42. package/src/account/Avatar.tsx +21 -3
  43. package/src/account/ProfilePictureDialog.tsx +699 -0
  44. package/src/account/profile-picture-messages.ts +269 -0
  45. package/src/account/ui.ts +8 -1
  46. package/src/ai/admin.ts +3 -0
  47. package/src/ai/app-frame.ts +12 -0
  48. package/src/ai/application-tasks.ts +101 -0
  49. package/src/ai/assistant-models.ts +126 -64
  50. package/src/ai/capabilities.ts +19 -4
  51. package/src/ai/capability-execution.ts +5 -2
  52. package/src/ai/chart-block.ts +63 -30
  53. package/src/ai/chat/chart-block.tsx +12 -3
  54. package/src/ai/chat/composer-adapter.ts +8 -7
  55. package/src/ai/chat/message-actions.tsx +1 -1
  56. package/src/ai/chat/message-utils.ts +2 -2
  57. package/src/ai/chat/presentation.tsx +22 -1
  58. package/src/ai/chat-quotas.ts +5 -4
  59. package/src/ai/chat-tasks.ts +2 -2
  60. package/src/ai/client/controller.ts +11 -10
  61. package/src/ai/client/projection.ts +3 -3
  62. package/src/ai/code-capability-routes.ts +1 -0
  63. package/src/ai/compaction.ts +3 -3
  64. package/src/ai/credentials.ts +13 -114
  65. package/src/ai/default-tool-contracts.ts +4 -3
  66. package/src/ai/dictation-runtime.ts +4 -4
  67. package/src/ai/enrich.ts +2 -2
  68. package/src/ai/executor.ts +38 -27
  69. package/src/ai/http.ts +17 -29
  70. package/src/ai/index.ts +11 -5
  71. package/src/ai/inference-calls.ts +6 -4
  72. package/src/ai/maintenance.ts +1 -1
  73. package/src/ai/memory-learning-runs.ts +5 -5
  74. package/src/ai/memory-learning.ts +3 -3
  75. package/src/ai/message-queue.ts +1 -2
  76. package/src/ai/migrate.ts +75 -42
  77. package/src/ai/model-access-migrate.ts +3 -17
  78. package/src/ai/model-access.ts +44 -85
  79. package/src/ai/model-card.tsx +134 -0
  80. package/src/ai/model-catalog.ts +359 -0
  81. package/src/ai/model-chooser-messages.ts +88 -0
  82. package/src/ai/model-chooser.tsx +577 -0
  83. package/src/ai/model-delete.ts +234 -0
  84. package/src/ai/model-fixture.ts +39 -0
  85. package/src/ai/model-icon.tsx +36 -0
  86. package/src/ai/model-overview.ts +53 -0
  87. package/src/ai/model-usage.ts +45 -0
  88. package/src/ai/openai-api-messages.ts +0 -4
  89. package/src/ai/openai-api-slots.ts +10 -2
  90. package/src/ai/openai-api.ts +83 -16
  91. package/src/ai/personal-agent.ts +1 -1
  92. package/src/ai/prefs.ts +12 -10
  93. package/src/ai/projects-routes.ts +2 -1
  94. package/src/ai/projects.ts +13 -12
  95. package/src/ai/protocol.ts +2 -2
  96. package/src/ai/provider-connections.ts +161 -0
  97. package/src/ai/provider-errors.ts +138 -0
  98. package/src/ai/provider-fetch.ts +4 -0
  99. package/src/ai/provider-retry.ts +1 -1
  100. package/src/ai/provider.ts +2 -1
  101. package/src/ai/quota-limits.ts +36 -0
  102. package/src/ai/quota-provider.ts +11 -2
  103. package/src/ai/quota-report.ts +7 -4
  104. package/src/ai/quotas-migrate.ts +1 -0
  105. package/src/ai/quotas.ts +19 -8
  106. package/src/ai/recent-resources.ts +6 -0
  107. package/src/ai/request-headers.ts +14 -87
  108. package/src/ai/resource-refs.ts +6 -2
  109. package/src/ai/routes.ts +17 -18
  110. package/src/ai/runtime.ts +50 -22
  111. package/src/ai/settings.ts +93 -580
  112. package/src/ai/store.ts +45 -23
  113. package/src/ai/stream.ts +1 -1
  114. package/src/ai/structured-runs.ts +2 -2
  115. package/src/ai/structured.ts +29 -46
  116. package/src/ai/task-contracts.ts +25 -5
  117. package/src/ai/task-execution.ts +15 -6
  118. package/src/ai/task-model.ts +9 -0
  119. package/src/ai/transcription.ts +37 -21
  120. package/src/ai/turn-failure.ts +24 -2
  121. package/src/ai/types.ts +50 -62
  122. package/src/ai/ui.tsx +13 -0
  123. package/src/ai/usage.ts +8 -8
  124. package/src/ai/validate.ts +8 -7
  125. package/src/ai/view-block.ts +2 -2
  126. package/src/api/accounts-entities.ts +40 -16
  127. package/src/api/admin-ai-quotas.ts +34 -56
  128. package/src/api/admin-ai-usage.ts +1 -1
  129. package/src/api/admin-ai.ts +173 -0
  130. package/src/api/admin-core-settings.ts +68 -244
  131. package/src/api/admin-directory-shares.ts +55 -0
  132. package/src/api/admin-identity.ts +81 -2
  133. package/src/api/app-access.ts +85 -0
  134. package/src/api/app-approval.ts +3 -1
  135. package/src/api/app-event-delivery.ts +51 -0
  136. package/src/api/app-events.ts +123 -0
  137. package/src/api/app-resource-access.ts +48 -0
  138. package/src/api/auth/account-mismatch.ts +24 -0
  139. package/src/api/auth.ts +26 -3
  140. package/src/api/capabilities.ts +127 -15
  141. package/src/api/capability-streams.ts +2 -5
  142. package/src/api/identity-child-invocation.ts +145 -0
  143. package/src/api/index.ts +11 -2
  144. package/src/api/mcp.ts +180 -5
  145. package/src/api/me.ts +38 -17
  146. package/src/api/pwa-phone.ts +87 -48
  147. package/src/api/pwa.ts +34 -8
  148. package/src/api/resource-links.ts +83 -6
  149. package/src/api/resource-resolve.ts +50 -0
  150. package/src/api/search/schemas.ts +3 -1
  151. package/src/api/search.ts +9 -7
  152. package/src/api/widgets.ts +1 -0
  153. package/src/app-pwa/authority.ts +75 -0
  154. package/src/app-pwa/contracts.ts +177 -0
  155. package/src/app-pwa/core-query-error.ts +20 -0
  156. package/src/app-pwa/index.ts +23 -0
  157. package/src/app-pwa/protocol.ts +27 -0
  158. package/src/app-pwa/schema.ts +59 -0
  159. package/src/app-pwa/service.ts +861 -0
  160. package/src/app-pwa/transaction.ts +338 -0
  161. package/src/app-pwa/transport-input.ts +57 -0
  162. package/src/app-pwa/transport.ts +235 -0
  163. package/src/approvals/messages.ts +2 -2
  164. package/src/browser/app-access-recovery.ts +44 -0
  165. package/src/browser/app-session.ts +14 -3
  166. package/src/browser/cloud-id.ts +86 -0
  167. package/src/browser/live-websocket.ts +5 -0
  168. package/src/browser/pwa-drafts.ts +57 -0
  169. package/src/browser/resource-clipboard.ts +4 -0
  170. package/src/browser/resource-links.ts +52 -0
  171. package/src/browser/resource-picker.tsx +3 -2
  172. package/src/capabilities/calendar-sources.ts +302 -0
  173. package/src/capabilities/executions.ts +10 -3
  174. package/src/capabilities/migrate.ts +4 -0
  175. package/src/capabilities/nested.ts +43 -0
  176. package/src/capabilities/server-invoke.ts +202 -0
  177. package/src/capabilities/server.ts +26 -187
  178. package/src/capabilities/testing.ts +5 -2
  179. package/src/cli/access.ts +6 -5
  180. package/src/cli/account.ts +57 -0
  181. package/src/cli/admin/agents.ts +66 -0
  182. package/src/cli/admin/ai-models.ts +199 -0
  183. package/src/cli/admin/ai-quotas.ts +6 -102
  184. package/src/cli/admin/ai-usage.ts +4 -4
  185. package/src/cli/admin/app-credentials.ts +5 -3
  186. package/src/cli/admin/index.ts +7 -3
  187. package/src/cli/app-access.ts +60 -0
  188. package/src/cli/apps.ts +89 -26
  189. package/src/cli/avatar.ts +64 -0
  190. package/src/cli/capabilities.ts +22 -1
  191. package/src/cli/events.ts +320 -0
  192. package/src/cli/index.ts +1 -1
  193. package/src/config/env.ts +40 -0
  194. package/src/contracts/app-access.ts +52 -5
  195. package/src/contracts/app-approval.ts +8 -1
  196. package/src/contracts/app-details.ts +89 -0
  197. package/src/contracts/app-events.ts +51 -0
  198. package/src/contracts/app.ts +24 -5
  199. package/src/contracts/calendar-source.ts +274 -0
  200. package/src/contracts/calendar-view.ts +35 -0
  201. package/src/contracts/capabilities.ts +164 -3
  202. package/src/contracts/geo-admin.ts +2 -0
  203. package/src/contracts/geo-route.ts +0 -11
  204. package/src/contracts/geo.ts +14 -2
  205. package/src/contracts/index.ts +6 -0
  206. package/src/contracts/installable.ts +40 -0
  207. package/src/contracts/notification-types.ts +36 -9
  208. package/src/contracts/profile.ts +32 -0
  209. package/src/contracts/pwa-builds.ts +14 -0
  210. package/src/contracts/pwa-location.ts +38 -0
  211. package/src/contracts/pwa.ts +24 -3
  212. package/src/contracts/registry.ts +8 -0
  213. package/src/contracts/resource-clipboard.ts +35 -0
  214. package/src/contracts/resource-pages.ts +140 -0
  215. package/src/contracts/resource-resolution.ts +13 -0
  216. package/src/contracts/shared.ts +4 -0
  217. package/src/contracts/user-notifications.ts +2 -0
  218. package/src/events/app-event-delivery.ts +100 -0
  219. package/src/events/app-events.testing.ts +65 -0
  220. package/src/events/app-events.ts +134 -0
  221. package/src/events/core-user-created.ts +24 -0
  222. package/src/events/index.ts +9 -0
  223. package/src/events/testing.ts +2 -0
  224. package/src/index.ts +2 -1
  225. package/src/server/api/respond.ts +8 -3
  226. package/src/server/index.ts +1 -0
  227. package/src/server/middleware/app-access.ts +7 -6
  228. package/src/server/middleware/auth.ts +168 -66
  229. package/src/server/middleware/index.ts +1 -0
  230. package/src/server/middleware/invocation.ts +4 -2
  231. package/src/server/middleware/workload.ts +3 -3
  232. package/src/server/oauth-scope.ts +10 -0
  233. package/src/server/public-storage.ts +106 -0
  234. package/src/server/services/access.ts +8 -0
  235. package/src/services/accounts/app.ts +35 -3
  236. package/src/services/accounts/avatar.ts +342 -35
  237. package/src/services/accounts/base-group.ts +4 -5
  238. package/src/services/accounts/directory-shares.ts +157 -0
  239. package/src/services/accounts/entities.ts +50 -6
  240. package/src/services/accounts/groups.ts +3 -1
  241. package/src/services/accounts/identities.ts +64 -12
  242. package/src/services/accounts/local-groups.ts +1 -1
  243. package/src/services/accounts/users.ts +23 -15
  244. package/src/services/app-access-management.ts +519 -0
  245. package/src/services/app-access.ts +25 -7
  246. package/src/services/app-approval.ts +15 -4
  247. package/src/services/app-details.ts +224 -0
  248. package/src/services/app-discovery.ts +2 -1
  249. package/src/services/app-event-shapes.ts +139 -0
  250. package/src/services/app-events/accept.ts +116 -0
  251. package/src/services/app-events/admin.ts +250 -0
  252. package/src/services/app-events/declarations.ts +187 -0
  253. package/src/services/app-events/deliver.ts +218 -0
  254. package/src/services/app-events.ts +92 -0
  255. package/src/services/config-conflict.ts +11 -0
  256. package/src/services/core-user-events.ts +1 -0
  257. package/src/services/freeipa-config.ts +9 -0
  258. package/src/services/geo/admin.ts +4 -1
  259. package/src/services/geo/ledger.ts +47 -14
  260. package/src/services/geo/route-chain.ts +40 -18
  261. package/src/services/geo/route-estimate.ts +19 -0
  262. package/src/services/geo/route-plan.ts +0 -2
  263. package/src/services/geo/route-service.ts +57 -39
  264. package/src/services/geo/service.ts +51 -25
  265. package/src/services/identity/core-origin.ts +12 -0
  266. package/src/services/identity/index.ts +2 -1
  267. package/src/services/identity/invocation-actor.ts +50 -2
  268. package/src/services/identity/invocation-authority.ts +38 -1
  269. package/src/services/identity/invocation-token.ts +255 -50
  270. package/src/services/identity/issue-invocation.ts +9 -1
  271. package/src/services/identity/models-only-auth.ts +13 -4
  272. package/src/services/identity/nested-invocation.ts +128 -0
  273. package/src/services/image-metadata.ts +65 -1
  274. package/src/services/index.ts +24 -1
  275. package/src/services/ipa/sync.ts +6 -2
  276. package/src/services/ipa/users.ts +4 -1
  277. package/src/services/mandates/policy.ts +19 -12
  278. package/src/services/notifications/browser.ts +4 -0
  279. package/src/services/notifications/catalog.ts +5 -2
  280. package/src/services/notifications/platform.ts +7 -2
  281. package/src/services/notifications/user.ts +18 -7
  282. package/src/services/oauth-tokens.ts +28 -2
  283. package/src/services/object-store.ts +217 -0
  284. package/src/services/providers/local/users.ts +5 -0
  285. package/src/services/pwa-devices.ts +147 -37
  286. package/src/services/service-account-credentials.ts +5 -2
  287. package/src/services/session/index.ts +14 -0
  288. package/src/services/settings/core-settings.ts +91 -47
  289. package/src/services/settings/index.ts +8 -0
  290. package/src/services/settings/versions.ts +26 -0
  291. package/src/shared/ai-models.ts +274 -0
  292. package/src/shared/ai-quotas.ts +2 -2
  293. package/src/shared/ai-usage.ts +2 -2
  294. package/src/shared/index.ts +2 -0
  295. package/src/shared-text/documents.ts +25 -8
  296. package/src/shared-text/log.ts +1 -1
  297. package/src/shared-text/text-diff.ts +75 -0
  298. package/src/ssr/AdminArea.tsx +131 -0
  299. package/src/ssr/AdminLayout.tsx +11 -3
  300. package/src/ssr/AdminSidebar.tsx +7 -7
  301. package/src/ssr/AppAccessDenied.tsx +114 -0
  302. package/src/ssr/AppBadge.tsx +14 -0
  303. package/src/ssr/AppLaunchpadPanel.tsx +1 -1
  304. package/src/ssr/AppPwaRuntime.island.tsx +82 -0
  305. package/src/ssr/CloudShell.island.tsx +25 -4
  306. package/src/ssr/Layout.tsx +15 -7
  307. package/src/ssr/PwaLayout.tsx +73 -8
  308. package/src/ssr/PwaPartCount.island.tsx +23 -0
  309. package/src/ssr/PwaRuntime.island.tsx +85 -6
  310. package/src/ssr/SettingsSaveOutcome.tsx +58 -0
  311. package/src/ssr/SettingsSaveStatus.island.tsx +16 -0
  312. package/src/ssr/ShellRuntime.island.tsx +5 -0
  313. package/src/ssr/__snapshots__/Layout.unchanged.render.test.tsx.snap +3 -3
  314. package/src/ssr/__snapshots__/LayoutHelp.ssr.render.test.tsx.snap +1 -1
  315. package/src/ssr/admin-active-link.ts +16 -0
  316. package/src/ssr/admin-navigation.ts +26 -20
  317. package/src/ssr/app-badges.ts +31 -7
  318. package/src/ssr/app-pwa-document.tsx +16 -0
  319. package/src/ssr/app-pwa-messages.ts +17 -0
  320. package/src/ssr/index.ts +8 -0
  321. package/src/ssr/layout-context.ts +1 -1
  322. package/src/ssr/page-trail-runtime.ts +3 -1
  323. package/src/ssr/platform-messages.ts +9 -1
  324. package/src/ssr/pwa-messages.ts +10 -2
  325. package/src/ssr/rail-navigation.ts +5 -1
  326. package/src/ssr/settings-draft.ts +153 -0
  327. package/src/ssr/shell-data.tsx +2 -0
  328. package/src/styles/global.css +1 -0
  329. package/src/styles/profile-picture.css +270 -0
  330. package/src/styles/utilities-navigation.css +6 -0
  331. package/src/workflows/ai/policy.ts +49 -0
  332. package/src/workflows/ai/runtime.ts +131 -21
  333. package/src/workflows/ai/store.ts +283 -35
  334. package/src/workflows/ai/types.ts +8 -2
  335. package/src/workflows/ai-actions.ts +148 -120
  336. package/src/workflows/authoring.ts +31 -0
  337. package/src/workflows/contracts.ts +1 -0
  338. package/src/workflows/definition.ts +2 -0
  339. package/src/workflows/editor.ts +15 -0
  340. package/src/workflows/index.ts +2 -1
  341. package/src/workflows/language/compiler.ts +34 -3
  342. package/src/workflows/runtime/executor.ts +132 -33
  343. package/src/workflows/runtime/index.ts +2 -0
  344. package/src/workflows/runtime/ports.ts +5 -1
  345. package/src/workflows/store/actions.ts +1 -1
  346. package/src/workflows/store/definitions.ts +5 -0
  347. package/src/workflows/store/runs.ts +5 -3
  348. package/src/workflows/store/worker.ts +7 -2
  349. package/src/ai/model-pricing.ts +0 -43
  350. package/src/ai/model-request-settings.ts +0 -98
  351. package/src/ai/settings-messages.ts +0 -129
  352. package/src/shared/ai-model-request-settings.ts +0 -21
@@ -1,138 +1,37 @@
1
1
  import { sql } from "bun";
2
- import { toPgTextArray } from "../services/postgres";
3
2
  import { decryptValue, encryptValue } from "../services/settings/crypto";
4
- import type { AiRequestHeaderPatch } from "./request-headers";
5
3
 
6
4
  type SqlClient = typeof sql;
7
5
 
8
- /**
9
- * Provider API keys, one per model profile.
10
- *
11
- * They used to live on the profile inside the `ai.model_profiles_json` setting.
12
- * Settings are encrypted at rest but a `text` setting is handed to the admin UI
13
- * in full, so every key travelled to the browser in the page payload — and the
14
- * form had to send them back to avoid dropping them while editing a label.
15
- * Only `kind: "secret"` settings are redacted, and the registry is static, so
16
- * one setting per profile was never an option.
17
- *
18
- * These rows are the answer: encrypted with the same `APP_SECRET` helper the
19
- * settings store uses, addressed by profile id, and never serialised into
20
- * anything the client receives.
21
- */
22
-
23
- /** The key for a profile, or null when none is stored. */
24
- export const getAiCredential = async (profileId: string, db: SqlClient = sql): Promise<string | null> => {
6
+ /** Provider keys are encrypted at rest and never returned by an API. */
7
+ export const getAiCredential = async (providerId: string, db: SqlClient = sql): Promise<string | null> => {
25
8
  const [row] = await db<{ secret: string }[]>`
26
- SELECT secret FROM ai.model_credentials WHERE profile_id = ${profileId}
9
+ SELECT secret FROM ai.provider_credentials WHERE provider_id = ${providerId}
27
10
  `;
28
11
  if (!row) return null;
29
12
  try {
30
13
  const value = await decryptValue(row.secret);
31
14
  return typeof value === "string" && value.trim() ? value : null;
32
15
  } catch {
33
- console.warn(`[ai] ignoring unreadable provider credential for profile ${JSON.stringify(profileId)}`);
16
+ console.warn(`[ai] ignoring unreadable provider credential for provider ${JSON.stringify(providerId)}`);
34
17
  return null;
35
18
  }
36
19
  };
37
20
 
38
- /** Store or replace a profile's key. */
39
- export const setAiCredential = async (profileId: string, secret: string, db: SqlClient = sql): Promise<void> => {
21
+ /** Store or replace a provider's key. */
22
+ export const setAiCredential = async (providerId: string, secret: string, db: SqlClient = sql): Promise<void> => {
40
23
  const encrypted = await encryptValue(secret);
41
24
  await db`
42
- INSERT INTO ai.model_credentials (profile_id, secret)
43
- VALUES (${profileId}, ${encrypted})
44
- ON CONFLICT (profile_id) DO UPDATE
25
+ INSERT INTO ai.provider_credentials (provider_id, secret)
26
+ VALUES (${providerId}, ${encrypted})
27
+ ON CONFLICT (provider_id) DO UPDATE
45
28
  SET secret = EXCLUDED.secret,
46
29
  updated_at = now()
47
30
  `;
48
31
  };
49
32
 
50
- /** Profile ids whose stored key can currently be decrypted and used. */
51
- export const listAiCredentialProfileIds = async (db: SqlClient = sql): Promise<string[]> => {
52
- const rows = await db<{ profile_id: string; secret: string }[]>`SELECT profile_id, secret FROM ai.model_credentials`;
53
- const usable: string[] = [];
54
- for (const row of rows) {
55
- try {
56
- const value = await decryptValue(row.secret);
57
- if (typeof value === "string" && value.trim()) usable.push(row.profile_id);
58
- } catch {
59
- console.warn(`[ai] ignoring unreadable provider credential for profile ${JSON.stringify(row.profile_id)}`);
60
- }
61
- }
62
- return usable;
63
- };
64
-
65
- /**
66
- * Drop keys for profiles that no longer exist.
67
- *
68
- * Profiles live in a JSON setting rather than a table, so there is no foreign
69
- * key to cascade from. Pruning on save keeps a deleted profile from leaving its
70
- * key behind, and makes recreating an id a clean slate rather than a silent
71
- * inheritance.
72
- */
73
- export const pruneAiCredentials = async (keepProfileIds: readonly string[], db: SqlClient = sql): Promise<void> => {
74
- if (keepProfileIds.length === 0) {
75
- await db`DELETE FROM ai.model_credentials`;
76
- return;
77
- }
78
- await db`DELETE FROM ai.model_credentials WHERE profile_id <> ALL(${toPgTextArray([...keepProfileIds])}::text[])`;
79
- };
80
-
81
- /**
82
- * Separate provider keys from the profiles blob.
83
- *
84
- * Pure on purpose: the admin route and the one-time migration both need the
85
- * exact same split, and getting it wrong either leaks a key into a setting or
86
- * drops one on save. `profilesJson` is what may be stored; `credentials` is
87
- * what must go into the table; `profileIds` is what survives pruning.
88
- *
89
- * Unparseable input is handed back untouched — the settings validator owns that
90
- * error, and rewriting it here would replace the admin's parse message.
91
- */
92
- export const splitAiProfileCredentials = (
93
- rawValue: unknown,
94
- ): {
95
- profilesJson: string;
96
- credentials: Array<{ profileId: string; secret: string }>;
97
- profileIds: string[];
98
- requestHeaders: AiRequestHeaderPatch[];
99
- } | null => {
100
- if (typeof rawValue !== "string") return null;
101
- const text = rawValue;
102
- let parsed: unknown;
103
- try {
104
- parsed = JSON.parse(text || "[]");
105
- } catch {
106
- return null;
107
- }
108
- if (!Array.isArray(parsed)) return null;
109
-
110
- const profiles: unknown[] = [];
111
- const credentials: Array<{ profileId: string; secret: string }> = [];
112
- const profileIds: string[] = [];
113
- const requestHeaders: AiRequestHeaderPatch[] = [];
114
-
115
- for (const entry of parsed) {
116
- if (!entry || typeof entry !== "object") {
117
- profiles.push(entry);
118
- continue;
119
- }
120
- const {
121
- apiKey,
122
- requestHeaders: headerPatch,
123
- requestHeaderNames: _headerNames,
124
- credentialSetting: _legacy,
125
- ...profile
126
- } = entry as Record<string, unknown>;
127
- const profileId = typeof profile.id === "string" ? profile.id.trim() : "";
128
- if (profileId) {
129
- profile.id = profileId;
130
- profileIds.push(profileId);
131
- if (headerPatch !== undefined) requestHeaders.push({ profileId, patch: headerPatch });
132
- if (typeof apiKey === "string" && apiKey.trim()) credentials.push({ profileId, secret: apiKey.trim() });
133
- }
134
- profiles.push(profile);
135
- }
136
-
137
- return { profilesJson: JSON.stringify(profiles), credentials, profileIds, requestHeaders };
33
+ /** Provider IDs with a stored key; decrypt only the selected provider at execution. */
34
+ export const listAiCredentialProviderIds = async (db: SqlClient = sql): Promise<string[]> => {
35
+ const rows = await db<{ provider_id: string }[]>`SELECT provider_id FROM ai.provider_credentials`;
36
+ return rows.map((row) => row.provider_id);
138
37
  };
@@ -171,6 +171,7 @@ const chartKinds = (limits = true) => {
171
171
  kind: z.literal("bar"),
172
172
  ...ChartKindCommon,
173
173
  data: list(ChartCategory, 1, CLOUD_AI_CHART_MAX_VALUES),
174
+ xAxis: ChartAxis.pick({ label: true }).strict().optional(),
174
175
  yAxis: ChartAxis.optional(),
175
176
  colorByBar: z.boolean().optional(),
176
177
  showValues: z.boolean().optional(),
@@ -387,14 +388,14 @@ const flatChartSchema = () => {
387
388
  .max(CLOUD_AI_CHART_MAX_SERIES)
388
389
  .optional()
389
390
  .describe("line, scatter: label/data, optional reference role; line: style. Line needs 2 points per series."),
390
- xAxis: ChartAxis.optional().describe("line, scatter: x axis."),
391
- yAxis: ChartAxis.optional().describe("bar, line, scatter, boxplot: y axis."),
391
+ xAxis: ChartAxis.optional().describe("line, scatter: x axis. bar: only label, the category axis title."),
392
+ yAxis: ChartAxis.optional().describe("bar, line, scatter, boxplot: y axis; label names the value, format sets units."),
392
393
  legend: z
393
394
  .boolean()
394
395
  .optional()
395
396
  .describe("bar (colorByBar), line, scatter, pie, donut: legend; defaults to several series or unlabeled slices."),
396
397
  colorByBar: z.boolean().optional().describe("bar: one palette color per bar."),
397
- showValues: z.boolean().optional().describe("bar, heatmap: print values."),
398
+ showValues: z.boolean().optional().describe("bar: print values, by default when they stay readable. heatmap: print values."),
398
399
  area: z.boolean().optional().describe("line, sparkline: fill under the line."),
399
400
  smooth: z.boolean().optional().describe("line: smooth curves."),
400
401
  showLabels: z.boolean().optional().describe("pie, donut: name slices next to them."),
@@ -43,7 +43,7 @@ export const isRetryableDictationError = (error: unknown): boolean => {
43
43
  export const processAiDictation = async (
44
44
  id: string,
45
45
  signal: AbortSignal,
46
- run: (input: RunAiTranscriptionInput) => Promise<{ text: string; modelProfileId: string }> = runAiTranscription,
46
+ run: (input: RunAiTranscriptionInput) => Promise<{ text: string; modelId: string }> = runAiTranscription,
47
47
  ): Promise<void> => {
48
48
  const token = crypto.randomUUID();
49
49
  const [task] = await sql<ClaimedDictation[]>`
@@ -57,7 +57,7 @@ export const processAiDictation = async (
57
57
  name: "ai.dictation",
58
58
  source: "ai:dictations",
59
59
  category: "ai",
60
- attributes: { dictationId: id, conversationId: task.conversation_id, modelProfileId: task.model_profile_id, attempt: task.attempts },
60
+ attributes: { dictationId: id, conversationId: task.conversation_id, modelId: task.model_profile_id, attempt: task.attempts },
61
61
  });
62
62
  let failure: AiTranscriptionError | undefined;
63
63
  const canceled = new AbortController();
@@ -92,7 +92,7 @@ export const processAiDictation = async (
92
92
  traceParent: span,
93
93
  file: new Blob([new Uint8Array(task.source_bytes)]),
94
94
  filename: task.source_path,
95
- requestedModelId: task.model_profile_id,
95
+ model: { id: task.model_profile_id },
96
96
  language: task.language ?? undefined,
97
97
  signal: combined,
98
98
  attribution: { userId: task.user_id, conversationId: task.conversation_id },
@@ -109,7 +109,7 @@ export const processAiDictation = async (
109
109
  log[combined.aborted ? "info" : retry ? "warn" : "error"](failure.message, {
110
110
  dictationId: id,
111
111
  traceId: span.traceId,
112
- modelProfileId: task.model_profile_id,
112
+ modelId: task.model_profile_id,
113
113
  attempt: task.attempts,
114
114
  retry,
115
115
  errorCode: failure.code,
package/src/ai/enrich.ts CHANGED
@@ -151,7 +151,7 @@ export const enrichDirtyAiConversations = async (input: {
151
151
  // Resolve the background model once per run; AI disabled / no model = quiet no-op.
152
152
  let resolved: AiResolvedModel;
153
153
  try {
154
- resolved = await (input.deps?.resolveModel ?? resolveAiBackgroundModel)();
154
+ resolved = await (input.deps?.resolveModel ?? resolveAiBackgroundModel)({ class: "fast" });
155
155
  } catch (error) {
156
156
  log.info("Enrichment skipped: no background model available", {
157
157
  error: error instanceof Error ? error.message : String(error),
@@ -224,7 +224,7 @@ export const enrichDirtyAiConversations = async (input: {
224
224
  conversationId: conversation.id,
225
225
  status: "ok",
226
226
  trigger,
227
- modelProfileId: result.modelProfileId,
227
+ modelId: result.model.id,
228
228
  mode: result.structuredMeta.mode,
229
229
  durationMs: Date.now() - startedAt,
230
230
  titleUpdated: applyTitle,
@@ -7,10 +7,11 @@ import { CapabilityActionReviewDataSchema, capabilityActionPolicy } from "../con
7
7
  import type { AccessSubject, RequestActor } from "../server";
8
8
  import { approvalAssurance, approvalScopeFromToolName, approvals } from "../services/approvals";
9
9
  import { createHelpReader } from "../services/help";
10
- import { logger } from "../services/logging";
10
+ import { logger, trace } from "../services/logging";
11
11
  import { getMandate } from "../services/mandates";
12
12
  import type { MandatePolicyV1 } from "../services/mandates/policy";
13
13
  import { coreSettings } from "../services/settings/api";
14
+ import type { AiModelRequest } from "../shared/ai-models";
14
15
  import { normalizeLocale } from "../shared/locale";
15
16
  import { type AiActionCheck, type AiStoredActionReview, storedActionReview } from "./action-review";
16
17
  import {
@@ -99,6 +100,7 @@ import {
99
100
  AiTurnFailure,
100
101
  type AiTurnFailureInfo,
101
102
  type AiTurnFailureReason,
103
+ aiIssueDiagnostic,
102
104
  aiTurnFailureFromThrown,
103
105
  aiTurnReasonFromThrown,
104
106
  rememberProviderErrors,
@@ -702,7 +704,7 @@ type MaterializedChatConfig = {
702
704
  tools: AiRuntimeTool[];
703
705
  toolApprovalContext?: AiToolApprovalContext;
704
706
  modelPolicy: AiChatTurnRunConfig["modelPolicy"];
705
- requestedModelId?: string;
707
+ model?: AiModelRequest;
706
708
  };
707
709
 
708
710
  const materializeChatConfig = async (config: AiChatTurnRunConfig, signal: AbortSignal, turnId: string): Promise<MaterializedChatConfig> => {
@@ -722,7 +724,7 @@ const materializeChatConfig = async (config: AiChatTurnRunConfig, signal: AbortS
722
724
  : [],
723
725
  toolApprovalContext: config.toolApprovalContext,
724
726
  modelPolicy: config.modelPolicy,
725
- requestedModelId: config.requestedModelId,
727
+ model: config.model,
726
728
  };
727
729
  };
728
730
 
@@ -752,7 +754,7 @@ export class AiTurnExecutor {
752
754
  const runConfig = claim.runConfig;
753
755
  if (!runConfig) {
754
756
  pipeline.seedBaseline(claim.liveBlocks ?? []);
755
- await pipeline.emitTurnStarted(claim.turn.modelProfileId ?? "");
757
+ await pipeline.emitTurnStarted(claim.turn.modelId ?? "");
756
758
  await pipeline.emitBaseline();
757
759
  await this.finalize(
758
760
  conversationId,
@@ -767,7 +769,7 @@ export class AiTurnExecutor {
767
769
 
768
770
  if (runConfig.kind === "compact") {
769
771
  pipeline.seedBaseline(claim.liveBlocks ?? []);
770
- await pipeline.emitTurnStarted(claim.turn.modelProfileId ?? "");
772
+ await pipeline.emitTurnStarted(claim.turn.modelId ?? "");
771
773
  await pipeline.emitBaseline();
772
774
  await this.runCompaction(conversationId, turnId, pipeline, runConfig, signal);
773
775
  return;
@@ -778,7 +780,7 @@ export class AiTurnExecutor {
778
780
  attemptState = await loadChatAttemptState(conversationId, turnId);
779
781
  } catch (error) {
780
782
  pipeline.seedBaseline(claim.liveBlocks ?? []);
781
- await pipeline.emitTurnStarted(claim.turn.modelProfileId ?? "");
783
+ await pipeline.emitTurnStarted(claim.turn.modelId ?? "");
782
784
  await pipeline.emitBaseline();
783
785
  await this.finalize(
784
786
  conversationId,
@@ -792,15 +794,15 @@ export class AiTurnExecutor {
792
794
  return;
793
795
  }
794
796
  pipeline.seedBaseline(rebuildAttemptBaseline(attemptState));
795
- await pipeline.emitTurnStarted(claim.turn.modelProfileId ?? "");
797
+ await pipeline.emitTurnStarted(claim.turn.modelId ?? "");
796
798
  await pipeline.emitBaseline();
797
799
  await this.runChat(conversationId, turnId, claim, runConfig, pipeline, signal, false, attemptState);
798
800
  }
799
801
 
800
802
  /**
801
803
  * Ends the turn. A failure stores its reason twice: as a code on the turn's last message, which the chat words in
802
- * the reader's language, and as text in the turn's language for readers without the chat view. The raw cause, such
803
- * as a provider's own message, goes only to the log. A failed compaction left the chat as it was, so its text names
804
+ * the reader's language, and as text in the turn's language for readers without the chat view. The diagnostic cause,
805
+ * with provider and loop issues reduced to fixed text, goes only to the log. A failed compaction left the chat as it was, so its text names
804
806
  * only the reason.
805
807
  */
806
808
  private async finalize(
@@ -888,9 +890,6 @@ export class AiTurnExecutor {
888
890
  }
889
891
  resolvedProjectId = project.id;
890
892
  }
891
- if (isAssistantChatTurn(config) && claim.turn.modelProfileId) {
892
- await aiModelAccess.assertAllowed(claim.turn.modelProfileId, accessSubjectForActor(material.actor));
893
- }
894
893
  validated = await (this.config.validateTurn ?? validateAiTurnRequest)({
895
894
  input: config.input,
896
895
  hasImageAttachments: config.files?.attached.some((file) => isAiImageMediaType(file.mediaType)),
@@ -898,12 +897,10 @@ export class AiTurnExecutor {
898
897
  material.tools.some((tool) => tool.def.name === "view_image") &&
899
898
  (await isAiVisionModelConfigured(material.modelPolicy?.allowedDataBoundaries)),
900
899
  modelPolicy: material.modelPolicy,
901
- requestedModelId: isAssistantChatTurn(config)
902
- ? (claim.turn.modelProfileId ?? material.requestedModelId)
903
- : material.requestedModelId,
900
+ model: claim.turn.modelId ? { id: claim.turn.modelId } : material.model,
904
901
  });
905
902
  if (isAssistantChatTurn(config)) {
906
- await aiModelAccess.assertAllowed(validated.resolved.profile.id, accessSubjectForActor(material.actor));
903
+ await aiModelAccess.assertAllowed(validated.resolved.profile.modelId, accessSubjectForActor(material.actor));
907
904
  }
908
905
  } catch (error) {
909
906
  signal.removeEventListener("abort", onSignal);
@@ -919,6 +916,11 @@ export class AiTurnExecutor {
919
916
  return;
920
917
  }
921
918
  const { settings, resolved } = validated;
919
+ if (config.modelResolved?.id === resolved.profile.id) resolved.model = config.modelResolved;
920
+ await trace.record({
921
+ event: "model.resolved",
922
+ attributes: { ...resolved.model, resolvedClass: resolved.model.class, turnId, conversationId },
923
+ });
922
924
 
923
925
  const defaultToolSource = config.toolSource?.kind === "default" ? config.toolSource : null;
924
926
  const toolActor =
@@ -1186,7 +1188,7 @@ export class AiTurnExecutor {
1186
1188
  await pipeline.emitMessage(message);
1187
1189
  },
1188
1190
  conversationId,
1189
- modelProfileId: resolved.profile.id,
1191
+ modelId: resolved.profile.id,
1190
1192
  turnId,
1191
1193
  leaseOwner: this.config.leaseOwner,
1192
1194
  turnInput,
@@ -1590,7 +1592,9 @@ export class AiTurnExecutor {
1590
1592
  provider: acceptCanonicalToolNames(
1591
1593
  retryTransientProviderErrors(
1592
1594
  rememberProviderErrors(
1593
- answerOpenToolCalls(assistantQuotaProvider(resolved.provider, config, quotaSubject, resolved.profile, turnId, conversationId)),
1595
+ answerOpenToolCalls(
1596
+ assistantQuotaProvider(resolved.provider, config, quotaSubject, resolved.profile, turnId, conversationId, resolved.model),
1597
+ ),
1594
1598
  remember,
1595
1599
  ),
1596
1600
  {
@@ -1658,7 +1662,7 @@ export class AiTurnExecutor {
1658
1662
  : createCloudCompactFn({
1659
1663
  conversationId,
1660
1664
  turnId,
1661
- modelProfileId: resolved.profile.id,
1665
+ modelId: resolved.profile.id,
1662
1666
  additionalInstructions: settings.compactionInstructions,
1663
1667
  maxOutputTokens: resolved.profile.maxOutputTokens,
1664
1668
  signal: abortController.signal,
@@ -1885,8 +1889,9 @@ export class AiTurnExecutor {
1885
1889
  if (!abortController.signal.aborted && isToolRound(event.message)) noteToolRound();
1886
1890
  } else if (event.type === "issue") {
1887
1891
  if (event.issue.kind === "malformed_tool_call" && event.issue.callId) malformedCallIds.add(event.issue.callId);
1888
- lastIssueMessage = event.issue.message;
1889
- log.warn("AI turn issue", { conversationId, turnId, kind: event.issue.kind, message: event.issue.message });
1892
+ const diagnostic = aiIssueDiagnostic(event.issue);
1893
+ lastIssueMessage = diagnostic.message;
1894
+ log.warn("AI turn issue", { conversationId, turnId, ...diagnostic });
1890
1895
  } else if (event.type === "loop_end") {
1891
1896
  const stoppedAfterFailure =
1892
1897
  event.reason === "stop" &&
@@ -2218,7 +2223,7 @@ export class AiTurnExecutor {
2218
2223
  validated = await (this.config.validateTurn ?? validateAiTurnRequest)({
2219
2224
  input: "",
2220
2225
  modelPolicy: config.modelPolicy,
2221
- requestedModelId: config.requestedModelId,
2226
+ model: config.model,
2222
2227
  });
2223
2228
  } catch (error) {
2224
2229
  signal.removeEventListener("abort", onSignal);
@@ -2226,10 +2231,15 @@ export class AiTurnExecutor {
2226
2231
  return;
2227
2232
  }
2228
2233
  const { settings, resolved } = validated;
2234
+ if (config.modelResolved?.id === resolved.profile.id) resolved.model = config.modelResolved;
2235
+ await trace.record({
2236
+ event: "model.resolved",
2237
+ attributes: { ...resolved.model, resolvedClass: resolved.model.class, turnId, conversationId },
2238
+ });
2229
2239
 
2230
2240
  const store = aiConversations.createSessionStore({
2231
2241
  conversationId,
2232
- modelProfileId: resolved.profile.id,
2242
+ modelId: resolved.profile.id,
2233
2243
  turnId,
2234
2244
  leaseOwner: this.config.leaseOwner,
2235
2245
  });
@@ -2242,6 +2252,7 @@ export class AiTurnExecutor {
2242
2252
  inferenceProvider(resolved.provider, resolved.profile, {
2243
2253
  kind: "background",
2244
2254
  task: "chat-compaction",
2255
+ modelRef: resolved.model,
2245
2256
  conversationId,
2246
2257
  turnId,
2247
2258
  appId: "core",
@@ -2255,7 +2266,7 @@ export class AiTurnExecutor {
2255
2266
  compact: createCloudCompactFn({
2256
2267
  conversationId,
2257
2268
  turnId,
2258
- modelProfileId: resolved.profile.id,
2269
+ modelId: resolved.profile.id,
2259
2270
  additionalInstructions: settings.compactionInstructions,
2260
2271
  maxOutputTokens: resolved.profile.maxOutputTokens,
2261
2272
  signal: abortController.signal,
@@ -2274,7 +2285,7 @@ export class AiTurnExecutor {
2274
2285
  for await (const event of loop as AsyncIterable<CompactEvent>) {
2275
2286
  if (event.type === "compaction_start") await pipeline.applyCompaction("running");
2276
2287
  else if (event.type === "compaction_end") await pipeline.applyCompaction("completed");
2277
- else if (event.type === "issue") issueMessage = event.issue.message;
2288
+ else if (event.type === "issue") issueMessage = aiIssueDiagnostic(event.issue).message;
2278
2289
  else if (event.type === "loop_end") {
2279
2290
  status = event.reason === "stop" ? "completed" : event.reason === "aborted" ? "aborted" : "failed";
2280
2291
  await pipeline.applyCompaction(status === "failed" ? "failed" : "completed", event.result);
@@ -2428,10 +2439,10 @@ class StreamPipeline {
2428
2439
  await this.maybeSnapshot();
2429
2440
  }
2430
2441
 
2431
- async emitTurnStarted(modelProfileId: string): Promise<void> {
2442
+ async emitTurnStarted(modelId: string): Promise<void> {
2432
2443
  const seq = this.nextSeq();
2433
2444
  await this.publish(
2434
- this.envelope({ type: "turn_started" as const, seq, modelProfileId, providerModel: "", blocks: this.blocks }) as AiWireEvent,
2445
+ this.envelope({ type: "turn_started" as const, seq, modelId, providerModel: "", blocks: this.blocks }) as AiWireEvent,
2435
2446
  );
2436
2447
  }
2437
2448
 
package/src/ai/http.ts CHANGED
@@ -3,6 +3,7 @@ import type { Context } from "hono";
3
3
  import { z } from "zod";
4
4
  import { CapabilityAppIdSchema } from "../contracts/capabilities";
5
5
  import { type AuthContext, err, fail, respond } from "../server";
6
+ import { AiModelRequestSchema } from "../shared/ai-models";
6
7
  import { aiAttachmentMarker } from "./attachments";
7
8
  import { CODE_RUNTIME_TOOL_NAMES } from "./browser-code-contracts";
8
9
  import { AI_TURN_ATTACHMENT_MAX_ITEMS } from "./limits";
@@ -11,13 +12,7 @@ import { AiQuotaError } from "./quotas";
11
12
  import { AiResourceMarkerSchema } from "./resource-markers";
12
13
  import { AI_SHORT_ID_PATTERN } from "./short-id";
13
14
  import { AI_SKILL_NAME_MAX_CHARS, AI_SKILL_NAME_PATTERN } from "./skill-format";
14
- import {
15
- AI_MESSAGE_FEEDBACK_REASONS,
16
- type AiClientToolId,
17
- type AiDraftContentPart,
18
- type AiSettingsError,
19
- type AiUserContentPart,
20
- } from "./types";
15
+ import { AI_MESSAGE_FEEDBACK_REASONS, type AiClientToolId, type AiDraftContentPart, type AiUserContentPart } from "./types";
21
16
  import { isAiSettingsError } from "./validate";
22
17
 
23
18
  export const AiDraftContentPartSchema = z.discriminatedUnion("type", [
@@ -144,9 +139,10 @@ export const AiTurnInputSchema = z
144
139
  .min(1)
145
140
  .max(AI_TURN_ATTACHMENT_MAX_ITEMS * 2 + 1)
146
141
  .optional(),
147
- modelProfileId: z.string().trim().min(1).optional(),
142
+ model: AiModelRequestSchema.optional(),
148
143
  clientToolIds: AiClientToolIdsSchema.optional(),
149
144
  })
145
+ .strict()
150
146
  .refine((input) => Boolean(input.message?.trim() || input.content?.length), {
151
147
  message: "Message or content is required.",
152
148
  path: ["message"],
@@ -166,13 +162,14 @@ export const AiSubmitConversationDraftInputSchema = z
166
162
  .object({
167
163
  queueId: z.uuid().optional(),
168
164
  draftRevision: z.number().int().min(1),
169
- modelProfileId: z.string().trim().min(1).optional(),
165
+ model: AiModelRequestSchema.optional(),
170
166
  clientToolIds: AiClientToolIdsSchema.optional(),
171
167
  /** Set by a local client such as cld; the web never sends it. */
172
168
  client: AiLocalClientSchema.optional(),
173
169
  /** AGENTS.md files and local Skills a local client found; only valid together with client. */
174
170
  localContext: AiLocalContextSchema.optional(),
175
171
  })
172
+ .strict()
176
173
  .refine((input) => !input.localContext || Boolean(input.client), {
177
174
  message: "Local context requires a local client.",
178
175
  path: ["localContext"],
@@ -183,9 +180,11 @@ export const AiSteerInputSchema = z.object({
183
180
  clientRequestId: z.string().trim().min(1).max(120),
184
181
  });
185
182
 
186
- export const AiCompactionInputSchema = z.object({
187
- modelProfileId: z.string().trim().min(1).optional(),
188
- });
183
+ export const AiCompactionInputSchema = z
184
+ .object({
185
+ model: AiModelRequestSchema.optional(),
186
+ })
187
+ .strict();
189
188
 
190
189
  export type AiCompactionInput = z.infer<typeof AiCompactionInputSchema>;
191
190
 
@@ -200,11 +199,12 @@ export const AiMessageRetryInputSchema = z
200
199
  .min(1)
201
200
  .max(AI_TURN_ATTACHMENT_MAX_ITEMS * 2 + 1)
202
201
  .optional(),
203
- modelProfileId: z.string().trim().min(1).optional(),
202
+ model: AiModelRequestSchema.optional(),
204
203
  clientToolIds: AiClientToolIdsSchema.optional(),
205
204
  client: AiLocalClientSchema.optional(),
206
205
  localContext: AiLocalContextSchema.omit({ selectedSkills: true }).optional(),
207
206
  })
207
+ .strict()
208
208
  .refine((input) => !input.localContext || Boolean(input.client), {
209
209
  message: "Local context requires a local client.",
210
210
  path: ["localContext"],
@@ -268,25 +268,13 @@ export const AiApiErrorSchema = z.object({
268
268
 
269
269
  const conflict = (message: string) => ({ code: "CONFLICT" as const, message, status: 409 as const });
270
270
 
271
- const aiSettingsServiceError = (error: AiSettingsError) => {
272
- switch (error.code) {
273
- case "model_access_denied":
274
- return err.forbidden(error.message);
275
- case "invalid_model_profiles":
276
- case "model_policy_mismatch":
277
- return err.badInput(error.message);
278
- case "ai_disabled":
279
- case "missing_default_model":
280
- case "default_model_disabled":
281
- case "missing_provider_credential":
282
- return conflict(error.message);
283
- }
284
- };
285
-
286
271
  export const toAiErrorResponse = (c: Context<AuthContext>, error: unknown) => {
287
272
  if (error instanceof AiQuotaError) return c.json({ ok: false, error: error.message, message: error.message, code: error.code }, 429);
288
273
  if (isAiSettingsError(error)) {
289
- return respond(c, fail(aiSettingsServiceError(error.aiError)));
274
+ return c.json(
275
+ { ok: false, code: error.aiError.code, message: error.aiError.message, error: error.aiError.message },
276
+ error.aiError.code === "model_policy_mismatch" ? 400 : 409,
277
+ );
290
278
  }
291
279
  const message = error instanceof Error ? error.message : "AI request failed";
292
280
  if (message.includes("idx_ai_turns_one_active")) return respond(c, fail(err.conflict("Running turn")));
package/src/ai/index.ts CHANGED
@@ -1,4 +1,13 @@
1
+ export * from "../shared/ai-models";
1
2
  export { type AiAppSkillIssue, aiAppSkills, appSkillsRegistryState, reconcileAppSkills } from "./app-skill-store";
3
+ export {
4
+ type ApplicationAiTaskInput,
5
+ type ApplicationAiTaskKey,
6
+ deleteApplicationAiTasks,
7
+ listUnreleasedApplicationAiTaskSources,
8
+ releaseApplicationAiTask,
9
+ runApplicationAiTask,
10
+ } from "./application-tasks";
2
11
  export { type AiApprovalPreferenceRoutes, type AiApprovalPreferenceView, createAiApprovalPreferenceRoutes } from "./approval-routes";
3
12
  export {
4
13
  AI_WEBSITE_APPROVAL_TOOL,
@@ -19,7 +28,7 @@ export {
19
28
  rememberAiToolApproval,
20
29
  revokeAiToolApprovalPreference,
21
30
  } from "./approvals";
22
- export { assistantAiSettingsState, listAssistantAiModels, selectAssistantAiModelId } from "./assistant-models";
31
+ export { assistantAiSettingsState, listAssistantAiModels, peopleAiModels, selectAssistantAiModel } from "./assistant-models";
23
32
  export { type AiAttachmentRef, aiAttachmentMarker, formatAiFileSize, parseAiAttachmentMarkers } from "./attachments";
24
33
  export { CloudAiTranscribeAudioInputSchema, CloudAiTranscribeAudioOutputSchema, createCloudAiTranscribeAudioTool } from "./audio-tool";
25
34
  export { AiBackgroundApprovalRequiredError, authorizeBackgroundAction } from "./background-review";
@@ -65,7 +74,7 @@ export { parseAiSse } from "./client/transport";
65
74
  export { CODE_CAPABILITY_TOKEN_HEADER, createCodeCapabilityTransport } from "./code-capability-transport";
66
75
  export { authorizeCodeExecution } from "./code-execution";
67
76
  export { CODE_SOURCE_TOOLS, type CodeSourceToolName } from "./code-source-contracts";
68
- export { listAiCredentialProfileIds } from "./credentials";
77
+ export { listAiCredentialProviderIds } from "./credentials";
69
78
  export {
70
79
  CLOUD_AI_CHART_MAX_SERIES,
71
80
  CLOUD_AI_CHART_MAX_VALUES,
@@ -306,7 +315,6 @@ export {
306
315
  listAiModels,
307
316
  readAiSettingsState,
308
317
  resolveAiModel,
309
- resolveAiSettingsStateFromRaw,
310
318
  resolveAiVisionModel,
311
319
  selectAiModelProfile,
312
320
  toPublicAiSettingsState,
@@ -365,7 +373,6 @@ export {
365
373
  streamAiConversationEvents,
366
374
  } from "./stream";
367
375
  export {
368
- AI_BACKGROUND_MODEL_SETTING_KEY,
369
376
  type RunAiStructuredInput,
370
377
  type RunAiStructuredResult,
371
378
  resolveAiBackgroundModel,
@@ -420,7 +427,6 @@ export type {
420
427
  AiPendingTurnAction,
421
428
  AiProjectPromptSnapshot,
422
429
  AiProviderId,
423
- AiPublicModelProfile,
424
430
  AiRecentResource,
425
431
  AiResolvedModel,
426
432
  AiRuntimeTool,
@@ -27,8 +27,10 @@ export class AiBackgroundAdmissionError extends Error {
27
27
  export type AiCallContext = {
28
28
  kind: "chat" | "background";
29
29
  task: string;
30
+ modelRef?: import("../shared/ai-models").AiResolvedModelRef;
30
31
  subject?: AccessSubject | null;
31
32
  userId?: string;
33
+ credentialId?: string;
32
34
  conversationId?: string;
33
35
  turnId?: string;
34
36
  workflowRunId?: string;
@@ -91,7 +93,7 @@ export async function beginAiCall(
91
93
  }
92
94
  if (hasBillableAiPricing(profile.pricing) && context.kind === "chat" && context.subject && config.enabled) {
93
95
  const now = new Date();
94
- const snapshot = await aiQuotas.snapshot(context.subject, profile.id, now, db);
96
+ const snapshot = await aiQuotas.snapshot(context.subject, profile.modelId, now, db);
95
97
  for (const balance of snapshot.balances) {
96
98
  if (balance.bypassed || balance.limit === null) continue;
97
99
  if (balance.unknown)
@@ -109,7 +111,7 @@ export async function beginAiCall(
109
111
  SELECT (COALESCE(sum(cost),0)+COALESCE(sum(reserved) FILTER(WHERE finished_at IS NULL AND lease_expires_at>now()),0))::text AS committed FROM ai.inference_calls c
110
112
  WHERE kind='chat'
111
113
  AND user_id IS NOT DISTINCT FROM ${user}::uuid AND service_account_id IS NOT DISTINCT FROM ${service}::uuid
112
- AND (${balance.scope}='*' OR model_profile_id=${balance.scope}) AND started_at>=${window.from} AND started_at<${window.until}
114
+ AND (${balance.scope}='*' OR model_id=${balance.scope}) AND started_at>=${window.from} AND started_at<${window.until}
113
115
  AND started_at>COALESCE((SELECT max(created_at) FROM ai.cost_resets r
114
116
  WHERE r.user_id IS NOT DISTINCT FROM ${user}::uuid AND r.service_account_id IS NOT DISTINCT FROM ${service}::uuid AND r.scope=${balance.scope}),'-infinity'::timestamptz)`;
115
117
  const available = aiBudgetUnits(balance.limit) - aiDecimalUnits(pending?.committed ?? "0");
@@ -145,10 +147,10 @@ export async function beginAiCall(
145
147
  const id = crypto.randomUUID();
146
148
  const userId = context.subject?.type === "user" ? context.subject.userId : context.userId;
147
149
  const serviceId = context.subject?.type === "service_account" ? context.subject.serviceAccountId : null;
148
- await db`INSERT INTO ai.inference_calls(id,user_id,service_account_id,model_profile_id,provider_model,kind,task,
150
+ await db`INSERT INTO ai.inference_calls(id,user_id,service_account_id,credential_id,model_profile_id,model_id,requested_class,resolved_class,provider_model,kind,task,
149
151
  turn_id,turn_attempt,conversation_id,workflow_run_id,step_key,trace_id,app_id,workflow_id,workflow_name,workflow_version,pricing,reserved,lease_expires_at)
150
152
  VALUES(${id}::uuid,CASE WHEN ${serviceId}::uuid IS NULL THEN COALESCE(${userId ?? null}::uuid,(SELECT created_by_user_id FROM ai.conversations WHERE id=${context.conversationId ?? null}::uuid)) ELSE NULL END,
151
- ${serviceId}::uuid,${profile.id},${profile.model},${context.kind},${context.task},${context.turnId ?? null}::uuid,
153
+ ${serviceId}::uuid,${context.credentialId ?? null}::uuid,${profile.id},${profile.modelId},${context.modelRef?.requestedClass ?? null},${context.modelRef?.class ?? null},${profile.model},${context.kind},${context.task},${context.turnId ?? null}::uuid,
152
154
  (SELECT attempt FROM ai.turns WHERE id=${context.turnId ?? null}::uuid),${context.conversationId ?? null}::uuid,
153
155
  ${context.workflowRunId ?? null}::uuid,${context.stepKey ?? null},${context.traceId ?? null},COALESCE(${workflow?.app ?? context.appId ?? null},(SELECT launched_by_app_id FROM ai.conversations WHERE id=${context.conversationId ?? null}::uuid),'core'),${workflow?.id ?? null}::uuid,${workflow?.name ?? null},${workflow?.revision ?? null},
154
156
  (${profile.pricing ? JSON.stringify(profile.pricing) : null}::text)::jsonb,${reserved}::numeric,now()+interval '2 minutes')`;