@opengeni/core 0.28.0 → 1.0.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 (224) hide show
  1. package/README.md +1 -1
  2. package/dist/access/external-actor-authority.d.ts +47 -0
  3. package/dist/access/index.d.ts +182 -3
  4. package/dist/access/service-initiator.d.ts +3 -0
  5. package/dist/application/api-integration-servers.d.ts +20 -0
  6. package/dist/application/archived-session-imports.d.ts +6 -0
  7. package/dist/application/composer-submit.d.ts +24 -0
  8. package/dist/application/connect-authority.d.ts +13 -0
  9. package/dist/application/connect-operation.d.ts +31 -0
  10. package/dist/application/external-continuation.d.ts +15 -0
  11. package/dist/application/external-identity-lifecycle.d.ts +20 -0
  12. package/dist/application/external-link-work-admission.d.ts +12 -0
  13. package/dist/application/external-workspace-members.d.ts +34 -0
  14. package/dist/application/modal-native-worker-host-transport.d.ts +297 -0
  15. package/dist/application/new-session-drafts.d.ts +39 -2
  16. package/dist/application/sandbox-recovery.d.ts +58 -0
  17. package/dist/application/session-commands.d.ts +26 -13
  18. package/dist/application/session-mcp-credential-rotation.d.ts +29 -0
  19. package/dist/application/session-tenancy.d.ts +19 -0
  20. package/dist/application/user-resource-grants.d.ts +20 -0
  21. package/dist/billing/limits.d.ts +11 -0
  22. package/dist/canonical-human-identities.d.ts +7 -2
  23. package/dist/canonical-human-identities.js +17 -4
  24. package/dist/canonical-human-identities.js.map +1 -1
  25. package/dist/{chunk-6WKPWE5S.js → chunk-2MKMRGSY.js} +108 -24
  26. package/dist/chunk-2MKMRGSY.js.map +1 -0
  27. package/dist/{chunk-NYPMQ7NO.js → chunk-GVO3N37G.js} +205 -37
  28. package/dist/chunk-GVO3N37G.js.map +1 -0
  29. package/dist/{chunk-JJU6I5XD.js → chunk-LCQXNR62.js} +36 -10
  30. package/dist/chunk-LCQXNR62.js.map +1 -0
  31. package/dist/chunk-MM6N7CTO.js +387 -0
  32. package/dist/chunk-MM6N7CTO.js.map +1 -0
  33. package/dist/chunk-QO5GVFFO.js +235 -0
  34. package/dist/chunk-QO5GVFFO.js.map +1 -0
  35. package/dist/codex-model-availability.d.ts +14 -0
  36. package/dist/default-session-model.d.ts +139 -0
  37. package/dist/dependencies.d.ts +87 -4
  38. package/dist/developer-setup-credential-ceiling.d.ts +10 -0
  39. package/dist/domain/agent-config-resolution.d.ts +49 -0
  40. package/dist/domain/automations.d.ts +58 -0
  41. package/dist/domain/capabilities.d.ts +22 -4
  42. package/dist/domain/company-profile-agent-admin.d.ts +25 -0
  43. package/dist/domain/connection-account-selection-error.d.ts +8 -0
  44. package/dist/domain/connector-tool-permissions.d.ts +36 -0
  45. package/dist/domain/direct-model-provider.d.ts +3 -0
  46. package/dist/domain/editable-artifacts/agent-application.d.ts +10 -1
  47. package/dist/domain/editable-artifacts/durable-export.d.ts +22 -1
  48. package/dist/domain/editable-artifacts/ports.d.ts +10 -0
  49. package/dist/domain/editable-artifacts/service.d.ts +4 -3
  50. package/dist/domain/environments.d.ts +5 -9
  51. package/dist/domain/external-creation-attribution.d.ts +6 -0
  52. package/dist/domain/file-owner.d.ts +8 -0
  53. package/dist/domain/github-action-policies.d.ts +36 -0
  54. package/dist/domain/github-repository-bindings.d.ts +117 -0
  55. package/dist/domain/github-skill-source.d.ts +5 -0
  56. package/dist/domain/governed-learning-slack-publication.d.ts +45 -0
  57. package/dist/domain/insights-timing.d.ts +16 -0
  58. package/dist/domain/insights-usage.d.ts +27 -0
  59. package/dist/domain/insights.d.ts +28 -1
  60. package/dist/domain/knowledge-files.d.ts +20 -0
  61. package/dist/domain/knowledge-messages.d.ts +39 -0
  62. package/dist/domain/knowledge-messages.test.d.ts +1 -0
  63. package/dist/domain/knowledge-preparation.d.ts +10 -0
  64. package/dist/domain/knowledge-preparation.test.d.ts +1 -0
  65. package/dist/domain/knowledge-search.d.ts +15 -0
  66. package/dist/domain/knowledge-search.test.d.ts +1 -0
  67. package/dist/domain/knowledge.d.ts +9 -0
  68. package/dist/domain/mcp-account-bindings.d.ts +19 -0
  69. package/dist/domain/mcp-account-routes.d.ts +21 -0
  70. package/dist/domain/native-mcp-connection-admission.d.ts +3 -0
  71. package/dist/domain/organization-integration-catalog.d.ts +15 -0
  72. package/dist/domain/organization-membership-lifecycle.d.ts +1 -0
  73. package/dist/domain/personal-connection-delegations.d.ts +87 -4
  74. package/dist/domain/pr-review.d.ts +249 -0
  75. package/dist/domain/resources.d.ts +24 -3
  76. package/dist/domain/scheduled-task-access.d.ts +247 -0
  77. package/dist/domain/scheduled-tasks.d.ts +103 -6
  78. package/dist/domain/session-connection-accounts.d.ts +16 -0
  79. package/dist/domain/session-file-access.d.ts +4 -0
  80. package/dist/domain/session-native-connection-replacement.d.ts +14 -0
  81. package/dist/domain/session-start-timing.d.ts +7 -0
  82. package/dist/domain/session-tool-policy.d.ts +93 -4
  83. package/dist/domain/sessions.d.ts +275 -28
  84. package/dist/domain/skill-imports.d.ts +1 -0
  85. package/dist/domain/skill-search.d.ts +30 -0
  86. package/dist/domain/skills.d.ts +117 -0
  87. package/dist/domain/slack-bot.d.ts +46 -1
  88. package/dist/domain/workspace-members.d.ts +8 -1
  89. package/dist/editable-artifact-live/application.d.ts +12 -3
  90. package/dist/editable-artifact-live/ports.d.ts +0 -1
  91. package/dist/editable-artifact-live/server.d.ts +5 -2
  92. package/dist/editable-artifact-live/ticket.d.ts +2 -2
  93. package/dist/editable-artifact-live/types.d.ts +14 -2
  94. package/dist/editable-artifact-live.js +2 -2
  95. package/dist/editable-artifacts.js +10 -4
  96. package/dist/index.d.ts +49 -3
  97. package/dist/index.js +18826 -6847
  98. package/dist/index.js.map +1 -1
  99. package/dist/managed-auth-session-sets.d.ts +131 -0
  100. package/dist/managed-auth-session-sets.js +51 -0
  101. package/dist/managed-auth-session-sets.js.map +1 -0
  102. package/dist/managed-session.d.ts +98 -11
  103. package/dist/model-catalog.d.ts +117 -0
  104. package/dist/product-usage-metrics.d.ts +30 -0
  105. package/dist/rigs/index.d.ts +22 -5
  106. package/dist/rigs/provider-images.d.ts +1 -1
  107. package/dist/sandbox/fleet.d.ts +59 -10
  108. package/dist/sandbox/routing.d.ts +22 -2
  109. package/dist/sandbox/runtime-settings.d.ts +56 -0
  110. package/dist/session-authorization.d.ts +38 -1
  111. package/dist/site-session-origin.d.ts +9 -0
  112. package/dist/transcription.d.ts +12 -1
  113. package/dist/turn-surface.d.ts +18 -0
  114. package/dist/user-presence.d.ts +32 -0
  115. package/package.json +18 -11
  116. package/src/access/external-actor-authority.ts +94 -0
  117. package/src/access/index.ts +1386 -44
  118. package/src/access/service-initiator.ts +50 -0
  119. package/src/application/api-integration-servers.ts +215 -0
  120. package/src/application/archived-session-imports.ts +161 -0
  121. package/src/application/composer-submit.ts +77 -0
  122. package/src/application/connect-authority.ts +77 -0
  123. package/src/application/connect-operation.ts +80 -0
  124. package/src/application/external-continuation.ts +129 -0
  125. package/src/application/external-identity-lifecycle.ts +48 -0
  126. package/src/application/external-link-work-admission.ts +90 -0
  127. package/src/application/external-workspace-members.ts +232 -0
  128. package/src/application/modal-native-worker-host-transport.ts +263 -0
  129. package/src/application/new-session-drafts.ts +222 -29
  130. package/src/application/sandbox-recovery.ts +78 -0
  131. package/src/application/session-commands.ts +430 -73
  132. package/src/application/session-mcp-credential-rotation.ts +217 -0
  133. package/src/application/session-tenancy.ts +445 -0
  134. package/src/application/user-resource-grants.ts +154 -0
  135. package/src/billing/limits.ts +103 -24
  136. package/src/canonical-human-identities.ts +22 -3
  137. package/src/codex-model-availability.ts +136 -0
  138. package/src/default-session-model.ts +656 -0
  139. package/src/dependencies.ts +97 -5
  140. package/src/developer-setup-credential-ceiling.ts +60 -0
  141. package/src/domain/agent-config-resolution.ts +148 -0
  142. package/src/domain/automations.ts +270 -0
  143. package/src/domain/capabilities.ts +244 -151
  144. package/src/domain/company-profile-agent-admin.ts +56 -0
  145. package/src/domain/connection-account-selection-error.ts +19 -0
  146. package/src/domain/connector-tool-permissions.ts +371 -0
  147. package/src/domain/direct-model-provider.ts +62 -0
  148. package/src/domain/editable-artifacts/agent-application.ts +61 -15
  149. package/src/domain/editable-artifacts/durable-export.ts +100 -5
  150. package/src/domain/editable-artifacts/in-memory.ts +6 -0
  151. package/src/domain/editable-artifacts/ports.ts +11 -0
  152. package/src/domain/editable-artifacts/service.ts +32 -1
  153. package/src/domain/editable-artifacts/snapshot-compaction.ts +9 -5
  154. package/src/domain/editable-artifacts/snapshot-verifier-genesis.ts +10 -8
  155. package/src/domain/editable-artifacts/snapshot-verifier.ts +10 -3
  156. package/src/domain/environments.ts +25 -45
  157. package/src/domain/external-creation-attribution.ts +22 -0
  158. package/src/domain/file-owner.ts +67 -0
  159. package/src/domain/github-action-policies.ts +169 -0
  160. package/src/domain/github-repository-bindings.ts +295 -0
  161. package/src/domain/github-skill-source.ts +233 -0
  162. package/src/domain/governed-learning-slack-publication.ts +157 -0
  163. package/src/domain/insights-timing.ts +95 -0
  164. package/src/domain/insights-usage.ts +67 -0
  165. package/src/domain/insights.ts +270 -169
  166. package/src/domain/knowledge-files.ts +46 -0
  167. package/src/domain/knowledge-messages.test.ts +91 -0
  168. package/src/domain/knowledge-messages.ts +106 -0
  169. package/src/domain/knowledge-preparation.test.ts +276 -0
  170. package/src/domain/knowledge-preparation.ts +147 -0
  171. package/src/domain/knowledge-search.test.ts +561 -0
  172. package/src/domain/knowledge-search.ts +256 -0
  173. package/src/domain/knowledge.ts +133 -0
  174. package/src/domain/mcp-account-bindings.ts +171 -0
  175. package/src/domain/mcp-account-routes.ts +121 -0
  176. package/src/domain/native-mcp-connection-admission.ts +14 -0
  177. package/src/domain/organization-integration-catalog.ts +88 -0
  178. package/src/domain/organization-membership-lifecycle.ts +10 -0
  179. package/src/domain/personal-connection-delegations.ts +758 -115
  180. package/src/domain/pr-review.ts +687 -0
  181. package/src/domain/resources.ts +182 -25
  182. package/src/domain/scheduled-task-access.ts +1052 -0
  183. package/src/domain/scheduled-tasks.ts +1952 -215
  184. package/src/domain/session-connection-accounts.ts +67 -0
  185. package/src/domain/session-file-access.ts +47 -0
  186. package/src/domain/session-native-connection-replacement.ts +64 -0
  187. package/src/domain/session-start-timing.ts +55 -0
  188. package/src/domain/session-tool-policy.ts +443 -19
  189. package/src/domain/sessions.ts +3417 -619
  190. package/src/domain/skill-imports.ts +137 -17
  191. package/src/domain/skill-search.ts +221 -0
  192. package/src/domain/skills.ts +104 -0
  193. package/src/domain/slack-bot.ts +128 -3
  194. package/src/domain/workspace-members.ts +34 -2
  195. package/src/editable-artifact-live/application.ts +29 -4
  196. package/src/editable-artifact-live/ports.ts +0 -1
  197. package/src/editable-artifact-live/server.ts +167 -13
  198. package/src/editable-artifact-live/ticket.ts +87 -6
  199. package/src/editable-artifact-live/types.ts +16 -2
  200. package/src/editable-artifact-live/wire.ts +0 -10
  201. package/src/index.ts +69 -4
  202. package/src/managed-auth-session-sets.ts +371 -0
  203. package/src/managed-session.ts +520 -2
  204. package/src/model-catalog.ts +873 -0
  205. package/src/product-usage-metrics.ts +101 -0
  206. package/src/rigs/index.ts +179 -63
  207. package/src/rigs/provider-images.ts +2 -3
  208. package/src/sandbox/fleet.ts +471 -59
  209. package/src/sandbox/routing.ts +160 -33
  210. package/src/sandbox/runtime-settings.ts +281 -0
  211. package/src/session-authorization.ts +262 -78
  212. package/src/site-session-origin.ts +22 -0
  213. package/src/transcription.ts +13 -1
  214. package/src/turn-surface.ts +45 -0
  215. package/src/user-presence.ts +134 -0
  216. package/dist/chunk-6WKPWE5S.js.map +0 -1
  217. package/dist/chunk-IBOEYG6N.js +0 -34
  218. package/dist/chunk-IBOEYG6N.js.map +0 -1
  219. package/dist/chunk-JJU6I5XD.js.map +0 -1
  220. package/dist/chunk-NYPMQ7NO.js.map +0 -1
  221. package/dist/domain/durable-learning-slack-publication.d.ts +0 -72
  222. package/dist/domain/packs.d.ts +0 -55
  223. package/src/domain/durable-learning-slack-publication.ts +0 -202
  224. package/src/domain/packs.ts +0 -556
