@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
@@ -0,0 +1,359 @@
1
+ import { sql } from "bun";
2
+ import { z } from "zod";
3
+ import { ConfigConflict } from "../services/config-conflict";
4
+ import { stripImageDataUrlMetadata } from "../services/image-metadata";
5
+ import {
6
+ AI_MODEL_CLASSES,
7
+ AiAssignmentSchema,
8
+ type AiModel,
9
+ AiModelInputSchema,
10
+ AiModelSchema,
11
+ type AiModelVariant,
12
+ type AiProvider,
13
+ AiProviderInputSchema,
14
+ AiProviderSchema,
15
+ AiVariantInputSchema,
16
+ AiVariantSchema,
17
+ aiModelClassFallbackChain,
18
+ aiModelSupportsSlot,
19
+ } from "../shared/ai-models";
20
+ import { listAiCredentialProviderIds } from "./credentials";
21
+ import { AiModelAccessConflict, type AiModelAccessDraft, aiModelAccess } from "./model-access";
22
+ import { getAiRequestHeaders } from "./request-headers";
23
+ import type { AiModelProfile } from "./types";
24
+
25
+ export const lockAiModelCatalog = async (db = sql) => {
26
+ await db`SELECT pg_advisory_xact_lock(hashtext('ai.catalog'))`;
27
+ };
28
+
29
+ type Table = "providers" | "models" | "model_variants" | "model_assignments";
30
+ const camel = (key: string) => key.replace(/_([a-z])/g, (_, letter: string) => letter.toUpperCase());
31
+ const snake = (key: string) => key.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
32
+ const decode = (value: unknown) =>
33
+ Object.fromEntries(Object.entries(z.record(z.string(), z.unknown()).parse(value)).map(([key, entry]) => [camel(key), entry]));
34
+ const jsonColumns = new Set(["capabilities", "icon", "pricing", "extraBody"]);
35
+ const parameter = (value: unknown): string | number | boolean | null => {
36
+ if (value === undefined || value === null) return null;
37
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") return value;
38
+ return JSON.stringify(value);
39
+ };
40
+ export class AiConfigError extends Error {
41
+ constructor(
42
+ readonly code: "model_not_found" | "invalid_configuration",
43
+ message: string,
44
+ ) {
45
+ super(message);
46
+ }
47
+ }
48
+ export const assertAiRevision = async (
49
+ current: { revision: number; updatedBy: string | null; updatedAt: string },
50
+ revision: number,
51
+ db = sql,
52
+ ) => {
53
+ if (current.revision === revision) return;
54
+ const [who] = current.updatedBy
55
+ ? await db<
56
+ { id: string; name: string }[]
57
+ >`SELECT id, COALESCE(NULLIF(display_name,''),uid) AS name FROM auth.users WHERE id=${current.updatedBy}::uuid`
58
+ : [];
59
+ throw new ConfigConflict(current, who ?? null, current.updatedAt);
60
+ };
61
+ const rows = async (table: Table, id?: string, db = sql, lock = false) => {
62
+ const key = table === "model_assignments" ? "slot" : "id";
63
+ const result = await db.unsafe<{ data: unknown }[]>(
64
+ `SELECT to_jsonb(t) AS data FROM ai.${table} t${id === undefined ? "" : ` WHERE ${key}=$1`}${lock ? " FOR UPDATE" : ""}`,
65
+ id === undefined ? [] : [id],
66
+ );
67
+ return result.map((row) => decode(row.data));
68
+ };
69
+ const write = async (table: Table, value: Record<string, unknown>, actorId: string, db = sql, id?: string) => {
70
+ const entries = Object.entries(value).filter(([, value]) => value !== undefined);
71
+ const columns = entries.map(([key]) => snake(key));
72
+ // Bind serialized JSON as text; Bun otherwise encodes the string as a JSON scalar.
73
+ const values = entries.map(([key], index) => `$${index + 1}${jsonColumns.has(key) ? "::text::jsonb" : ""}`);
74
+ const params = entries.map(([, value]) => parameter(value));
75
+ if (id === undefined) {
76
+ await db.unsafe(`INSERT INTO ai.${table} (${columns.join(",")},updated_by) VALUES (${values.join(",")},$${params.length + 1}::uuid)`, [
77
+ ...params,
78
+ actorId,
79
+ ]);
80
+ } else {
81
+ await db.unsafe(
82
+ `UPDATE ai.${table} SET ${columns.map((key, index) => `${key}=${values[index]}`).join(",")}, revision=revision+1,updated_at=clock_timestamp(),updated_by=$${params.length + 1}::uuid WHERE ${table === "model_assignments" ? "slot" : "id"}=$${params.length + 2}`,
83
+ [...params, actorId, id],
84
+ );
85
+ }
86
+ };
87
+ const requireRow = <T>(value: T | undefined): T => {
88
+ if (!value) throw new AiConfigError("model_not_found", "Model or provider no longer exists.");
89
+ return value;
90
+ };
91
+ const presentProvider = async (value: Record<string, unknown>, db = sql, credentialIds?: string[]): Promise<AiProvider> => {
92
+ const id = z.string().parse(value.id);
93
+ return AiProviderSchema.parse({
94
+ ...value,
95
+ credentialSet: (credentialIds ?? (await listAiCredentialProviderIds(db))).includes(id),
96
+ requestHeaderNames: Object.keys(await getAiRequestHeaders(id, db)).sort(),
97
+ });
98
+ };
99
+ const validateProvider = (provider: z.infer<typeof AiProviderInputSchema>) => {
100
+ if (provider.type === "openai-compatible" && !provider.baseUrl)
101
+ throw new AiConfigError("invalid_configuration", "OpenAI-compatible providers require a base URL.");
102
+ };
103
+ const sanitizeModel = (model: z.infer<typeof AiModelInputSchema>) => ({
104
+ ...model,
105
+ icon: model.icon.kind === "image" ? { ...model.icon, dataUrl: stripImageDataUrlMetadata(model.icon.dataUrl) } : model.icon,
106
+ });
107
+ const validateModel = (model: z.infer<typeof AiModelInputSchema>, provider: AiProvider) => {
108
+ if (
109
+ model.kind === "transcription" &&
110
+ (!["openai", "openai-compatible"].includes(provider.type) ||
111
+ model.pricing ||
112
+ model.extraBody ||
113
+ model.capabilities.some((value) => value !== "transcription"))
114
+ )
115
+ throw new AiConfigError(
116
+ "invalid_configuration",
117
+ "Transcription requires an OpenAI-compatible provider and cannot have chat capabilities, parameters or token prices.",
118
+ );
119
+ if (model.kind === "chat" && model.capabilities.includes("transcription"))
120
+ throw new AiConfigError("invalid_configuration", "Chat models cannot transcribe audio.");
121
+ };
122
+ export const aiModelCatalog = {
123
+ async providers(db = sql, admin = true) {
124
+ const [values, credentialIds] = await Promise.all([rows("providers", undefined, db), listAiCredentialProviderIds(db)]);
125
+ return Promise.all(
126
+ values.map((row) =>
127
+ admin
128
+ ? presentProvider(row, db, credentialIds)
129
+ : AiProviderSchema.parse({ ...row, credentialSet: credentialIds.includes(z.string().parse(row.id)), requestHeaderNames: [] }),
130
+ ),
131
+ );
132
+ },
133
+ async provider(id: string, db = sql, lock = false) {
134
+ return presentProvider(requireRow((await rows("providers", id, db, lock))[0]), db);
135
+ },
136
+ async providerDetail(id: string, db = sql) {
137
+ const provider = await this.provider(id, db);
138
+ return { ...provider, models: (await this.models(db)).filter((model) => model.providerId === id) };
139
+ },
140
+ async modelDetail(id: string, db = sql) {
141
+ const model = await this.model(id, db);
142
+ return {
143
+ ...model,
144
+ variants: (await this.variants(db)).filter((variant) => variant.modelId === id),
145
+ access: (await aiModelAccess.listForAdmin(db))[id],
146
+ };
147
+ },
148
+ async models(db = sql): Promise<AiModel[]> {
149
+ return (await rows("models", undefined, db)).map((row) => AiModelSchema.parse(row));
150
+ },
151
+ async model(id: string, db = sql, lock = false): Promise<AiModel> {
152
+ return AiModelSchema.parse(requireRow((await rows("models", id, db, lock))[0]));
153
+ },
154
+ async variants(db = sql): Promise<AiModelVariant[]> {
155
+ return (await rows("model_variants", undefined, db))
156
+ .map((row) => AiVariantSchema.parse(row))
157
+ .sort((a, b) => a.position - b.position || a.id.localeCompare(b.id));
158
+ },
159
+ async variant(id: string, db = sql, lock = false): Promise<AiModelVariant> {
160
+ return AiVariantSchema.parse(requireRow((await rows("model_variants", id, db, lock))[0]));
161
+ },
162
+ async assignments(db = sql) {
163
+ return (await rows("model_assignments", undefined, db)).map((row) => AiAssignmentSchema.parse(row));
164
+ },
165
+ async assignmentsForAdmin(db = sql) {
166
+ const assignments = await this.assignments(db);
167
+ const usage = await db<
168
+ { class: string; app: string | null; task: string; calls: number }[]
169
+ >`SELECT requested_class AS class,app_id AS app,task,count(*)::int AS calls FROM ai.inference_calls WHERE requested_class IS NOT NULL AND started_at>=now()-interval '14 days' GROUP BY requested_class,app_id,task`;
170
+ return assignments.map((assignment) => {
171
+ const modelClass = AI_MODEL_CLASSES.find((value) => value === assignment.slot);
172
+ const fallbackChain = modelClass ? aiModelClassFallbackChain(modelClass) : [];
173
+ const effectiveClass = modelClass
174
+ ? ([modelClass, ...fallbackChain].find((value) => assignments.some((row) => row.slot === value && row.variantId)) ?? null)
175
+ : null;
176
+ return { ...assignment, fallbackChain, effectiveClass, usage: usage.filter((row) => row.class === assignment.slot) };
177
+ });
178
+ },
179
+ async createProvider(input: z.input<typeof AiProviderInputSchema>, actorId: string) {
180
+ const data = AiProviderInputSchema.parse(input);
181
+ validateProvider(data);
182
+ if (data.logo) data.logo = stripImageDataUrlMetadata(data.logo);
183
+ return sql.begin(async (db) => {
184
+ await lockAiModelCatalog(db);
185
+ await write("providers", data, actorId, db);
186
+ return this.provider(data.id, db);
187
+ });
188
+ },
189
+ async updateProvider(id: string, patch: z.infer<ReturnType<typeof AiProviderInputSchema.partial>>, revision: number, actorId: string) {
190
+ return sql.begin(async (db) => {
191
+ await lockAiModelCatalog(db);
192
+ const current = await this.providerDetail(id, db);
193
+ await assertAiRevision(current, revision, db);
194
+ const data = AiProviderInputSchema.parse({ ...AiProviderInputSchema.strip().parse(current), ...patch, id });
195
+ validateProvider(data);
196
+ if (data.logo) data.logo = stripImageDataUrlMetadata(data.logo);
197
+ for (const model of (await this.models(db)).filter((model) => model.providerId === id)) validateModel(model, { ...current, ...data });
198
+ if (data.type !== current.type || data.baseUrl !== current.baseUrl) {
199
+ await db`DELETE FROM ai.provider_credentials WHERE provider_id=${id}`;
200
+ await db`DELETE FROM ai.provider_request_headers WHERE provider_id=${id}`;
201
+ }
202
+ await write(
203
+ "providers",
204
+ {
205
+ ...data,
206
+ ...(data.type !== current.type || data.baseUrl !== current.baseUrl
207
+ ? { status: "unknown", statusCode: null, statusMessage: null, statusCheckedAt: null }
208
+ : {}),
209
+ },
210
+ actorId,
211
+ db,
212
+ id,
213
+ );
214
+ return this.provider(id, db);
215
+ });
216
+ },
217
+ async createModels(providerId: string, inputs: z.input<typeof AiModelInputSchema>[], actorId: string) {
218
+ return sql.begin(async (db) => {
219
+ await lockAiModelCatalog(db);
220
+ const provider = await this.provider(providerId, db, true);
221
+ const result: AiModel[] = [];
222
+ for (const input of inputs) {
223
+ const data = sanitizeModel(AiModelInputSchema.parse(input));
224
+ validateModel(data, provider);
225
+ if (data.kind === "transcription") data.capabilities = ["transcription"];
226
+ await write("models", { ...data, providerId }, actorId, db);
227
+ await write(
228
+ "model_variants",
229
+ { ...AiVariantInputSchema.parse({ slug: "standard", label: "Standard" }), id: data.id, modelId: data.id },
230
+ actorId,
231
+ db,
232
+ );
233
+ result.push(await this.model(data.id, db));
234
+ }
235
+ await aiModelAccess.syncModels(
236
+ (await this.models(db)).map((model) => model.id),
237
+ [],
238
+ db,
239
+ );
240
+ return result;
241
+ });
242
+ },
243
+ async updateModel(
244
+ id: string,
245
+ patch: Partial<z.infer<typeof AiModelInputSchema>>,
246
+ revision: number,
247
+ actorId: string,
248
+ access?: AiModelAccessDraft,
249
+ ) {
250
+ return sql.begin(async (db) => {
251
+ await lockAiModelCatalog(db);
252
+ const current = await this.modelDetail(id, db);
253
+ await assertAiRevision(current, revision, db);
254
+ const data = sanitizeModel(AiModelInputSchema.parse({ ...AiModelInputSchema.strip().parse(current), ...patch, id }));
255
+ validateModel(data, await this.provider(current.providerId, db));
256
+ if (data.kind === "transcription") data.capabilities = ["transcription"];
257
+ if (data.kind === "transcription" && (await this.variants(db)).some((v) => v.modelId === id && v.slug !== "standard"))
258
+ throw new AiConfigError("invalid_configuration", "Transcription models only have a standard variant.");
259
+ const variantIds = (await this.variants(db)).filter((variant) => variant.modelId === id).map((variant) => variant.id);
260
+ for (const assignment of await this.assignments(db))
261
+ if (assignment.variantId && variantIds.includes(assignment.variantId)) validateAiSlot(assignment.slot, data);
262
+ if (access) {
263
+ try {
264
+ await aiModelAccess.syncModels(
265
+ (await this.models(db)).map((model) => model.id),
266
+ [{ ...access, modelId: id }],
267
+ db,
268
+ );
269
+ } catch (error) {
270
+ if (error instanceof AiModelAccessConflict) {
271
+ const detail = await this.modelDetail(id, db);
272
+ throw new ConfigConflict(detail, null, detail.updatedAt);
273
+ }
274
+ throw error;
275
+ }
276
+ }
277
+ await write("models", data, actorId, db, id);
278
+ return this.model(id, db);
279
+ });
280
+ },
281
+ async createVariant(modelId: string, input: z.input<typeof AiVariantInputSchema>, actorId: string) {
282
+ return sql.begin(async (db) => {
283
+ await lockAiModelCatalog(db);
284
+ const model = await this.model(modelId, db, true);
285
+ if (model.kind !== "chat") throw new AiConfigError("invalid_configuration", "Transcription models only have a standard variant.");
286
+ const data = AiVariantInputSchema.parse(input);
287
+ if (data.slug === "standard") throw new AiConfigError("invalid_configuration", "The standard variant already exists.");
288
+ const id = `${modelId}:${data.slug}`;
289
+ await write("model_variants", { ...data, id, modelId }, actorId, db);
290
+ return this.variant(id, db);
291
+ });
292
+ },
293
+ async updateVariant(id: string, patch: Partial<z.infer<typeof AiVariantInputSchema>>, revision: number, actorId: string) {
294
+ return sql.begin(async (db) => {
295
+ await lockAiModelCatalog(db);
296
+ const current = await this.variant(id, db, true);
297
+ await assertAiRevision(current, revision, db);
298
+ const data = AiVariantInputSchema.parse({ ...AiVariantInputSchema.strip().parse(current), ...patch, slug: current.slug });
299
+ const model = await this.model(current.modelId, db);
300
+ if (model.kind === "transcription" && (data.reasoningEffort || data.extraBody || data.pricing || data.maxOutputTokens))
301
+ throw new AiConfigError("invalid_configuration", "Audio variants do not support chat overrides.");
302
+ await write("model_variants", data, actorId, db, id);
303
+ return this.variant(id, db);
304
+ });
305
+ },
306
+ async assign(slot: import("../shared/ai-models").AiModelSlot, variantId: string | null, revision: number, actorId: string) {
307
+ return sql.begin(async (db) => {
308
+ await lockAiModelCatalog(db);
309
+ const current = AiAssignmentSchema.parse(requireRow((await rows("model_assignments", slot, db, true))[0]));
310
+ await assertAiRevision((await this.assignmentsForAdmin(db)).find((assignment) => assignment.slot === slot) ?? current, revision, db);
311
+ if (variantId) {
312
+ const variant = await this.variant(variantId, db);
313
+ const model = await this.model(variant.modelId, db);
314
+ validateAiSlot(slot, model);
315
+ }
316
+ await write("model_assignments", { variantId }, actorId, db, slot);
317
+ return requireRow((await this.assignments(db)).find((row) => row.slot === slot));
318
+ });
319
+ },
320
+ };
321
+ export const validateAiSlot = (slot: import("../shared/ai-models").AiModelSlot, model: Pick<AiModel, "kind" | "capabilities">) => {
322
+ if (!aiModelSupportsSlot(slot, model)) throw new AiConfigError("invalid_configuration", "The model does not support this assignment.");
323
+ };
324
+ export const flattenAiModelVariant = (provider: AiProvider, model: AiModel, variant: AiModelVariant): AiModelProfile => ({
325
+ id: variant.id,
326
+ modelId: model.id,
327
+ providerId: provider.id,
328
+ label: model.label,
329
+ variantLabel: variant.label,
330
+ description: variant.hint || model.description,
331
+ icon: model.icon,
332
+ showInPicker: variant.showInPicker,
333
+ provider: provider.type,
334
+ model: model.providerModel,
335
+ enabled: provider.enabled && model.enabled,
336
+ capabilities: model.capabilities,
337
+ dataBoundary: provider.dataBoundary,
338
+ baseURL: provider.baseUrl ?? undefined,
339
+ contextWindow: model.contextWindow ?? undefined,
340
+ temperature: model.temperature ?? undefined,
341
+ maxOutputTokens: variant.maxOutputTokens ?? model.maxOutputTokens ?? undefined,
342
+ reasoningEffort: variant.reasoningEffort ?? undefined,
343
+ extraBody: { ...model.extraBody, ...variant.extraBody },
344
+ maxLoadedTools: model.maxLoadedTools ?? undefined,
345
+ maxToolRounds: model.maxToolRounds ?? undefined,
346
+ pricing: variant.pricing ?? model.pricing ?? undefined,
347
+ });
348
+ export const readAiModelProfiles = async (): Promise<AiModelProfile[]> => {
349
+ const [providers, models, variants] = await Promise.all([
350
+ aiModelCatalog.providers(sql, false),
351
+ aiModelCatalog.models(),
352
+ aiModelCatalog.variants(),
353
+ ]);
354
+ return variants.flatMap((variant) => {
355
+ const model = models.find((model) => model.id === variant.modelId);
356
+ const provider = providers.find((provider) => provider.id === model?.providerId);
357
+ return model && provider ? [flattenAiModelVariant(provider, model, variant)] : [];
358
+ });
359
+ };
@@ -0,0 +1,88 @@
1
+ import { i18n } from "@k2b/stdlib";
2
+
3
+ /** What people read when they choose a model: class names, their subtitles, the chip, the card and the notes. */
4
+ export const aiModelChooserMessages = i18n.define({
5
+ baseLocale: "en",
6
+ messages: {
7
+ en: {
8
+ class_fast: "Fast",
9
+ class_balanced: "Balanced",
10
+ class_thorough: "Thorough",
11
+ class_maximum: "Maximum",
12
+ classSub_fast: "When speed matters",
13
+ classSub_balanced: "For most tasks",
14
+ classSub_thorough: "For complex tasks",
15
+ classSub_maximum: "For especially difficult questions",
16
+ ownChoice: "Own choice",
17
+ resetTo: "Back to “Balanced”",
18
+ sliderAria: "Model, from fast to thorough",
19
+ model: "Model",
20
+ allModels: "All models",
21
+ classes: "Classes",
22
+ back: "Back",
23
+ chooseModel: "Choose model",
24
+ search: "Search models",
25
+ noMatch: "No model matches",
26
+ clearSearch: "Clear search",
27
+ loadFailed: "Models could not be loaded.",
28
+ loadFailedSub: "The server did not answer. Your chosen model stays.",
29
+ retry: "Try again",
30
+ empty: "No model is available to you yet.",
31
+ emptySub: "Ask your administrators.",
32
+ chipNone: "No model",
33
+ chipNotLoaded: "Models not loaded",
34
+ unavailable: "Unavailable right now",
35
+ keyRejected: "Access disrupted",
36
+ budget: ({ time }: { time: string }) => `Budget used up · back ${time}`,
37
+ revoked: "No longer available to you",
38
+ classMissing: ({ chosen, answers }: { chosen: string; answers: string }) => `${chosen} is not set up right now; ${answers} answers.`,
39
+ noteRunning: "While an answer runs, the model stays the same.",
40
+ noteNoModel: "No AI model is available to you yet. Administrators can grant models under Administration → AI.",
41
+ noteUnavailable: ({ name }: { name: string }) => `${name} is unavailable right now. Please choose another model.`,
42
+ noteBudget: ({ name, time }: { name: string; time: string }) => `Your budget for ${name} is used up. Available again ${time}.`,
43
+ noteRevoked: ({ name }: { name: string }) => `You may no longer use ${name}. Choose another model.`,
44
+ noteRemoved: ({ removed, next }: { removed: string; next: string }) => `${removed} was removed. This chat now uses ${next}.`,
45
+ switchTo: ({ name }: { name: string }) => `Switch to “${name}”`,
46
+ },
47
+ de: {
48
+ class_fast: "Schnell",
49
+ class_balanced: "Ausgewogen",
50
+ class_thorough: "Gründlich",
51
+ class_maximum: "Maximal",
52
+ classSub_fast: "Wenn es schnell gehen soll",
53
+ classSub_balanced: "Für die meisten Aufgaben",
54
+ classSub_thorough: "Für komplexe Aufgaben",
55
+ classSub_maximum: "Für besonders schwierige Fragen",
56
+ ownChoice: "Eigene Auswahl",
57
+ resetTo: "Zurück zu „Ausgewogen“",
58
+ sliderAria: "Modell, von schnell bis gründlich",
59
+ model: "Modell",
60
+ allModels: "Alle Modelle",
61
+ classes: "Klassen",
62
+ back: "Zurück",
63
+ chooseModel: "Modell wählen",
64
+ search: "Modelle suchen",
65
+ noMatch: "Kein Modell passt",
66
+ clearSearch: "Suche leeren",
67
+ loadFailed: "Modelle konnten nicht geladen werden.",
68
+ loadFailedSub: "Der Server antwortete nicht. Dein gewähltes Modell bleibt.",
69
+ retry: "Erneut versuchen",
70
+ empty: "Für dich ist noch kein Modell freigegeben.",
71
+ emptySub: "Bitte deine Administration darum.",
72
+ chipNone: "Kein Modell",
73
+ chipNotLoaded: "Modelle nicht geladen",
74
+ unavailable: "Gerade nicht verfügbar",
75
+ keyRejected: "Zugang gestört",
76
+ budget: ({ time }) => `Budget aufgebraucht · wieder ${time}`,
77
+ revoked: "Nicht mehr freigegeben",
78
+ classMissing: ({ chosen, answers }) => `${chosen} ist gerade nicht eingerichtet; ${answers} antwortet.`,
79
+ noteRunning: "Während eine Antwort läuft, bleibt das Modell gleich.",
80
+ noteNoModel: "Für dich ist noch kein KI-Modell freigegeben. Die Administration kann Modelle unter Administration → KI freigeben.",
81
+ noteUnavailable: ({ name }) => `${name} ist gerade nicht verfügbar. Wähle bitte ein anderes Modell.`,
82
+ noteBudget: ({ name, time }) => `Dein Budget für ${name} ist aufgebraucht. Wieder verfügbar ${time}.`,
83
+ noteRevoked: ({ name }) => `Du darfst ${name} nicht mehr nutzen. Wähle ein anderes Modell.`,
84
+ noteRemoved: ({ removed, next }) => `${removed} wurde entfernt. Dieser Chat nutzt ab jetzt ${next}.`,
85
+ switchTo: ({ name }) => `Zu „${name}“ wechseln`,
86
+ },
87
+ },
88
+ });