@k2b/cloud 0.43.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 (267) hide show
  1. package/package.json +7 -2
  2. package/src/_internal/app-access-request.ts +13 -0
  3. package/src/_internal/app-access-resources.ts +141 -0
  4. package/src/_internal/app-event-catalog.ts +40 -0
  5. package/src/_internal/app-event-schemas.ts +127 -0
  6. package/src/_internal/application-package.ts +40 -0
  7. package/src/_internal/built-in-app-ids.json +27 -0
  8. package/src/_internal/capabilities.ts +158 -12
  9. package/src/_internal/capability-origin.ts +4 -0
  10. package/src/_internal/capability-streams.ts +7 -5
  11. package/src/_internal/define-app.ts +57 -6
  12. package/src/_internal/object-store-area.ts +9 -0
  13. package/src/_internal/object-store-config.ts +101 -0
  14. package/src/_internal/object-store-key.ts +14 -0
  15. package/src/_internal/process-identity.ts +7 -2
  16. package/src/_internal/registry-validation.ts +11 -1
  17. package/src/_internal/registry.ts +8 -1
  18. package/src/_internal/runtime-context.ts +8 -1
  19. package/src/access/AppAccessEditor.tsx +2 -0
  20. package/src/access/PermissionEditor.tsx +9 -3
  21. package/src/access/PrincipalPicker.tsx +14 -4
  22. package/src/access/ResourceApiKeys.tsx +3 -4
  23. package/src/access/app-access-messages.ts +6 -0
  24. package/src/account/ProfilePictureDialog.tsx +699 -0
  25. package/src/account/profile-picture-messages.ts +269 -0
  26. package/src/account/ui.ts +7 -0
  27. package/src/ai/admin.ts +3 -0
  28. package/src/ai/app-frame.ts +12 -0
  29. package/src/ai/application-tasks.ts +101 -0
  30. package/src/ai/assistant-models.ts +126 -64
  31. package/src/ai/capabilities.ts +19 -4
  32. package/src/ai/chart-block.ts +63 -30
  33. package/src/ai/chat/chart-block.tsx +12 -3
  34. package/src/ai/chat/composer-adapter.ts +8 -7
  35. package/src/ai/chat/message-actions.tsx +1 -1
  36. package/src/ai/chat/message-utils.ts +2 -2
  37. package/src/ai/chat/presentation.tsx +22 -1
  38. package/src/ai/chat-quotas.ts +5 -4
  39. package/src/ai/chat-tasks.ts +2 -2
  40. package/src/ai/client/controller.ts +11 -10
  41. package/src/ai/client/projection.ts +3 -3
  42. package/src/ai/compaction.ts +3 -3
  43. package/src/ai/credentials.ts +13 -114
  44. package/src/ai/default-tool-contracts.ts +4 -3
  45. package/src/ai/dictation-runtime.ts +4 -4
  46. package/src/ai/enrich.ts +2 -2
  47. package/src/ai/executor.ts +31 -22
  48. package/src/ai/http.ts +17 -29
  49. package/src/ai/index.ts +11 -5
  50. package/src/ai/inference-calls.ts +5 -4
  51. package/src/ai/maintenance.ts +1 -1
  52. package/src/ai/memory-learning-runs.ts +5 -5
  53. package/src/ai/memory-learning.ts +3 -3
  54. package/src/ai/message-queue.ts +1 -2
  55. package/src/ai/migrate.ts +75 -42
  56. package/src/ai/model-access-migrate.ts +3 -17
  57. package/src/ai/model-access.ts +44 -85
  58. package/src/ai/model-card.tsx +134 -0
  59. package/src/ai/model-catalog.ts +359 -0
  60. package/src/ai/model-chooser-messages.ts +88 -0
  61. package/src/ai/model-chooser.tsx +577 -0
  62. package/src/ai/model-delete.ts +234 -0
  63. package/src/ai/model-fixture.ts +39 -0
  64. package/src/ai/model-icon.tsx +36 -0
  65. package/src/ai/model-overview.ts +53 -0
  66. package/src/ai/model-usage.ts +45 -0
  67. package/src/ai/openai-api-messages.ts +0 -4
  68. package/src/ai/openai-api.ts +26 -9
  69. package/src/ai/personal-agent.ts +1 -1
  70. package/src/ai/prefs.ts +12 -10
  71. package/src/ai/projects-routes.ts +2 -1
  72. package/src/ai/projects.ts +13 -12
  73. package/src/ai/protocol.ts +2 -2
  74. package/src/ai/provider-connections.ts +161 -0
  75. package/src/ai/provider-errors.ts +55 -6
  76. package/src/ai/provider-fetch.ts +4 -0
  77. package/src/ai/provider.ts +1 -1
  78. package/src/ai/quota-limits.ts +36 -0
  79. package/src/ai/quota-provider.ts +4 -2
  80. package/src/ai/quota-report.ts +7 -4
  81. package/src/ai/quotas.ts +19 -8
  82. package/src/ai/recent-resources.ts +6 -0
  83. package/src/ai/request-headers.ts +14 -87
  84. package/src/ai/resource-refs.ts +6 -2
  85. package/src/ai/routes.ts +17 -18
  86. package/src/ai/runtime.ts +50 -22
  87. package/src/ai/settings.ts +93 -580
  88. package/src/ai/store.ts +45 -23
  89. package/src/ai/stream.ts +1 -1
  90. package/src/ai/structured-runs.ts +2 -2
  91. package/src/ai/structured.ts +21 -44
  92. package/src/ai/task-contracts.ts +25 -5
  93. package/src/ai/task-execution.ts +15 -6
  94. package/src/ai/task-model.ts +9 -0
  95. package/src/ai/transcription.ts +27 -22
  96. package/src/ai/types.ts +50 -62
  97. package/src/ai/ui.tsx +13 -0
  98. package/src/ai/usage.ts +8 -8
  99. package/src/ai/validate.ts +8 -7
  100. package/src/ai/view-block.ts +2 -2
  101. package/src/api/accounts-entities.ts +16 -0
  102. package/src/api/admin-ai-quotas.ts +34 -56
  103. package/src/api/admin-ai-usage.ts +1 -1
  104. package/src/api/admin-ai.ts +173 -0
  105. package/src/api/admin-core-settings.ts +68 -244
  106. package/src/api/app-approval.ts +3 -1
  107. package/src/api/app-event-delivery.ts +51 -0
  108. package/src/api/app-events.ts +123 -0
  109. package/src/api/app-resource-access.ts +48 -0
  110. package/src/api/auth/account-mismatch.ts +24 -0
  111. package/src/api/auth.ts +26 -3
  112. package/src/api/capabilities.ts +28 -4
  113. package/src/api/index.ts +7 -1
  114. package/src/api/mcp.ts +175 -2
  115. package/src/api/me.ts +6 -4
  116. package/src/api/pwa-phone.ts +75 -26
  117. package/src/api/pwa.ts +34 -8
  118. package/src/api/resource-links.ts +83 -6
  119. package/src/api/resource-resolve.ts +50 -0
  120. package/src/api/search/schemas.ts +3 -1
  121. package/src/api/search.ts +9 -7
  122. package/src/app-pwa/contracts.ts +4 -0
  123. package/src/app-pwa/core-query-error.ts +20 -0
  124. package/src/app-pwa/index.ts +1 -0
  125. package/src/app-pwa/service.ts +91 -0
  126. package/src/app-pwa/transport.ts +22 -0
  127. package/src/approvals/messages.ts +2 -2
  128. package/src/browser/cloud-id.ts +86 -0
  129. package/src/browser/resource-clipboard.ts +4 -0
  130. package/src/browser/resource-links.ts +52 -0
  131. package/src/capabilities/server-invoke.ts +2 -11
  132. package/src/capabilities/testing.ts +5 -2
  133. package/src/cli/access.ts +6 -5
  134. package/src/cli/admin/ai-models.ts +199 -0
  135. package/src/cli/admin/ai-quotas.ts +6 -102
  136. package/src/cli/admin/ai-usage.ts +4 -4
  137. package/src/cli/admin/app-credentials.ts +5 -3
  138. package/src/cli/admin/index.ts +5 -2
  139. package/src/cli/apps.ts +89 -26
  140. package/src/cli/capabilities.ts +22 -1
  141. package/src/cli/events.ts +320 -0
  142. package/src/config/env.ts +40 -0
  143. package/src/contracts/app-access.ts +26 -2
  144. package/src/contracts/app-details.ts +89 -0
  145. package/src/contracts/app-events.ts +51 -0
  146. package/src/contracts/app.ts +21 -5
  147. package/src/contracts/capabilities.ts +143 -3
  148. package/src/contracts/index.ts +2 -0
  149. package/src/contracts/notification-types.ts +36 -9
  150. package/src/contracts/pwa.ts +17 -1
  151. package/src/contracts/registry.ts +6 -0
  152. package/src/contracts/resource-clipboard.ts +35 -0
  153. package/src/contracts/resource-pages.ts +140 -0
  154. package/src/contracts/resource-resolution.ts +13 -0
  155. package/src/contracts/shared.ts +4 -0
  156. package/src/contracts/user-notifications.ts +2 -0
  157. package/src/events/app-event-delivery.ts +100 -0
  158. package/src/events/app-events.testing.ts +65 -0
  159. package/src/events/app-events.ts +134 -0
  160. package/src/events/core-user-created.ts +24 -0
  161. package/src/events/index.ts +9 -0
  162. package/src/events/testing.ts +2 -0
  163. package/src/index.ts +1 -1
  164. package/src/server/api/respond.ts +8 -3
  165. package/src/server/index.ts +1 -0
  166. package/src/server/middleware/app-access.ts +1 -1
  167. package/src/server/middleware/auth.ts +21 -7
  168. package/src/server/middleware/index.ts +1 -0
  169. package/src/server/middleware/invocation.ts +2 -0
  170. package/src/server/public-storage.ts +106 -0
  171. package/src/server/services/access.ts +8 -0
  172. package/src/services/accounts/app.ts +6 -0
  173. package/src/services/accounts/entities.ts +14 -3
  174. package/src/services/accounts/groups.ts +3 -1
  175. package/src/services/accounts/identities.ts +62 -10
  176. package/src/services/accounts/users.ts +7 -6
  177. package/src/services/app-access.ts +25 -7
  178. package/src/services/app-details.ts +224 -0
  179. package/src/services/app-discovery.ts +2 -1
  180. package/src/services/app-event-shapes.ts +139 -0
  181. package/src/services/app-events/accept.ts +116 -0
  182. package/src/services/app-events/admin.ts +250 -0
  183. package/src/services/app-events/declarations.ts +187 -0
  184. package/src/services/app-events/deliver.ts +218 -0
  185. package/src/services/app-events.ts +92 -0
  186. package/src/services/config-conflict.ts +11 -0
  187. package/src/services/core-user-events.ts +1 -0
  188. package/src/services/freeipa-config.ts +9 -0
  189. package/src/services/geo/ledger.ts +28 -10
  190. package/src/services/geo/service.ts +43 -24
  191. package/src/services/identity/core-origin.ts +12 -0
  192. package/src/services/identity/invocation-actor.ts +20 -2
  193. package/src/services/identity/invocation-authority.ts +4 -0
  194. package/src/services/identity/invocation-token.ts +238 -56
  195. package/src/services/identity/issue-invocation.ts +6 -1
  196. package/src/services/image-metadata.ts +65 -1
  197. package/src/services/index.ts +13 -1
  198. package/src/services/ipa/sync.ts +6 -2
  199. package/src/services/ipa/users.ts +4 -1
  200. package/src/services/mandates/policy.ts +19 -12
  201. package/src/services/notifications/browser.ts +4 -0
  202. package/src/services/notifications/catalog.ts +5 -2
  203. package/src/services/notifications/platform.ts +7 -2
  204. package/src/services/notifications/user.ts +18 -7
  205. package/src/services/oauth-tokens.ts +28 -2
  206. package/src/services/object-store.ts +217 -0
  207. package/src/services/providers/local/users.ts +5 -0
  208. package/src/services/pwa-devices.ts +147 -37
  209. package/src/services/session/index.ts +14 -0
  210. package/src/services/settings/core-settings.ts +71 -43
  211. package/src/services/settings/index.ts +8 -0
  212. package/src/services/settings/versions.ts +26 -0
  213. package/src/shared/ai-models.ts +274 -0
  214. package/src/shared/ai-quotas.ts +2 -2
  215. package/src/shared/ai-usage.ts +2 -2
  216. package/src/shared/index.ts +2 -0
  217. package/src/shared-text/documents.ts +25 -8
  218. package/src/shared-text/log.ts +1 -1
  219. package/src/shared-text/text-diff.ts +75 -0
  220. package/src/ssr/AdminArea.tsx +131 -0
  221. package/src/ssr/AdminLayout.tsx +9 -3
  222. package/src/ssr/AdminSidebar.tsx +7 -7
  223. package/src/ssr/AppAccessDenied.tsx +2 -0
  224. package/src/ssr/AppBadge.tsx +14 -0
  225. package/src/ssr/AppLaunchpadPanel.tsx +1 -1
  226. package/src/ssr/CloudShell.island.tsx +25 -4
  227. package/src/ssr/Layout.tsx +8 -6
  228. package/src/ssr/PwaLayout.tsx +16 -1
  229. package/src/ssr/PwaPartCount.island.tsx +23 -0
  230. package/src/ssr/SettingsSaveOutcome.tsx +58 -0
  231. package/src/ssr/SettingsSaveStatus.island.tsx +16 -0
  232. package/src/ssr/__snapshots__/Layout.unchanged.render.test.tsx.snap +3 -3
  233. package/src/ssr/admin-active-link.ts +16 -0
  234. package/src/ssr/admin-navigation.ts +25 -20
  235. package/src/ssr/app-badges.ts +31 -7
  236. package/src/ssr/index.ts +8 -0
  237. package/src/ssr/page-trail-runtime.ts +3 -1
  238. package/src/ssr/platform-messages.ts +9 -1
  239. package/src/ssr/pwa-messages.ts +2 -2
  240. package/src/ssr/rail-navigation.ts +5 -1
  241. package/src/ssr/settings-draft.ts +153 -0
  242. package/src/ssr/shell-data.tsx +2 -0
  243. package/src/styles/global.css +1 -0
  244. package/src/styles/profile-picture.css +270 -0
  245. package/src/styles/utilities-navigation.css +6 -0
  246. package/src/workflows/ai/policy.ts +49 -0
  247. package/src/workflows/ai/runtime.ts +131 -21
  248. package/src/workflows/ai/store.ts +283 -35
  249. package/src/workflows/ai/types.ts +8 -2
  250. package/src/workflows/ai-actions.ts +148 -120
  251. package/src/workflows/authoring.ts +31 -0
  252. package/src/workflows/contracts.ts +1 -0
  253. package/src/workflows/definition.ts +2 -0
  254. package/src/workflows/editor.ts +15 -0
  255. package/src/workflows/index.ts +2 -1
  256. package/src/workflows/language/compiler.ts +34 -3
  257. package/src/workflows/runtime/executor.ts +132 -33
  258. package/src/workflows/runtime/index.ts +2 -0
  259. package/src/workflows/runtime/ports.ts +5 -1
  260. package/src/workflows/store/actions.ts +1 -1
  261. package/src/workflows/store/definitions.ts +5 -0
  262. package/src/workflows/store/runs.ts +5 -3
  263. package/src/workflows/store/worker.ts +7 -2
  264. package/src/ai/model-pricing.ts +0 -43
  265. package/src/ai/model-request-settings.ts +0 -98
  266. package/src/ai/settings-messages.ts +0 -129
  267. package/src/shared/ai-model-request-settings.ts +0 -21