@@ -0,0 +1,656 @@
1
+ import {
2
+ canonicalizeConfiguredModelId,
3
+ CLAUDE_CONNECTION_KINDS,
4
+ type ClaudeConnectionCatalog,
5
+ type ConfiguredModel,
6
+ type Settings,
7
+ } from "@opengeni/config";
8
+ import {
9
+ resolveWorkspaceSessionDefaults,
10
+ type DefaultModelSelection,
11
+ type ReasoningEffort,
12
+ type ScheduledTask,
13
+ type WorkspaceSessionDefaults,
14
+ type XaiProviderAccountAuthoritySnapshotV1,
15
+ type ClaudeProviderAccountAuthoritySnapshotV1,
16
+ } from "@opengeni/contracts";
17
+ import {
18
+ getScheduledTaskXaiProviderAccountAuthoritySnapshot,
19
+ getScheduledTaskClaudeProviderAccountAuthoritySnapshot,
20
+ getSession,
21
+ getWorkspace,
22
+ getWorkspaceConnectionModelRestrictions,
23
+ getWorkspaceModelPolicy,
24
+ getWorkspaceProviderApiKeyConnectionMetadata,
25
+ listWorkspaceProviderCustomModels,
26
+ listOrganizationModelProviderCustomModelsForWorkspace,
27
+ listWorkspaceGatewayCustomModels,
28
+ listWorkspaceOpenRouterCustomModels,
29
+ organizationModelProviderConnectionActiveForWorkspace,
30
+ workspaceCodexSubscriptionActive,
31
+ workspaceOpenRouterConnectionActive,
32
+ workspaceVercelAiGatewayConnectionActive,
33
+ organizationHoldsCredits,
34
+ workspaceXaiSubscriptionActive,
35
+ workspaceXaiSubscriptionActiveForAuthority,
36
+ XaiAuthorityPoolInactiveError,
37
+ ClaudeAuthorityPoolInactiveError,
38
+ resolveClaudeProviderAccountAuthoritySnapshotForAcceptance,
39
+ workspaceClaudeSubscriptionActiveForAuthority,
40
+ type ConnectionModelRestrictions,
41
+ type Database,
42
+ } from "@opengeni/db";
43
+ import {
44
+ isWorkspaceModelAdmissible,
45
+ resolveWorkspaceModelSelection,
46
+ type WorkspaceModelSelection,
47
+ type WorkspaceModelSelectionInput,
48
+ } from "./model-catalog";
49
+
50
+ import { loadWorkspaceCodexModelAvailability } from "./codex-model-availability";
51
+
52
+ const REASONING_EFFORT_ORDER: readonly ReasoningEffort[] = [
53
+ "none",
54
+ "minimal",
55
+ "low",
56
+ "medium",
57
+ "high",
58
+ "xhigh",
59
+ "max",
60
+ ];
61
+
62
+ function supportedReasoningEfforts(model: ConfiguredModel): ReasoningEffort[] {
63
+ const efforts = model.capabilities.reasoning.efforts;
64
+ return REASONING_EFFORT_ORDER.filter((effort) => efforts.includes(effort));
65
+ }
66
+
67
+ /** The model's own default effort, matching the web picker's choice. */
68
+ export function defaultReasoningEffortForConfiguredModel(
69
+ model: ConfiguredModel,
70
+ fallback: ReasoningEffort,
71
+ ): ReasoningEffort {
72
+ const options = supportedReasoningEfforts(model);
73
+ if (options.length === 0) return fallback;
74
+ const configured = model.capabilities.reasoning.defaultEffort;
75
+ if (configured && options.includes(configured)) return configured;
76
+ return options[0]!;
77
+ }
78
+
79
+ /** The highest supported effort at or below `preferred`. */
80
+ export function clampReasoningEffortForConfiguredModel(
81
+ model: ConfiguredModel,
82
+ preferred: ReasoningEffort,
83
+ fallback: ReasoningEffort,
84
+ ): ReasoningEffort {
85
+ const options = supportedReasoningEfforts(model);
86
+ if (options.length === 0) return fallback;
87
+ if (options.includes(preferred)) return preferred;
88
+ const ceiling = REASONING_EFFORT_ORDER.indexOf(preferred);
89
+ const below = options.filter((effort) => REASONING_EFFORT_ORDER.indexOf(effort) <= ceiling);
90
+ return below.at(-1) ?? defaultReasoningEffortForConfiguredModel(model, fallback);
91
+ }
92
+
93
+ function findSelection(
94
+ selections: readonly WorkspaceModelSelection[],
95
+ modelId: string,
96
+ ): WorkspaceModelSelection | undefined {
97
+ return (
98
+ selections.find((selection) => selection.model.id === modelId) ??
99
+ selections.find((selection) => selection.model.aliases.includes(modelId))
100
+ );
101
+ }
102
+
103
+ export type DefaultSessionModelInput = {
104
+ /** Catalog-resolved settings: `openaiModel` is the deployment default. */
105
+ settings: Settings;
106
+ /** This workspace's selection, in operator catalog order. */
107
+ selections: readonly WorkspaceModelSelection[];
108
+ workspaceDefaults: WorkspaceSessionDefaults | null;
109
+ /**
110
+ * True while the organization holds a positive OpenGeni credit balance from
111
+ * any source, the verified-signup trial grant included (see
112
+ * `organizationHoldsCredits`).
113
+ */
114
+ creditsAvailable: boolean;
115
+ };
116
+
117
+ function creditsCandidate(
118
+ input: Pick<DefaultSessionModelInput, "settings" | "selections">,
119
+ ): DefaultModelSelection | null {
120
+ const fallbackEffort = input.settings.openaiReasoningEffort;
121
+ const deployment = findSelection(input.selections, input.settings.openaiModel);
122
+ const configured = findSelection(input.selections, input.settings.creditsDefaultModel);
123
+ if (deployment?.availability.selectable && deployment.model.cost === "credits") {
124
+ // An operator's paid deployment default is never replaced. When it is the
125
+ // credits default model itself, credit holders get the credits default
126
+ // effort rather than the deployment-wide fallback effort.
127
+ if (configured?.model.id !== deployment.model.id) return null;
128
+ return {
129
+ model: deployment.model.id,
130
+ reasoningEffort: clampReasoningEffortForConfiguredModel(
131
+ deployment.model,
132
+ input.settings.creditsDefaultReasoningEffort,
133
+ fallbackEffort,
134
+ ),
135
+ source: "credits",
136
+ };
137
+ }
138
+ if (configured?.availability.selectable && configured.model.cost === "credits") {
139
+ return {
140
+ model: configured.model.id,
141
+ reasoningEffort: clampReasoningEffortForConfiguredModel(
142
+ configured.model,
143
+ input.settings.creditsDefaultReasoningEffort,
144
+ fallbackEffort,
145
+ ),
146
+ source: "credits",
147
+ };
148
+ }
149
+ const first = input.selections.find(
150
+ (selection) => selection.availability.selectable && selection.model.cost === "credits",
151
+ );
152
+ return first
153
+ ? {
154
+ model: first.model.id,
155
+ reasoningEffort: defaultReasoningEffortForConfiguredModel(first.model, fallbackEffort),
156
+ source: "credits",
157
+ }
158
+ : null;
159
+ }
160
+
161
+ /**
162
+ * Default model policy for a new chat or scheduled task that names no model.
163
+ *
164
+ * Precedence, first match wins:
165
+ *
166
+ * 1. `workspace`: the saved workspace default (`settings.sessionDefaults`)
167
+ * while it is selectable in this workspace, its saved effort clamped to
168
+ * what the model supports today.
169
+ * 2. `subscription`: the first selectable connected-subscription model
170
+ * (ChatGPT/Codex, then SuperGrok) in operator catalog order. The
171
+ * deployment default wins inside this step when it is itself a selectable
172
+ * subscription model.
173
+ * 3. `credits`: while the organization holds a positive OpenGeni credit
174
+ * balance (a purchase, a grant, or the verified-signup trial grant), the
175
+ * configured credits default (`OPENGENI_CREDITS_DEFAULT_MODEL`, effort
176
+ * clamped to what the model supports), or the first selectable
177
+ * credits-billed model when that one is not selectable. When the
178
+ * deployment default is already a selectable credits-billed model it is
179
+ * never replaced: it keeps the deployment effort, except that it takes the
180
+ * credits default effort when it is the credits default model itself.
181
+ * 4. `deployment`: the deployment default with the deployment reasoning effort
182
+ * when stably admissible; otherwise the first stably admissible catalog
183
+ * model with its own default effort. With no admitted models, retain the
184
+ * deployment hint and let fresh creation refuse it.
185
+ *
186
+ * An explicit model on the request, the scheduled task, or the person's
187
+ * new-chat draft is never passed through this function.
188
+ */
189
+ export function selectDefaultSessionModel(input: DefaultSessionModelInput): DefaultModelSelection {
190
+ const fallbackEffort = input.settings.openaiReasoningEffort;
191
+ if (input.workspaceDefaults) {
192
+ const saved = findSelection(input.selections, input.workspaceDefaults.model);
193
+ if (saved?.availability.selectable) {
194
+ return {
195
+ model: saved.model.id,
196
+ reasoningEffort: clampReasoningEffortForConfiguredModel(
197
+ saved.model,
198
+ input.workspaceDefaults.reasoningEffort,
199
+ fallbackEffort,
200
+ ),
201
+ source: "workspace",
202
+ };
203
+ }
204
+ }
205
+ const deployment = findSelection(input.selections, input.settings.openaiModel);
206
+ const subscription =
207
+ deployment?.availability.selectable && deployment.model.cost === "subscription"
208
+ ? deployment
209
+ : input.selections.find(
210
+ (selection) =>
211
+ selection.availability.selectable && selection.model.cost === "subscription",
212
+ );
213
+ if (subscription) {
214
+ return {
215
+ model: subscription.model.id,
216
+ reasoningEffort: defaultReasoningEffortForConfiguredModel(subscription.model, fallbackEffort),
217
+ source: "subscription",
218
+ };
219
+ }
220
+ if (input.creditsAvailable) {
221
+ const credits = creditsCandidate(input);
222
+ if (credits) return credits;
223
+ }
224
+ if (deployment && isWorkspaceModelAdmissible(deployment)) {
225
+ return { model: deployment.model.id, reasoningEffort: fallbackEffort, source: "deployment" };
226
+ }
227
+ const fallback = input.selections.find(isWorkspaceModelAdmissible);
228
+ return fallback
229
+ ? {
230
+ model: fallback.model.id,
231
+ reasoningEffort: defaultReasoningEffortForConfiguredModel(fallback.model, fallbackEffort),
232
+ source: "deployment",
233
+ }
234
+ : {
235
+ model: canonicalizeConfiguredModelId(input.settings, input.settings.openaiModel),
236
+ reasoningEffort: fallbackEffort,
237
+ source: "deployment",
238
+ };
239
+ }
240
+
241
+ /**
242
+ * The default this workspace would use while its organization holds a
243
+ * positive OpenGeni credit balance. Null when the deployment does not bill
244
+ * credits.
245
+ */
246
+ export function creditsDefaultSessionModel(input: {
247
+ settings: Settings;
248
+ selections: readonly WorkspaceModelSelection[];
249
+ workspaceSettings: unknown;
250
+ }): DefaultModelSelection | null {
251
+ if (input.settings.billingMode !== "stripe") return null;
252
+ return selectDefaultSessionModel({
253
+ settings: input.settings,
254
+ selections: input.selections,
255
+ workspaceDefaults: resolveWorkspaceSessionDefaults(input.workspaceSettings),
256
+ creditsAvailable: true,
257
+ });
258
+ }
259
+
260
+ /**
261
+ * Resolve the default for an already-loaded workspace selection. The credit
262
+ * ledger is read only when it can change the answer.
263
+ */
264
+ export async function resolveDefaultSessionModelForSelections(
265
+ db: Database,
266
+ input: {
267
+ settings: Settings;
268
+ accountId: string;
269
+ workspaceSettings: unknown;
270
+ selections: readonly WorkspaceModelSelection[];
271
+ },
272
+ ): Promise<DefaultModelSelection> {
273
+ const decision = {
274
+ settings: input.settings,
275
+ selections: input.selections,
276
+ workspaceDefaults: resolveWorkspaceSessionDefaults(input.workspaceSettings),
277
+ };
278
+ const withoutCredits = selectDefaultSessionModel({ ...decision, creditsAvailable: false });
279
+ if (
280
+ withoutCredits.source !== "deployment" ||
281
+ input.settings.billingMode !== "stripe" ||
282
+ creditsCandidate(decision) === null
283
+ ) {
284
+ return withoutCredits;
285
+ }
286
+ return (await organizationHoldsCredits(db, input.accountId))
287
+ ? selectDefaultSessionModel({ ...decision, creditsAvailable: true })
288
+ : withoutCredits;
289
+ }
290
+
291
+ export type WorkspaceModelSelectionContext = {
292
+ accountId: string;
293
+ workspaceId: string;
294
+ /**
295
+ * The subject whose connected-subscription authority applies: the
296
+ * authenticated caller for direct creates, or a scheduled task's immutable
297
+ * execution owner. Never another member.
298
+ */
299
+ subjectId: string;
300
+ /**
301
+ * An already-frozen SuperGrok authority (a scheduled task's snapshot).
302
+ * Omitted means the subject's current acceptance authority, exactly as a
303
+ * direct Send resolves it.
304
+ */
305
+ xaiAuthoritySnapshot?: XaiProviderAccountAuthoritySnapshotV1 | undefined;
306
+ /** Already accepted Claude pool; never replace it with current selection. */
307
+ claudeAuthoritySnapshot?: ClaudeProviderAccountAuthoritySnapshotV1 | undefined;
308
+ };
309
+
310
+ /**
311
+ * Connection restrictions and SuperGrok readiness for the subject. With a
312
+ * frozen authority (a scheduled task's snapshot), a user pool that no longer
313
+ * resolves (disconnected, reconnected under a new authority generation, or its
314
+ * owner left) means SuperGrok is not ready: no SuperGrok model is selectable,
315
+ * and resolution falls through to credits or the deployment default instead of
316
+ * failing the occurrence. The frozen authority is never swapped for the
317
+ * subject's current one; the SuperGrok restriction is closed outright, while
318
+ * the other providers' restrictions do not depend on SuperGrok authority.
319
+ */
320
+ async function connectionRestrictionsAndXaiReadiness(
321
+ db: Database,
322
+ settings: Settings,
323
+ context: WorkspaceModelSelectionContext,
324
+ ): Promise<{ restrictions: ConnectionModelRestrictions; xaiSubscriptionActive: boolean }> {
325
+ const { workspaceId, subjectId, xaiAuthoritySnapshot } = context;
326
+ if (!xaiAuthoritySnapshot) {
327
+ const [restrictions, xaiSubscriptionActive] = await Promise.all([
328
+ getWorkspaceConnectionModelRestrictions(
329
+ db,
330
+ workspaceId,
331
+ subjectId,
332
+ undefined,
333
+ context.claudeAuthoritySnapshot,
334
+ ),
335
+ workspaceXaiSubscriptionActive(db, settings, workspaceId, subjectId),
336
+ ]);
337
+ return { restrictions, xaiSubscriptionActive };
338
+ }
339
+ const frozen = await Promise.allSettled([
340
+ getWorkspaceConnectionModelRestrictions(
341
+ db,
342
+ workspaceId,
343
+ subjectId,
344
+ xaiAuthoritySnapshot,
345
+ context.claudeAuthoritySnapshot,
346
+ ),
347
+ workspaceXaiSubscriptionActiveForAuthority(db, settings, {
348
+ workspaceId,
349
+ subjectId,
350
+ authoritySnapshot: xaiAuthoritySnapshot,
351
+ }),
352
+ ]);
353
+ const [restrictions, readiness] = frozen;
354
+ if (restrictions.status === "fulfilled" && readiness.status === "fulfilled") {
355
+ return { restrictions: restrictions.value, xaiSubscriptionActive: readiness.value };
356
+ }
357
+ for (const result of frozen) {
358
+ if (result.status === "rejected" && !(result.reason instanceof XaiAuthorityPoolInactiveError)) {
359
+ throw result.reason;
360
+ }
361
+ }
362
+ const current = await getWorkspaceConnectionModelRestrictions(
363
+ db,
364
+ workspaceId,
365
+ subjectId,
366
+ undefined,
367
+ context.claudeAuthoritySnapshot,
368
+ );
369
+ return {
370
+ restrictions: { ...current, "supergrok/": [] },
371
+ xaiSubscriptionActive: false,
372
+ };
373
+ }
374
+
375
+ /**
376
+ * Readiness for the subject's current or already accepted Claude pool observes
377
+ * metadata only; quota is handled by the runtime allocator. This is not an
378
+ * authorization: callers must supply their authenticated or frozen subject.
379
+ */
380
+ export async function loadWorkspaceClaudeSubscriptionReadiness(
381
+ db: Database,
382
+ settings: Settings,
383
+ context: WorkspaceModelSelectionContext,
384
+ ): Promise<{ workspace: boolean; organization: boolean }> {
385
+ if (!settings.claudeSubscriptionEnabled) return { workspace: false, organization: false };
386
+ const authoritySnapshot =
387
+ context.claudeAuthoritySnapshot ??
388
+ (await resolveClaudeProviderAccountAuthoritySnapshotForAcceptance(db, context));
389
+ let active: boolean;
390
+ try {
391
+ active = await workspaceClaudeSubscriptionActiveForAuthority(db, settings, {
392
+ ...context,
393
+ authoritySnapshot,
394
+ });
395
+ } catch (error) {
396
+ if (!context.claudeAuthoritySnapshot || !(error instanceof ClaudeAuthorityPoolInactiveError))
397
+ throw error;
398
+ active = false;
399
+ }
400
+ return {
401
+ workspace: active && authoritySnapshot.scope !== "organization",
402
+ organization: active && authoritySnapshot.scope === "organization",
403
+ };
404
+ }
405
+
406
+ /** Load the same inputs the workspace model catalog route evaluates. */
407
+ export async function loadWorkspaceModelSelectionInput(
408
+ db: Database,
409
+ settings: Settings,
410
+ context: WorkspaceModelSelectionContext,
411
+ options: { observeAvailability?: boolean } = {},
412
+ ): Promise<WorkspaceModelSelectionInput> {
413
+ const { accountId, workspaceId } = context;
414
+ const restrictionsRead = (async () =>
415
+ connectionRestrictionsAndXaiReadiness(db, settings, context))();
416
+ const claudePoolRead = (async () =>
417
+ loadWorkspaceClaudeSubscriptionReadiness(db, settings, context))();
418
+ const inputRead = (async () =>
419
+ Promise.all([
420
+ restrictionsRead,
421
+ claudePoolRead,
422
+ getWorkspaceModelPolicy(db, workspaceId),
423
+ workspaceCodexSubscriptionActive(db, settings, workspaceId),
424
+ options.observeAvailability === false
425
+ ? Promise.resolve({})
426
+ : loadWorkspaceCodexModelAvailability(db, settings, workspaceId),
427
+ workspaceVercelAiGatewayConnectionActive(db, workspaceId),
428
+ listWorkspaceGatewayCustomModels(db, { accountId, workspaceId }),
429
+ workspaceOpenRouterConnectionActive(db, workspaceId),
430
+ listWorkspaceOpenRouterCustomModels(db, { accountId, workspaceId }),
431
+ organizationModelProviderConnectionActiveForWorkspace(db, {
432
+ accountId,
433
+ workspaceId,
434
+ providerKind: "vercel_gateway",
435
+ }),
436
+ organizationModelProviderConnectionActiveForWorkspace(db, {
437
+ accountId,
438
+ workspaceId,
439
+ providerKind: "openrouter",
440
+ }),
441
+ listOrganizationModelProviderCustomModelsForWorkspace(db, {
442
+ accountId,
443
+ workspaceId,
444
+ providerKind: "vercel_gateway",
445
+ }),
446
+ listOrganizationModelProviderCustomModelsForWorkspace(db, {
447
+ accountId,
448
+ workspaceId,
449
+ providerKind: "openrouter",
450
+ }),
451
+ ]))();
452
+ const claudeConnections: ClaudeConnectionCatalog = {};
453
+ const workspaceClaudeConnections: ClaudeConnectionCatalog = {};
454
+ // Transaction handles share one backend and LOCAL scope. Preserve the old
455
+ // batch ordering there rather than introduce concurrent nested savepoints.
456
+ if (typeof (db as Database & { rollback?: unknown }).rollback === "function") await inputRead;
457
+ // Catalog metadata does not depend on the other providers' policy/readiness
458
+ // batch. Only subscription activation joins its exact existing pool read.
459
+ // These helpers keep their separate root-pool RLS scopes; no shared transaction.
460
+ const catalogRead = (async () =>
461
+ Promise.all(
462
+ CLAUDE_CONNECTION_KINDS.map(async (kind) => {
463
+ if (kind === "claude_subscription" && !settings.claudeSubscriptionEnabled) return;
464
+ const [active, models, metadata, workspaceModels] = await Promise.all([
465
+ kind === "claude_subscription"
466
+ ? claudePoolRead.then((pool) => pool.organization)
467
+ : organizationModelProviderConnectionActiveForWorkspace(db, {
468
+ accountId,
469
+ workspaceId,
470
+ providerKind: kind,
471
+ }),
472
+ listOrganizationModelProviderCustomModelsForWorkspace(db, {
473
+ accountId,
474
+ workspaceId,
475
+ providerKind: kind,
476
+ }),
477
+ kind === "claude_subscription"
478
+ ? Promise.resolve(null)
479
+ : getWorkspaceProviderApiKeyConnectionMetadata(db, workspaceId, kind),
480
+ listWorkspaceProviderCustomModels(db, { accountId, workspaceId, providerKind: kind }),
481
+ ]);
482
+ claudeConnections[kind] = { active, models };
483
+ workspaceClaudeConnections[kind] = {
484
+ active:
485
+ kind === "claude_subscription" ? (await claudePoolRead).workspace : metadata !== null,
486
+ models: workspaceModels,
487
+ };
488
+ }),
489
+ ))();
490
+ // Observe both batches before admission resumes or fails. Retain the previous
491
+ // input-batch error precedence even if the independent catalog fails first.
492
+ const [inputResult, catalogResult] = await Promise.allSettled([inputRead, catalogRead]);
493
+ if (inputResult.status === "rejected") throw inputResult.reason;
494
+ if (catalogResult.status === "rejected") throw catalogResult.reason;
495
+ const [
496
+ { restrictions: connectionModelRestrictions, xaiSubscriptionActive },
497
+ _claudePool,
498
+ policy,
499
+ codexSubscriptionActive,
500
+ observations,
501
+ workspaceGatewayConnectionActive,
502
+ workspaceGatewayCustomModels,
503
+ openRouterConnectionActive,
504
+ workspaceOpenRouterCustomModels,
505
+ organizationGatewayConnectionActive,
506
+ organizationOpenRouterConnectionActive,
507
+ organizationGatewayCustomModels,
508
+ organizationOpenRouterCustomModels,
509
+ ] = inputResult.value;
510
+ return {
511
+ claudeConnections,
512
+ workspaceClaudeConnections,
513
+ connectionModelRestrictions,
514
+ settings,
515
+ policy,
516
+ codexSubscriptionActive,
517
+ observations,
518
+ xaiSubscriptionActive,
519
+ workspaceGatewayConnectionActive,
520
+ workspaceGatewayCustomModels,
521
+ workspaceOpenRouterConnectionActive: openRouterConnectionActive,
522
+ workspaceOpenRouterCustomModels,
523
+ organizationGatewayConnectionActive,
524
+ organizationOpenRouterConnectionActive,
525
+ organizationGatewayCustomModels,
526
+ organizationOpenRouterCustomModels,
527
+ };
528
+ }
529
+
530
+ /**
531
+ * Caller-scoped stable fresh admission; never re-admit already accepted work.
532
+ * Live discovery is opt-in: its token refresh can mutate connection readiness,
533
+ * so a health hint must not change the billing rail of an explicit create.
534
+ */
535
+ export async function resolveCallerWorkspaceModelSelections(
536
+ db: Database,
537
+ settings: Settings,
538
+ context: WorkspaceModelSelectionContext,
539
+ options: { observeAvailability?: boolean } = {},
540
+ ): Promise<WorkspaceModelSelection[]> {
541
+ return resolveWorkspaceModelSelection(
542
+ await loadWorkspaceModelSelectionInput(db, settings, context, {
543
+ observeAvailability: options.observeAvailability === true,
544
+ }),
545
+ );
546
+ }
547
+
548
+ /** Transient selectability for existing consumers; not fresh-create admission. */
549
+ export function selectableWorkspaceModel(
550
+ selections: readonly WorkspaceModelSelection[],
551
+ modelId: string,
552
+ ): WorkspaceModelSelection | undefined {
553
+ const selection = findSelection(selections, modelId);
554
+ return selection?.availability.selectable ? selection : undefined;
555
+ }
556
+
557
+ /** Same stable decision used by client config and direct fresh session creation. */
558
+ export function admissibleWorkspaceModel(
559
+ selections: readonly WorkspaceModelSelection[],
560
+ modelId: string,
561
+ ): WorkspaceModelSelection | undefined {
562
+ const selection = findSelection(selections, modelId);
563
+ return selection && isWorkspaceModelAdmissible(selection) ? selection : undefined;
564
+ }
565
+
566
+ /**
567
+ * Server-side default for new work that names no model: API and Slack session
568
+ * creates, new-chat drafts that follow the default, and scheduled-task
569
+ * occurrences. The result is resolved once and then frozen by the accepted
570
+ * session or occurrence like any other model choice.
571
+ */
572
+ export async function resolveDefaultSessionModel(
573
+ db: Database,
574
+ settings: Settings,
575
+ context: WorkspaceModelSelectionContext & { workspaceSettings?: unknown },
576
+ ): Promise<DefaultModelSelection> {
577
+ const [selectionInput, workspaceSettings] = await Promise.all([
578
+ loadWorkspaceModelSelectionInput(db, settings, context),
579
+ context.workspaceSettings !== undefined
580
+ ? Promise.resolve(context.workspaceSettings)
581
+ : getWorkspace(db, context.workspaceId).then((workspace) => workspace?.settings ?? {}),
582
+ ]);
583
+ return await resolveDefaultSessionModelForSelections(db, {
584
+ settings,
585
+ accountId: context.accountId,
586
+ workspaceSettings,
587
+ selections: resolveWorkspaceModelSelection(selectionInput),
588
+ });
589
+ }
590
+
591
+ /**
592
+ * The default a scheduled occurrence uses when its task names no model and the
593
+ * occurrence creates its own session: resolved under the task's immutable
594
+ * execution owner (or its creator for a workspace/service task) and the task's
595
+ * frozen SuperGrok authority, never another member's. The worker stamps the
596
+ * result onto the accepted execution, so retries and recovery replay it; the
597
+ * API's manual-trigger limit check uses the same resolution so it gates the
598
+ * model that will run.
599
+ */
600
+ export async function resolveScheduledTaskDefaultModel(
601
+ db: Database,
602
+ settings: Settings,
603
+ task: Pick<ScheduledTask, "id" | "accountId" | "workspaceId" | "ownerSubjectId" | "createdBy">,
604
+ ): Promise<DefaultModelSelection> {
605
+ return await resolveDefaultSessionModel(db, settings, {
606
+ accountId: task.accountId,
607
+ workspaceId: task.workspaceId,
608
+ subjectId: task.ownerSubjectId ?? task.createdBy.subjectId,
609
+ claudeAuthoritySnapshot: await getScheduledTaskClaudeProviderAccountAuthoritySnapshot(
610
+ db,
611
+ task.workspaceId,
612
+ task.id,
613
+ ),
614
+ xaiAuthoritySnapshot: await getScheduledTaskXaiProviderAccountAuthoritySnapshot(
615
+ db,
616
+ task.workspaceId,
617
+ task.id,
618
+ ),
619
+ });
620
+ }
621
+
622
+ /**
623
+ * The model a manual trigger's limit pre-check evaluates, matching what the
624
+ * occurrence will run: the task's explicit model, else the target or reusable
625
+ * session's model when that session exists, else the resolved scheduled
626
+ * default. The worker still resolves authoritatively at dispatch.
627
+ */
628
+ export async function resolveScheduledTaskPreflightModel(
629
+ db: Database,
630
+ settings: Settings,
631
+ task: Pick<
632
+ ScheduledTask,
633
+ | "id"
634
+ | "accountId"
635
+ | "workspaceId"
636
+ | "ownerSubjectId"
637
+ | "createdBy"
638
+ | "agentConfig"
639
+ | "runMode"
640
+ | "targetSessionId"
641
+ | "reusableSessionId"
642
+ >,
643
+ ): Promise<string> {
644
+ if (task.agentConfig.model) return task.agentConfig.model;
645
+ const targetSessionId =
646
+ task.runMode === "existing_session"
647
+ ? task.targetSessionId
648
+ : task.runMode === "reusable_session"
649
+ ? task.reusableSessionId
650
+ : null;
651
+ if (targetSessionId) {
652
+ const session = await getSession(db, task.workspaceId, targetSessionId);
653
+ if (session) return session.model;
654
+ }
655
+ return (await resolveScheduledTaskDefaultModel(db, settings, task)).model;
656
+ }