@opengeni/core 3.0.1 → 3.1.2

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