@@ -1,117 +1,44 @@
1
1
  import { sql } from "bun";
2
- import { toPgTextArray } from "../services/postgres";
3
2
  import { decryptValue, encryptValue } from "../services/settings/crypto";
4
- import {
5
- AI_REQUEST_HEADERS_PROVIDER_ERROR,
6
- AiRequestHeadersSchema,
7
- patchAiRequestHeaders,
8
- providerSupportsRequestHeaders,
9
- } from "../shared/ai-request-options";
10
- import type { AiModelProfile } from "./types";
3
+ import { AiRequestHeadersSchema, patchAiRequestHeaders } from "../shared/ai-request-options";
11
4
 
12
5
  type SqlClient = typeof sql;
13
- export type AiRequestHeaderPatch = { profileId: string; patch: unknown };
14
6
 
15
- const decryptHeaders = async (profileId: string, secret: string): Promise<Record<string, string>> => {
7
+ const decryptHeaders = async (providerId: string, secret: string): Promise<Record<string, string>> => {
16
8
  try {
17
9
  const parsed = AiRequestHeadersSchema.safeParse(await decryptValue(secret));
18
10
  if (parsed.success && Object.values(parsed.data).every((value) => typeof value === "string"))
19
11
  return patchAiRequestHeaders({}, parsed.data);
20
12
  } catch {}
21
- console.warn(`[ai] ignoring unreadable request headers for profile ${JSON.stringify(profileId)}`);
13
+ console.warn(`[ai] ignoring unreadable request headers for provider ${JSON.stringify(providerId)}`);
22
14
  return {};
23
15
  };
24
16
 
25
17
  /** Secrets stay server-side, in a separate encrypted row like provider API keys. */
26
- export const getAiRequestHeaders = async (profileId: string, db: SqlClient = sql): Promise<Record<string, string>> => {
27
- const [row] = await db<{ secret: string }[]>`SELECT secret FROM ai.model_request_headers WHERE profile_id = ${profileId}`;
28
- return row ? decryptHeaders(profileId, row.secret) : {};
18
+ export const getAiRequestHeaders = async (providerId: string, db: SqlClient = sql): Promise<Record<string, string>> => {
19
+ const [row] = await db<{ secret: string }[]>`SELECT secret FROM ai.provider_request_headers WHERE provider_id = ${providerId}`;
20
+ return row ? decryptHeaders(providerId, row.secret) : {};
29
21
  };
30
22
 
31
- export const setAiRequestHeaders = async (profileId: string, headers: Record<string, string>, db: SqlClient = sql): Promise<void> => {
23
+ export const setAiRequestHeaders = async (providerId: string, headers: Record<string, string>, db: SqlClient = sql): Promise<void> => {
32
24
  const normalized = patchAiRequestHeaders({}, headers);
33
25
  if (!Object.keys(normalized).length) {
34
- await db`DELETE FROM ai.model_request_headers WHERE profile_id = ${profileId}`;
26
+ await db`DELETE FROM ai.provider_request_headers WHERE provider_id = ${providerId}`;
35
27
  return;
36
28
  }
37
29
  const encrypted = await encryptValue(normalized);
38
- await db`INSERT INTO ai.model_request_headers (profile_id, secret) VALUES (${profileId}, ${encrypted})
39
- ON CONFLICT (profile_id) DO UPDATE SET secret = EXCLUDED.secret, updated_at = now()`;
30
+ await db`INSERT INTO ai.provider_request_headers (provider_id, secret) VALUES (${providerId}, ${encrypted})
31
+ ON CONFLICT (provider_id) DO UPDATE SET secret = EXCLUDED.secret, updated_at = now()`;
40
32
  };
41
33
 
42
- /** Admin reads contain names only. Never serialize header values into profiles. */
34
+ /** Admin reads contain names only. Never serialize header values into providers. */
43
35
  export const listAiRequestHeaderNames = async (db: SqlClient = sql): Promise<Record<string, string[]>> => {
44
- const rows = await db<{ profile_id: string; secret: string }[]>`SELECT profile_id, secret FROM ai.model_request_headers`;
36
+ const rows = await db<{ provider_id: string; secret: string }[]>`SELECT provider_id, secret FROM ai.provider_request_headers`;
45
37
  const names: Record<string, string[]> = {};
46
38
  for (const row of rows)
47
- Object.defineProperty(names, row.profile_id, {
48
- value: Object.keys(await decryptHeaders(row.profile_id, row.secret)).sort(),
39
+ Object.defineProperty(names, row.provider_id, {
40
+ value: Object.keys(await decryptHeaders(row.provider_id, row.secret)).sort(),
49
41
  enumerable: true,
50
42
  });
51
43
  return names;
52
44
  };
53
-
54
- export const pruneAiRequestHeaders = async (keepProfileIds: readonly string[], db: SqlClient = sql): Promise<void> => {
55
- if (!keepProfileIds.length) await db`DELETE FROM ai.model_request_headers`;
56
- else await db`DELETE FROM ai.model_request_headers WHERE profile_id <> ALL(${toPgTextArray([...keepProfileIds])}::text[])`;
57
- };
58
-
59
- /** Follow credentials: changing provider discards stored secrets; omission preserves them on the same provider. */
60
- export const planAiProfileRequestHeaders = (input: {
61
- currentProfiles: readonly AiModelProfile[];
62
- nextProfiles: readonly AiModelProfile[];
63
- existingNames: Record<string, string[]>;
64
- submitted: readonly AiRequestHeaderPatch[];
65
- }): { keepHeaderProfileIds: string[]; patches: { profileId: string; patch: Record<string, string | null> }[]; error?: string } => {
66
- const current = new Map(input.currentProfiles.map((profile) => [profile.id, profile]));
67
- const keepHeaderProfileIds = input.nextProfiles
68
- .filter(
69
- (profile) =>
70
- providerSupportsRequestHeaders(profile.provider) &&
71
- current.get(profile.id)?.provider === profile.provider &&
72
- !profile.capabilities.includes("transcription"),
73
- )
74
- .map((profile) => profile.id);
75
- const patches: { profileId: string; patch: Record<string, string | null> }[] = [];
76
- for (const submitted of input.submitted) {
77
- const profile = input.nextProfiles.find((profile) => profile.id === submitted.profileId);
78
- const parsed = AiRequestHeadersSchema.safeParse(submitted.patch);
79
- if (!parsed.success)
80
- return { keepHeaderProfileIds: [], patches: [], error: `requestHeaders: ${zodHeaderMessage(parsed.error.issues)}` };
81
- if (!profile || !providerSupportsRequestHeaders(profile.provider))
82
- return { keepHeaderProfileIds: [], patches: [], error: AI_REQUEST_HEADERS_PROVIDER_ERROR };
83
- if (profile.capabilities.includes("transcription"))
84
- return {
85
- keepHeaderProfileIds: [],
86
- patches: [],
87
- error: "requestHeaders: Request settings are not supported on transcription profiles.",
88
- };
89
- const oldNames =
90
- keepHeaderProfileIds.includes(profile.id) && Object.hasOwn(input.existingNames, profile.id) ? input.existingNames[profile.id]! : [];
91
- try {
92
- patchAiRequestHeaders(Object.fromEntries(oldNames.map((name) => [name, ""])), parsed.data);
93
- } catch {
94
- return {
95
- keepHeaderProfileIds: [],
96
- patches: [],
97
- error: "requestHeaders: At most 32 extra headers are allowed after applying the patch.",
98
- };
99
- }
100
- patches.push({ profileId: profile.id, patch: parsed.data });
101
- }
102
- return { keepHeaderProfileIds, patches };
103
- };
104
-
105
- const zodHeaderMessage = (issues: readonly { path: readonly PropertyKey[]; message: string }[]) =>
106
- issues.map((issue) => `${issue.path.map(String).join(".") || "headers"}: ${issue.message}`).join("; ");
107
-
108
- /** Apply after pruning so a provider change cannot inherit old secrets. Call inside the settings transaction. */
109
- export const storeAiRequestHeaderPlan = async (
110
- plan: ReturnType<typeof planAiProfileRequestHeaders>,
111
- db: SqlClient = sql,
112
- ): Promise<void> => {
113
- if (plan.error) throw new Error(plan.error);
114
- await pruneAiRequestHeaders(plan.keepHeaderProfileIds, db);
115
- for (const { profileId, patch } of plan.patches)
116
- await setAiRequestHeaders(profileId, patchAiRequestHeaders(await getAiRequestHeaders(profileId, db), patch), db);
117
- };
@@ -13,7 +13,7 @@ import type { AiConversationResourceObservation } from "./types";
13
13
 
14
14
  const LinkedResourceReferenceSchema = CloudResourceReferenceSchema.extend({
15
15
  href: CloudResourceHrefSchema.optional(),
16
- });
16
+ }).strip();
17
17
 
18
18
  export const addCloudResourceHrefs = <T>(
19
19
  value: T,
@@ -67,12 +67,14 @@ export const collectConversationResourceObservations = (...values: unknown[]): A
67
67
  return;
68
68
  }
69
69
 
70
- const view = CloudResourceViewSchema.safeParse(value);
70
+ const view = CloudResourceViewSchema.strip().safeParse(value);
71
71
  if (view.success) {
72
72
  const resource: CloudResourceView = view.data;
73
73
  observations.set(observationKey(resource.ref), {
74
74
  ref: resource.ref,
75
75
  title: resource.title,
76
+ context: resource.context,
77
+ status: resource.status,
76
78
  preview: resource.preview,
77
79
  icon: resource.icon,
78
80
  href: resource.links.find((link) => link.rel === "open")?.href,
@@ -87,6 +89,8 @@ export const collectConversationResourceObservations = (...values: unknown[]): A
87
89
  ...previous,
88
90
  ref,
89
91
  ...(resource.title !== undefined ? { title: resource.title } : {}),
92
+ ...(resource.context !== undefined ? { context: resource.context } : {}),
93
+ ...(resource.status !== undefined ? { status: resource.status } : {}),
90
94
  ...(resource.preview !== undefined ? { preview: resource.preview } : {}),
91
95
  ...(resource.icon !== undefined ? { icon: resource.icon } : {}),
92
96
  ...(resource.href !== undefined ? { href: resource.href } : {}),
package/src/ai/routes.ts CHANGED
@@ -37,7 +37,7 @@ import { coreSettings } from "../services/settings/api";
37
37
  import { resolveAppIdentityPresentation } from "../shared/app-presentation";
38
38
  import { readThemeFromCookieHeader } from "../shared/theme";
39
39
  import { type AiStoredActionReview, isLegacyActionReview, isUnknownActionOutcome, storedActionReview } from "./action-review";
40
- import { assistantAiSettingsState, listAssistantAiModels, resolveAssistantAudioModel, selectAssistantAiModelId } from "./assistant-models";
40
+ import { assistantAiSettingsState, peopleAiModels, resolveAssistantAudioModel, selectAssistantAiModel } from "./assistant-models";
41
41
  import { AI_AUDIO_MAX_BYTES } from "./audio-format";
42
42
  import { buildAiCapabilityCatalog } from "./capabilities";
43
43
  import { checkAiCapability, resolveAiCapabilityActor } from "./capability-execution";
@@ -254,10 +254,10 @@ const publicMemory = async (userId: string, memory: Awaited<ReturnType<typeof ai
254
254
  (await publicMemories(userId, [memory]))[0]!;
255
255
 
256
256
  /** Fire-and-forget: remember the model this user actually ran a turn with (preselected for new chats). */
257
- const rememberLastUsedModel = (actor: RequestActor, modelProfileId: string | null | undefined): void => {
257
+ const rememberLastUsedModel = (actor: RequestActor, model: import("../shared/ai-models").AiModelRequest | undefined): void => {
258
258
  const userId = aiPrefsUserId(actor);
259
- if (!userId || !modelProfileId) return;
260
- void aiUserPrefs.update(userId, { lastModelId: modelProfileId }).catch(() => undefined);
259
+ if (!userId || !model) return;
260
+ void aiUserPrefs.update(userId, { selection: model }).catch(() => undefined);
261
261
  };
262
262
 
263
263
  const conversationDetail = async (conversation: AiConversation, subject: AccessSubject) => {
@@ -348,7 +348,7 @@ export const aiRoutes = (() => {
348
348
  return c.json(await getAiChatQuotas(c.get("accessSubject")));
349
349
  })
350
350
  .get("/models", async (c) => {
351
- return respond(c, ok(await listAssistantAiModels(c.get("accessSubject"))));
351
+ return respond(c, ok(await peopleAiModels(c.get("accessSubject"))));
352
352
  })
353
353
  .get("/prefs", async (c) => {
354
354
  const ctx = await resolveContext(c);
@@ -438,12 +438,11 @@ export const aiRoutes = (() => {
438
438
  const state = await readAiSettingsState();
439
439
  let previewProfile: ReturnType<typeof selectAiModelProfile> | null = null;
440
440
  if (state.ok && state.enabled) {
441
- const models = await listAssistantAiModels(c.get("accessSubject"), ctx.modelPolicy);
442
- const preferredId =
443
- models.find((model) => model.id === prefs?.lastModelId)?.id ??
444
- models.find((model) => model.id === state.defaultModelId)?.id ??
445
- models[0]?.id;
446
- if (preferredId) previewProfile = selectAiModelProfile(state, ctx.modelPolicy, preferredId);
441
+ try {
442
+ previewProfile = selectAiModelProfile(state, ctx.modelPolicy, prefs.selection ?? undefined);
443
+ } catch {
444
+ /* The prompt preview remains available without a usable model. */
445
+ }
447
446
  }
448
447
  const toolsSupported = Boolean(previewProfile?.capabilities.includes("tools"));
449
448
  const availableSkills = toolsSupported
@@ -1022,7 +1021,7 @@ export const aiRoutes = (() => {
1022
1021
  locale: getLocale(c),
1023
1022
  theme: readThemeFromCookieHeader(c.req.header("cookie")),
1024
1023
  timeZone: getTimeZone(c),
1025
- requestedModelId: body.modelProfileId ?? project?.defaultModelProfileId ?? undefined,
1024
+ model: body.model ?? project?.defaultModel ?? undefined,
1026
1025
  modelPolicy: ctx.modelPolicy,
1027
1026
  project: project ?? undefined,
1028
1027
  clientToolIds: body.clientToolIds,
@@ -1043,7 +1042,7 @@ export const aiRoutes = (() => {
1043
1042
  return respond(c, ok({ queued: true }));
1044
1043
  }
1045
1044
  const result = await submitAiChatTurn(submission);
1046
- rememberLastUsedModel(ctx.actor, result.turn.modelProfileId);
1045
+ rememberLastUsedModel(ctx.actor, body.model);
1047
1046
  return respond(
1048
1047
  c,
1049
1048
  ok({
@@ -1115,9 +1114,9 @@ export const aiRoutes = (() => {
1115
1114
  if (originalProject && !currentProject) return respond(c, fail(err.notFound("Project")));
1116
1115
 
1117
1116
  try {
1118
- const requestedModelId = await selectAssistantAiModelId(
1117
+ const model = await selectAssistantAiModel(
1119
1118
  c.get("accessSubject"),
1120
- body.modelProfileId ?? originalProject?.defaultModelProfileId ?? undefined,
1119
+ body.model ?? originalProject?.defaultModel ?? undefined,
1121
1120
  ctx.modelPolicy,
1122
1121
  );
1123
1122
  if ((await prepareConversationForMessageRetry(conversation.id)) === "busy") {
@@ -1134,7 +1133,7 @@ export const aiRoutes = (() => {
1134
1133
  locale: getLocale(c),
1135
1134
  theme: readThemeFromCookieHeader(c.req.header("cookie")),
1136
1135
  timeZone: getTimeZone(c),
1137
- requestedModelId,
1136
+ model,
1138
1137
  modelPolicy: ctx.modelPolicy,
1139
1138
  systemPrompt,
1140
1139
  project: originalProject,
@@ -1158,7 +1157,7 @@ export const aiRoutes = (() => {
1158
1157
  }
1159
1158
  : undefined,
1160
1159
  });
1161
- rememberLastUsedModel(ctx.actor, result.turn.modelProfileId);
1160
+ rememberLastUsedModel(ctx.actor, body.model);
1162
1161
  return respond(
1163
1162
  c,
1164
1163
  ok({
@@ -1230,7 +1229,7 @@ export const aiRoutes = (() => {
1230
1229
  const result = await submitAiCompaction({
1231
1230
  conversationId: conversation.id,
1232
1231
  actor: ctx.actor,
1233
- requestedModelId: body.modelProfileId,
1232
+ model: body.model,
1234
1233
  modelPolicy: ctx.modelPolicy,
1235
1234
  });
1236
1235
  return respond(c, ok({ turn: publicTurn(result.turn, conversation.shortId) }), 201);
package/src/ai/runtime.ts CHANGED
@@ -8,6 +8,7 @@ import { type ApprovalDecisionChannel, approvals } from "../services/approvals";
8
8
  import { logger } from "../services/logging";
9
9
  import { superviseRuntimeTask } from "../services/runtime-lifecycle";
10
10
  import { coreSettings } from "../services/settings/api";
11
+ import type { AiModelRequest } from "../shared/ai-models";
11
12
  import {
12
13
  AI_WEBSITE_APPROVAL_TOOL,
13
14
  type AiToolApprovalChangeContext,
@@ -15,16 +16,17 @@ import {
15
16
  aiTurnAllowsRememberedApprovals,
16
17
  rememberAiToolApproval,
17
18
  } from "./approvals";
18
- import { aiChatAccessSubject, selectAssistantAiModelId } from "./assistant-models";
19
+ import { aiChatAccessSubject, selectAssistantAiModel } from "./assistant-models";
19
20
  import { startAiDictationRuntime } from "./dictation-runtime";
20
21
  import { AiTurnExecutor, type ExecutorConfig } from "./executor";
21
22
  import { canonicalizeAiConversationAttachments, snapshotAiConversationFiles } from "./file-context";
22
23
  import type { AiLocalClient, AiLocalContext } from "./local-tool-contracts";
23
24
  import { drainQueuedMessages } from "./message-queue";
25
+ import { aiUserPrefs } from "./prefs";
24
26
  import { AI_TURN_LEASE_MS } from "./protocol";
25
27
  import { aiQuotas } from "./quotas";
26
28
  import { parseAiResourceMarker } from "./resource-markers";
27
- import { isAiVisionModelConfigured } from "./settings";
29
+ import { isAiVisionModelConfigured, readAiSettingsState } from "./settings";
28
30
  import { aiConversations } from "./store";
29
31
  import { publishAiWireEvent } from "./stream";
30
32
  import { aiToolAudit } from "./tool-audit";
@@ -110,7 +112,7 @@ export type SubmitAiChatTurnInput = {
110
112
  theme?: "light" | "dark";
111
113
  timeZone?: string;
112
114
  modelPolicy?: AiModelPolicy;
113
- requestedModelId?: string;
115
+ model?: AiModelRequest;
114
116
  /** Optional instructions that apply only to this turn. */
115
117
  systemPrompt?: string;
116
118
  project?: AiChatTurnRunConfig["project"];
@@ -143,21 +145,43 @@ export const prepareAiChatTurn = async (input: SubmitAiChatTurnInput) => {
143
145
  );
144
146
  const canonicalInput = files?.attached.length ? inputMessage.content : input.input;
145
147
  const hasImageAttachments = Boolean(files?.attached.some((file) => isAiImageMediaType(file.mediaType)));
148
+ const state = await readAiSettingsState();
146
149
  const canInspectAttachedImages =
147
150
  input.canInspectAttachedImages ??
148
- (input.toolSource?.kind === "default" && (await isAiVisionModelConfigured(input.modelPolicy?.allowedDataBoundaries)));
149
- const requestedModelId = input.assistantChat
150
- ? await selectAssistantAiModelId(aiChatAccessSubject(input.actor), input.requestedModelId, input.modelPolicy)
151
- : input.requestedModelId;
152
- const { resolved } = await validateAiTurnRequest({
153
- input: canonicalInput,
154
- hasImageAttachments,
155
- canInspectAttachedImages,
156
- modelPolicy: input.modelPolicy,
157
- requestedModelId,
158
- });
151
+ (input.toolSource?.kind === "default" && (await isAiVisionModelConfigured(input.modelPolicy?.allowedDataBoundaries, state)));
152
+ let requested = input.model;
153
+ let previousLabel: string | undefined;
154
+ if (input.assistantChat) {
155
+ const latest = await aiConversations.getLatestTurn({ conversationId: input.conversationId, kind: "chat" });
156
+ const previous = latest ? await aiConversations.getTurnRunConfig({ conversationId: input.conversationId, turnId: latest.id }) : null;
157
+ const inherited = previous?.kind === "chat" ? previous.model : undefined;
158
+ requested ??= inherited;
159
+ if (inherited && "id" in inherited && requested && "id" in requested && requested.id === inherited.id) {
160
+ if (!state.profiles.some((profile) => profile.id === inherited.id)) {
161
+ previousLabel = previous?.kind === "chat" ? (previous.modelLabel ?? inherited.id) : inherited.id;
162
+ const subject = aiChatAccessSubject(input.actor);
163
+ const prefs = subject?.type === "user" ? await aiUserPrefs.get(subject.userId) : null;
164
+ requested = prefs?.selection && "class" in prefs.selection ? prefs.selection : { class: "balanced" };
165
+ }
166
+ }
167
+ }
168
+ const model = input.assistantChat
169
+ ? await selectAssistantAiModel(aiChatAccessSubject(input.actor), requested, input.modelPolicy, state)
170
+ : requested;
171
+ const { resolved } = await validateAiTurnRequest(
172
+ {
173
+ input: canonicalInput,
174
+ hasImageAttachments,
175
+ canInspectAttachedImages,
176
+ modelPolicy: input.modelPolicy,
177
+ model,
178
+ },
179
+ state,
180
+ );
159
181
  const quotaSubject = aiChatAccessSubject(input.actor);
160
- if (input.assistantChat && quotaSubject) await aiQuotas.assertAllowed(quotaSubject, resolved.profile.id);
182
+ if (input.assistantChat && quotaSubject) await aiQuotas.assertAllowed(quotaSubject, resolved.profile.id, state);
183
+ const modelLabel =
184
+ resolved.profile.label + (resolved.profile.id !== resolved.profile.modelId ? ` · ${resolved.profile.variantLabel}` : "");
161
185
  const runConfig: AiChatTurnRunConfig = {
162
186
  kind: "chat",
163
187
  selectedSkillIds: [
@@ -177,7 +201,10 @@ export const prepareAiChatTurn = async (input: SubmitAiChatTurnInput) => {
177
201
  ...(input.theme ? { theme: input.theme } : {}),
178
202
  ...(input.timeZone ? { timeZone: input.timeZone } : {}),
179
203
  modelPolicy: input.modelPolicy,
180
- requestedModelId,
204
+ model,
205
+ modelResolved: resolved.model,
206
+ modelLabel,
207
+ ...(previousLabel ? { modelReplacement: { kind: "model_replaced", previousLabel, nextLabel: modelLabel } } : {}),
181
208
  systemPrompt: input.systemPrompt,
182
209
  project: input.project,
183
210
  files,
@@ -191,7 +218,7 @@ export const prepareAiChatTurn = async (input: SubmitAiChatTurnInput) => {
191
218
 
192
219
  return {
193
220
  conversationId: input.conversationId,
194
- modelProfileId: resolved.profile.id,
221
+ modelId: resolved.profile.id,
195
222
  runConfig,
196
223
  userMessage,
197
224
  truncateFromSeq: input.truncateFromSeq,
@@ -244,7 +271,7 @@ export const deliverAiInterChatMessage = async (input: {
244
271
  };
245
272
  const delivered = await aiConversations.deliverInterChatMessage({
246
273
  messageId: input.message.id,
247
- modelProfileId: resolved.profile.id,
274
+ modelId: resolved.profile.id,
248
275
  runConfig,
249
276
  userMessage: { role: "user", content: [{ type: "text", text }] },
250
277
  sourceHref: input.sourceHref,
@@ -257,20 +284,21 @@ export type SubmitAiCompactionInput = {
257
284
  conversationId: string;
258
285
  actor?: RequestActor;
259
286
  modelPolicy?: AiModelPolicy;
260
- requestedModelId?: string;
287
+ model?: AiModelRequest;
261
288
  };
262
289
 
263
290
  export const submitAiCompaction = async (input: SubmitAiCompactionInput): Promise<{ turn: AiTurn }> => {
264
- const { resolved } = await validateAiTurnRequest({ input: "", modelPolicy: input.modelPolicy, requestedModelId: input.requestedModelId });
291
+ const { resolved } = await validateAiTurnRequest({ input: "", modelPolicy: input.modelPolicy, model: input.model });
265
292
  const runConfig: AiCompactionTurnRunConfig = {
266
293
  kind: "compact",
267
294
  actor: input.actor,
268
295
  modelPolicy: input.modelPolicy,
269
- requestedModelId: input.requestedModelId,
296
+ model: { id: resolved.profile.id },
297
+ modelResolved: resolved.model,
270
298
  };
271
299
  const turn = await aiConversations.createCompactionTurn({
272
300
  conversationId: input.conversationId,
273
- modelProfileId: resolved.profile.id,
301
+ modelId: resolved.profile.id,
274
302
  runConfig,
275
303
  });
276
304
  await enqueueAiTurn({ conversationId: input.conversationId, turnId: turn.id });