@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,873 @@
1
+ import { modelAllowedByConnections, type ConnectionModelRestrictions } from "@opengeni/db";
2
+ import {
3
+ withDirectModelProviders,
4
+ applyModelCatalogDocument,
5
+ CLAUDE_CONNECTION_KINDS,
6
+ claudeProviderId,
7
+ withClaudeConnectionCatalog,
8
+ type ClaudeConnectionCatalog,
9
+ configuredGatewayWorkspaceProductModelIds,
10
+ configuredGatewayOrganizationProductModelIds,
11
+ configuredModels,
12
+ isModelAvailableForNewSelection,
13
+ configuredModelNotes,
14
+ configuredOpenRouterWorkspaceProductModelIds,
15
+ configuredOpenRouterOrganizationProductModelIds,
16
+ configuredProviders,
17
+ validateModelCatalogSettings,
18
+ withCodexCatalogProvider,
19
+ withOrganizationGatewayCatalogProvider,
20
+ withOrganizationOpenRouterCatalogProvider,
21
+ withWorkspaceGatewayCatalogProvider,
22
+ withWorkspaceOpenRouterCatalogProvider,
23
+ withXaiSubscriptionCatalogProvider,
24
+ WORKSPACE_GATEWAY_MODEL_ID_PREFIX,
25
+ WORKSPACE_OPENROUTER_MODEL_ID_PREFIX,
26
+ WORKSPACE_OPENROUTER_PROVIDER_ID,
27
+ ORGANIZATION_OPENROUTER_PROVIDER_ID,
28
+ ORGANIZATION_GATEWAY_MODEL_ID_PREFIX,
29
+ ORGANIZATION_OPENROUTER_MODEL_ID_PREFIX,
30
+ type ConfiguredModel,
31
+ type Settings,
32
+ } from "@opengeni/config";
33
+ import {
34
+ evaluateWorkspaceModelPolicy,
35
+ type ModelAvailabilityV1,
36
+ type ModelCredentialReadinessV1,
37
+ type WorkspaceModelPolicyContract,
38
+ } from "@opengeni/contracts";
39
+ import {
40
+ listConnectionsMetadata,
41
+ getDeploymentModelCatalog,
42
+ listWorkspaceProviderCustomModels,
43
+ getWorkspaceProviderCustomModelForExecution,
44
+ lockActiveWorkspaceProviderCustomModelForAdmission,
45
+ getWorkspaceGatewayCustomModelForExecution,
46
+ getWorkspaceOpenRouterCustomModelForExecution,
47
+ getOrganizationModelProviderCustomModelForExecution,
48
+ listWorkspaceGatewayCustomModels,
49
+ listWorkspaceOpenRouterCustomModels,
50
+ listOrganizationModelProviderCustomModelsForWorkspace,
51
+ lockActiveOrganizationModelProviderCustomModelForAdmission,
52
+ type OrganizationClaudeModelAdmissionAuthority,
53
+ type Database,
54
+ } from "@opengeni/db";
55
+
56
+ export type ResolvedCatalogSettings = {
57
+ settings: Settings;
58
+ source: "code" | "database";
59
+ version: number | null;
60
+ modelNotes: Record<string, string>;
61
+ };
62
+
63
+ /**
64
+ * Curated workspace Gateway products and workspace-owned custom slugs share
65
+ * one public prefix. Only the latter have a mutable catalog row whose active
66
+ * generation must be rechecked at a fresh acceptance commit boundary.
67
+ */
68
+ export function isWorkspaceGatewayCustomModelId(settings: Settings, modelId: string): boolean {
69
+ return (
70
+ modelId.startsWith(WORKSPACE_GATEWAY_MODEL_ID_PREFIX) &&
71
+ !configuredGatewayWorkspaceProductModelIds(settings).includes(modelId)
72
+ );
73
+ }
74
+
75
+ export function isWorkspaceOpenRouterCustomModelId(settings: Settings, modelId: string): boolean {
76
+ return (
77
+ modelId.startsWith(WORKSPACE_OPENROUTER_MODEL_ID_PREFIX) &&
78
+ !configuredOpenRouterWorkspaceProductModelIds(settings).includes(modelId)
79
+ );
80
+ }
81
+
82
+ export type WorkspaceCustomModelReference = {
83
+ scope: "workspace" | "organization";
84
+ providerKind: "vercel_gateway" | "openrouter" | "anthropic" | "claude_subscription";
85
+ upstreamModelId: string;
86
+ };
87
+
88
+ export function workspaceCustomModelReference(
89
+ settings: Settings,
90
+ modelId: string,
91
+ ): WorkspaceCustomModelReference | null {
92
+ for (const scope of ["workspace", "organization"] as const)
93
+ for (const kind of CLAUDE_CONNECTION_KINDS) {
94
+ const prefix = claudeProviderId(kind, scope) + "/";
95
+ if (modelId.startsWith(prefix))
96
+ return {
97
+ scope,
98
+ providerKind: kind,
99
+ upstreamModelId: modelId.slice(prefix.length),
100
+ };
101
+ }
102
+ if (isWorkspaceGatewayCustomModelId(settings, modelId)) {
103
+ return {
104
+ scope: "workspace",
105
+ providerKind: "vercel_gateway",
106
+ upstreamModelId: modelId.slice(WORKSPACE_GATEWAY_MODEL_ID_PREFIX.length),
107
+ };
108
+ }
109
+ if (isWorkspaceOpenRouterCustomModelId(settings, modelId)) {
110
+ return {
111
+ scope: "workspace",
112
+ providerKind: "openrouter",
113
+ upstreamModelId: modelId.slice(WORKSPACE_OPENROUTER_MODEL_ID_PREFIX.length),
114
+ };
115
+ }
116
+ if (
117
+ modelId.startsWith(ORGANIZATION_GATEWAY_MODEL_ID_PREFIX) &&
118
+ !configuredGatewayOrganizationProductModelIds(settings).includes(modelId)
119
+ ) {
120
+ return {
121
+ scope: "organization",
122
+ providerKind: "vercel_gateway",
123
+ upstreamModelId: modelId.slice(ORGANIZATION_GATEWAY_MODEL_ID_PREFIX.length),
124
+ };
125
+ }
126
+ if (
127
+ modelId.startsWith(ORGANIZATION_OPENROUTER_MODEL_ID_PREFIX) &&
128
+ !configuredOpenRouterOrganizationProductModelIds(settings).includes(modelId)
129
+ ) {
130
+ return {
131
+ scope: "organization",
132
+ providerKind: "openrouter",
133
+ upstreamModelId: modelId.slice(ORGANIZATION_OPENROUTER_MODEL_ID_PREFIX.length),
134
+ };
135
+ }
136
+ return null;
137
+ }
138
+
139
+ export function isWorkspaceCustomModelId(settings: Settings, modelId: string): boolean {
140
+ return workspaceCustomModelReference(settings, modelId) !== null;
141
+ }
142
+
143
+ export async function lockActiveCustomModelForAdmission(
144
+ db: Database,
145
+ input: {
146
+ accountId: string;
147
+ workspaceId: string;
148
+ reference: WorkspaceCustomModelReference;
149
+ claudeAuthority?: OrganizationClaudeModelAdmissionAuthority;
150
+ },
151
+ ): Promise<boolean> {
152
+ if (input.reference.scope === "organization") {
153
+ return Boolean(
154
+ await lockActiveOrganizationModelProviderCustomModelForAdmission(db, {
155
+ accountId: input.accountId,
156
+ workspaceId: input.workspaceId,
157
+ providerKind: input.reference.providerKind,
158
+ upstreamModelId: input.reference.upstreamModelId,
159
+ ...(input.claudeAuthority ? { claudeAuthority: input.claudeAuthority } : {}),
160
+ }),
161
+ );
162
+ }
163
+ return Boolean(
164
+ await lockActiveWorkspaceProviderCustomModelForAdmission(db, {
165
+ accountId: input.accountId,
166
+ workspaceId: input.workspaceId,
167
+ providerKind: input.reference.providerKind,
168
+ upstreamModelId: input.reference.upstreamModelId,
169
+ }),
170
+ );
171
+ }
172
+
173
+ /**
174
+ * Resolve the deployment catalog without making synchronous env settings read
175
+ * Postgres. Database mode fails closed when the singleton is absent or invalid;
176
+ * code mode preserves the already-validated env catalog.
177
+ */
178
+ export async function resolveCatalogSettings(
179
+ db: Database,
180
+ envSettings: Settings,
181
+ ): Promise<ResolvedCatalogSettings> {
182
+ if (envSettings.modelCatalogSource === "code") {
183
+ validateModelCatalogSettings(envSettings);
184
+ return {
185
+ settings: envSettings,
186
+ source: "code",
187
+ version: null,
188
+ modelNotes: configuredModelNotes(envSettings),
189
+ };
190
+ }
191
+
192
+ const row = await getDeploymentModelCatalog(db);
193
+ if (!row) {
194
+ throw new Error("database model catalog source is configured but the singleton row is missing");
195
+ }
196
+ const settings = applyModelCatalogDocument(envSettings, row.document);
197
+ validateModelCatalogSettings(settings);
198
+ return {
199
+ settings,
200
+ source: "database",
201
+ version: row.version,
202
+ modelNotes: configuredModelNotes(settings),
203
+ };
204
+ }
205
+
206
+ /**
207
+ * Resolve the deployment catalog and add only the custom Gateway slugs owned by
208
+ * one workspace. Use this at model-bearing workspace boundaries; public config
209
+ * and deployment-operator surfaces must continue to use `resolveCatalogSettings`.
210
+ */
211
+ export async function resolveWorkspaceCatalogSettings(
212
+ db: Database,
213
+ envSettings: Settings,
214
+ input: {
215
+ accountId: string;
216
+ workspaceId: string;
217
+ retainedProductModelId?: string | null;
218
+ retainedProductModelIds?: readonly (string | null | undefined)[];
219
+ },
220
+ ): Promise<ResolvedCatalogSettings> {
221
+ // A few pure catalog tests inject the historical minimal DB port and mock the
222
+ // workspace model helpers directly. Real/injected runtime databases always
223
+ // expose transactions; keep that narrow test port compatible.
224
+ const supportsOrganizationProviderReads =
225
+ typeof (db as Database & { transaction?: unknown }).transaction === "function";
226
+ const retainedProductModelIds = [
227
+ ...(input.retainedProductModelIds ?? []),
228
+ input.retainedProductModelId,
229
+ ];
230
+ const retainedGatewayUpstreamModelIds = retainedProductModelIds.flatMap((productModelId) =>
231
+ productModelId?.startsWith(WORKSPACE_GATEWAY_MODEL_ID_PREFIX)
232
+ ? [productModelId.slice(WORKSPACE_GATEWAY_MODEL_ID_PREFIX.length)]
233
+ : [],
234
+ );
235
+ const retainedOpenRouterUpstreamModelIds = retainedProductModelIds.flatMap((productModelId) =>
236
+ productModelId?.startsWith(WORKSPACE_OPENROUTER_MODEL_ID_PREFIX)
237
+ ? [productModelId.slice(WORKSPACE_OPENROUTER_MODEL_ID_PREFIX.length)]
238
+ : [],
239
+ );
240
+ const retainedOrganizationGatewayUpstreamModelIds = retainedProductModelIds.flatMap(
241
+ (productModelId) =>
242
+ productModelId?.startsWith(ORGANIZATION_GATEWAY_MODEL_ID_PREFIX)
243
+ ? [productModelId.slice(ORGANIZATION_GATEWAY_MODEL_ID_PREFIX.length)]
244
+ : [],
245
+ );
246
+ const retainedOrganizationOpenRouterUpstreamModelIds = retainedProductModelIds.flatMap(
247
+ (productModelId) =>
248
+ productModelId?.startsWith(ORGANIZATION_OPENROUTER_MODEL_ID_PREFIX)
249
+ ? [productModelId.slice(ORGANIZATION_OPENROUTER_MODEL_ID_PREFIX.length)]
250
+ : [],
251
+ );
252
+ const [
253
+ resolved,
254
+ activeGatewayCustomModels,
255
+ activeOpenRouterCustomModels,
256
+ retainedGatewayCustomModels,
257
+ retainedOpenRouterCustomModels,
258
+ organizationGatewayCustomModels,
259
+ organizationOpenRouterCustomModels,
260
+ retainedOrganizationGatewayCustomModels,
261
+ retainedOrganizationOpenRouterCustomModels,
262
+ directConnections,
263
+ ] = await Promise.all([
264
+ resolveCatalogSettings(db, envSettings),
265
+ listWorkspaceGatewayCustomModels(db, {
266
+ accountId: input.accountId,
267
+ workspaceId: input.workspaceId,
268
+ }),
269
+ listWorkspaceOpenRouterCustomModels(db, {
270
+ accountId: input.accountId,
271
+ workspaceId: input.workspaceId,
272
+ }),
273
+ Promise.all(
274
+ [...new Set(retainedGatewayUpstreamModelIds)].map(
275
+ async (upstreamModelId) =>
276
+ await getWorkspaceGatewayCustomModelForExecution(db, {
277
+ accountId: input.accountId,
278
+ workspaceId: input.workspaceId,
279
+ upstreamModelId,
280
+ }),
281
+ ),
282
+ ),
283
+ Promise.all(
284
+ [...new Set(retainedOpenRouterUpstreamModelIds)].map(
285
+ async (upstreamModelId) =>
286
+ await getWorkspaceOpenRouterCustomModelForExecution(db, {
287
+ accountId: input.accountId,
288
+ workspaceId: input.workspaceId,
289
+ upstreamModelId,
290
+ }),
291
+ ),
292
+ ),
293
+ supportsOrganizationProviderReads
294
+ ? listOrganizationModelProviderCustomModelsForWorkspace(db, {
295
+ accountId: input.accountId,
296
+ workspaceId: input.workspaceId,
297
+ providerKind: "vercel_gateway",
298
+ })
299
+ : Promise.resolve([]),
300
+ supportsOrganizationProviderReads
301
+ ? listOrganizationModelProviderCustomModelsForWorkspace(db, {
302
+ accountId: input.accountId,
303
+ workspaceId: input.workspaceId,
304
+ providerKind: "openrouter",
305
+ })
306
+ : Promise.resolve([]),
307
+ supportsOrganizationProviderReads
308
+ ? Promise.all(
309
+ [...new Set(retainedOrganizationGatewayUpstreamModelIds)].map(
310
+ async (upstreamModelId) =>
311
+ await getOrganizationModelProviderCustomModelForExecution(db, {
312
+ accountId: input.accountId,
313
+ workspaceId: input.workspaceId,
314
+ providerKind: "vercel_gateway",
315
+ upstreamModelId,
316
+ }),
317
+ ),
318
+ )
319
+ : Promise.resolve([]),
320
+ supportsOrganizationProviderReads
321
+ ? Promise.all(
322
+ [...new Set(retainedOrganizationOpenRouterUpstreamModelIds)].map(
323
+ async (upstreamModelId) =>
324
+ await getOrganizationModelProviderCustomModelForExecution(db, {
325
+ accountId: input.accountId,
326
+ workspaceId: input.workspaceId,
327
+ providerKind: "openrouter",
328
+ upstreamModelId,
329
+ }),
330
+ ),
331
+ )
332
+ : Promise.resolve([]),
333
+ supportsOrganizationProviderReads
334
+ ? listConnectionsMetadata(db, input.workspaceId, null)
335
+ : Promise.resolve([]),
336
+ ]);
337
+ const includeRetainedModels = <T extends { upstreamModelId: string }>(
338
+ activeModels: readonly T[],
339
+ retainedModels: readonly (T | null)[],
340
+ ): T[] => {
341
+ const customModels = [...activeModels];
342
+ const includedUpstreamModelIds = new Set(activeModels.map((model) => model.upstreamModelId));
343
+ for (const retainedCustomModel of retainedModels) {
344
+ if (
345
+ retainedCustomModel &&
346
+ !includedUpstreamModelIds.has(retainedCustomModel.upstreamModelId)
347
+ ) {
348
+ customModels.push(retainedCustomModel);
349
+ includedUpstreamModelIds.add(retainedCustomModel.upstreamModelId);
350
+ }
351
+ }
352
+ return customModels;
353
+ };
354
+ const gatewayCustomModels = includeRetainedModels(
355
+ activeGatewayCustomModels,
356
+ retainedGatewayCustomModels,
357
+ );
358
+ const openRouterCustomModels = includeRetainedModels(
359
+ activeOpenRouterCustomModels,
360
+ retainedOpenRouterCustomModels,
361
+ );
362
+ const organizationGatewayModels = includeRetainedModels(
363
+ organizationGatewayCustomModels,
364
+ retainedOrganizationGatewayCustomModels,
365
+ );
366
+ const organizationOpenRouterModels = includeRetainedModels(
367
+ organizationOpenRouterCustomModels,
368
+ retainedOrganizationOpenRouterCustomModels,
369
+ );
370
+ const claudeConnections: ClaudeConnectionCatalog = {};
371
+ if (supportsOrganizationProviderReads)
372
+ for (const kind of CLAUDE_CONNECTION_KINDS) {
373
+ const models = await listOrganizationModelProviderCustomModelsForWorkspace(db, {
374
+ accountId: input.accountId,
375
+ workspaceId: input.workspaceId,
376
+ providerKind: kind,
377
+ });
378
+ const prefix = claudeProviderId(kind) + "/";
379
+ for (const productId of retainedProductModelIds)
380
+ if (productId?.startsWith(prefix)) {
381
+ const retained = await getOrganizationModelProviderCustomModelForExecution(db, {
382
+ accountId: input.accountId,
383
+ workspaceId: input.workspaceId,
384
+ providerKind: kind,
385
+ upstreamModelId: productId.slice(prefix.length),
386
+ });
387
+ if (retained && !models.some((model) => model.id === retained.id)) models.push(retained);
388
+ }
389
+ claudeConnections[kind] = { models };
390
+ }
391
+ const workspaceClaudeConnections: ClaudeConnectionCatalog = {};
392
+ if (supportsOrganizationProviderReads)
393
+ for (const kind of CLAUDE_CONNECTION_KINDS) {
394
+ if (kind === "claude_subscription" && !resolved.settings.claudeSubscriptionEnabled) continue;
395
+ const models = await listWorkspaceProviderCustomModels(db, {
396
+ accountId: input.accountId,
397
+ workspaceId: input.workspaceId,
398
+ providerKind: kind,
399
+ });
400
+ const prefix = claudeProviderId(kind, "workspace") + "/";
401
+ for (const productId of retainedProductModelIds) {
402
+ if (!productId?.startsWith(prefix)) continue;
403
+ const retained = await getWorkspaceProviderCustomModelForExecution(db, {
404
+ accountId: input.accountId,
405
+ workspaceId: input.workspaceId,
406
+ providerKind: kind,
407
+ upstreamModelId: productId.slice(prefix.length),
408
+ });
409
+ if (retained && !models.some((model) => model.id === retained.id)) models.push(retained);
410
+ }
411
+ workspaceClaudeConnections[kind] = { models };
412
+ }
413
+ return {
414
+ ...resolved,
415
+ settings: withDirectModelProviders(
416
+ withClaudeConnectionCatalog(
417
+ withClaudeConnectionCatalog(
418
+ withOrganizationOpenRouterCatalogProvider(
419
+ withOrganizationGatewayCatalogProvider(
420
+ withWorkspaceOpenRouterCatalogProvider(
421
+ withWorkspaceGatewayCatalogProvider(resolved.settings, gatewayCustomModels),
422
+ openRouterCustomModels,
423
+ ),
424
+ organizationGatewayModels,
425
+ ),
426
+ organizationOpenRouterModels,
427
+ ),
428
+ claudeConnections,
429
+ ),
430
+ workspaceClaudeConnections,
431
+ "workspace",
432
+ ),
433
+ directConnections,
434
+ ),
435
+ };
436
+ }
437
+
438
+ export type ModelAvailabilityObservation = {
439
+ status: "available" | "degraded" | "unavailable";
440
+ reason: "not_entitled" | "provider_unhealthy" | null;
441
+ checkedAt: string;
442
+ };
443
+
444
+ export type ModelCredentialReadinessObservation =
445
+ | { status: "ready"; checkedAt: string }
446
+ | {
447
+ status: "not_ready";
448
+ reason: "prerequisites_missing" | "needs_reauth";
449
+ checkedAt: string;
450
+ }
451
+ | { status: "error"; reason: "resolver_error"; checkedAt: string };
452
+
453
+ export const MODEL_CREDENTIAL_READINESS_OBSERVATION_MAX_AGE_MS = 5 * 60_000;
454
+
455
+ export type WorkspaceModelSelectionInput = {
456
+ connectionModelRestrictions?: ConnectionModelRestrictions;
457
+ settings: Settings;
458
+ policy: WorkspaceModelPolicyContract | null;
459
+ codexSubscriptionActive: boolean;
460
+ xaiSubscriptionActive?: boolean;
461
+ workspaceGatewayConnectionActive?: boolean;
462
+ workspaceOpenRouterConnectionActive?: boolean;
463
+ claudeConnections?: ClaudeConnectionCatalog;
464
+ workspaceClaudeConnections?: ClaudeConnectionCatalog;
465
+ organizationGatewayConnectionActive?: boolean;
466
+ organizationOpenRouterConnectionActive?: boolean;
467
+ workspaceGatewayCustomModels?: readonly {
468
+ upstreamModelId: string;
469
+ label?: string | null;
470
+ }[];
471
+ workspaceOpenRouterCustomModels?: readonly {
472
+ upstreamModelId: string;
473
+ label?: string | null;
474
+ }[];
475
+ organizationGatewayCustomModels?: readonly {
476
+ upstreamModelId: string;
477
+ label?: string | null;
478
+ }[];
479
+ organizationOpenRouterCustomModels?: readonly {
480
+ upstreamModelId: string;
481
+ label?: string | null;
482
+ }[];
483
+ credentialReadinessObservations?:
484
+ | Readonly<Record<string, ModelCredentialReadinessObservation>>
485
+ | undefined;
486
+ observations?: Readonly<Record<string, ModelAvailabilityObservation>> | undefined;
487
+ now?: Date | undefined;
488
+ credentialReadinessMaxAgeMs?: number | undefined;
489
+ };
490
+
491
+ export type WorkspaceModelSelection = {
492
+ model: ConfiguredModel;
493
+ credentialReadiness: ModelCredentialReadinessV1;
494
+ policyAllowed: boolean;
495
+ availability: ModelAvailabilityV1;
496
+ };
497
+
498
+ function modelDefinitionRunnable(model: ConfiguredModel): boolean {
499
+ return (
500
+ model.capabilities.inputModalities.includes("text") &&
501
+ model.capabilities.outputModalities.includes("text") &&
502
+ model.capabilities.transports.sse.runnable
503
+ );
504
+ }
505
+
506
+ /**
507
+ * Stable fresh-selection admission, independent of transient observations.
508
+ * Missing catalog membership is handled by the caller's lookup. Resolver-based
509
+ * deployment credentials (Azure AD/managed identity) are resolved at execution;
510
+ * absent, stale or failed observations cannot make their definition uncreatable.
511
+ */
512
+ export function isWorkspaceModelAdmissible(selection: WorkspaceModelSelection): boolean {
513
+ if (!modelDefinitionRunnable(selection.model) || !selection.policyAllowed) return false;
514
+ const source = selection.model.credentialSource;
515
+ if (source.kind === "deployment") {
516
+ return source.mechanism !== "api_key" || selection.credentialReadiness.status === "ready";
517
+ }
518
+ // Connected subscriptions and workspace/organization connections have a
519
+ // durable active/reauth prerequisite, unlike deployment credential resolvers.
520
+ return selection.credentialReadiness.status === "ready";
521
+ }
522
+
523
+ function observedCredentialReadiness(input: {
524
+ observation: ModelCredentialReadinessObservation | undefined;
525
+ basis: "connection" | "resolver";
526
+ nowMs: number;
527
+ maxAgeMs: number;
528
+ }): ModelCredentialReadinessV1 {
529
+ if (!input.observation) {
530
+ return {
531
+ status: "not_ready",
532
+ reason: "prerequisites_missing",
533
+ basis: input.basis,
534
+ checkedAt: null,
535
+ };
536
+ }
537
+ const checkedAtMs = Date.parse(input.observation.checkedAt);
538
+ if (!Number.isFinite(checkedAtMs)) {
539
+ return {
540
+ status: "error",
541
+ reason: "resolver_error",
542
+ basis: input.basis,
543
+ checkedAt: null,
544
+ };
545
+ }
546
+ const checkedAt = new Date(checkedAtMs).toISOString();
547
+ if (Math.abs(input.nowMs - checkedAtMs) > input.maxAgeMs) {
548
+ return {
549
+ status: "not_ready",
550
+ reason: "observation_stale",
551
+ basis: input.basis,
552
+ checkedAt,
553
+ };
554
+ }
555
+ if (input.observation.status === "ready") {
556
+ return { status: "ready", reason: null, basis: input.basis, checkedAt };
557
+ }
558
+ if (input.observation.status === "not_ready") {
559
+ return {
560
+ status: "not_ready",
561
+ reason:
562
+ input.observation.reason === "needs_reauth" ? "needs_reauth" : "prerequisites_missing",
563
+ basis: input.basis,
564
+ checkedAt,
565
+ };
566
+ }
567
+ return {
568
+ status: "error",
569
+ reason: "resolver_error",
570
+ basis: input.basis,
571
+ checkedAt,
572
+ };
573
+ }
574
+
575
+ function credentialReadinessFor(input: {
576
+ model: ConfiguredModel;
577
+ provider: ReturnType<typeof configuredProviders>[number] | undefined;
578
+ codexSubscriptionActive: boolean;
579
+ xaiSubscriptionActive: boolean;
580
+ workspaceGatewayConnectionActive: boolean;
581
+ workspaceOpenRouterConnectionActive: boolean;
582
+ claudeConnections?: ClaudeConnectionCatalog | undefined;
583
+ workspaceClaudeConnections?: ClaudeConnectionCatalog | undefined;
584
+ organizationGatewayConnectionActive: boolean;
585
+ organizationOpenRouterConnectionActive: boolean;
586
+ observation: ModelCredentialReadinessObservation | undefined;
587
+ nowMs: number;
588
+ maxAgeMs: number;
589
+ }): ModelCredentialReadinessV1 {
590
+ const source = input.model.credentialSource;
591
+ if (source.kind === "connected_subscription") {
592
+ const active =
593
+ source.provider === "xai"
594
+ ? input.xaiSubscriptionActive
595
+ : source.provider === "claude"
596
+ ? (input.model.providerId === claudeProviderId("claude_subscription", "workspace")
597
+ ? input.workspaceClaudeConnections
598
+ : input.claudeConnections
599
+ )?.claude_subscription?.active === true
600
+ : input.codexSubscriptionActive;
601
+ return active
602
+ ? { status: "ready", reason: null, basis: "connection", checkedAt: null }
603
+ : {
604
+ status: "not_ready",
605
+ reason: "needs_reauth",
606
+ basis: "connection",
607
+ checkedAt: null,
608
+ };
609
+ }
610
+ if (source.kind === "workspace_connection") {
611
+ const claudeKind = CLAUDE_CONNECTION_KINDS.find(
612
+ (kind) => claudeProviderId(kind, "workspace") === input.model.providerId,
613
+ );
614
+ const connectionActive =
615
+ input.provider?.kind === "direct-openai-workspace" ||
616
+ input.provider?.kind === "direct-azure-workspace"
617
+ ? true
618
+ : claudeKind
619
+ ? input.workspaceClaudeConnections?.[claudeKind]?.active === true
620
+ : input.model.providerId === WORKSPACE_OPENROUTER_PROVIDER_ID
621
+ ? input.workspaceOpenRouterConnectionActive
622
+ : input.workspaceGatewayConnectionActive;
623
+ return connectionActive
624
+ ? { status: "ready", reason: null, basis: "connection", checkedAt: null }
625
+ : {
626
+ status: "not_ready",
627
+ reason: "needs_reauth",
628
+ basis: "connection",
629
+ checkedAt: null,
630
+ };
631
+ }
632
+ if (source.kind === "organization_connection") {
633
+ const claudeKind = CLAUDE_CONNECTION_KINDS.find(
634
+ (kind) => claudeProviderId(kind) === input.model.providerId,
635
+ );
636
+ const connectionActive = claudeKind
637
+ ? input.claudeConnections?.[claudeKind]?.active === true
638
+ : input.model.providerId === ORGANIZATION_OPENROUTER_PROVIDER_ID
639
+ ? input.organizationOpenRouterConnectionActive
640
+ : input.organizationGatewayConnectionActive;
641
+ return connectionActive
642
+ ? { status: "ready", reason: null, basis: "connection", checkedAt: null }
643
+ : {
644
+ status: "not_ready",
645
+ reason: "needs_reauth",
646
+ basis: "connection",
647
+ checkedAt: null,
648
+ };
649
+ }
650
+ if (source.kind === "deployment" && source.mechanism === "none") {
651
+ return { status: "ready", reason: null, basis: "configuration", checkedAt: null };
652
+ }
653
+ if (source.kind === "deployment" && source.mechanism === "api_key") {
654
+ return input.provider?.apiKey
655
+ ? { status: "ready", reason: null, basis: "configuration", checkedAt: null }
656
+ : {
657
+ status: "not_ready",
658
+ reason: "missing_credential",
659
+ basis: "configuration",
660
+ checkedAt: null,
661
+ };
662
+ }
663
+ return observedCredentialReadiness({
664
+ observation: input.observation,
665
+ basis: "resolver",
666
+ nowMs: input.nowMs,
667
+ maxAgeMs: input.maxAgeMs,
668
+ });
669
+ }
670
+
671
+ function isXaiGrokModel(model: ConfiguredModel): boolean {
672
+ return model.providerId === "xai" && model.id.startsWith("xai/grok-");
673
+ }
674
+
675
+ function observationTimestamp(observation: ModelAvailabilityObservation | undefined): {
676
+ checkedAt: string | null;
677
+ checkedAtMs: number | null;
678
+ } {
679
+ if (!observation || typeof observation.checkedAt !== "string") {
680
+ return { checkedAt: null, checkedAtMs: null };
681
+ }
682
+ const checkedAtMs = Date.parse(observation.checkedAt);
683
+ if (!Number.isFinite(checkedAtMs)) {
684
+ return { checkedAt: null, checkedAtMs: null };
685
+ }
686
+ return { checkedAt: new Date(checkedAtMs).toISOString(), checkedAtMs };
687
+ }
688
+
689
+ function xaiGrokAvailabilityFor(input: {
690
+ observation: ModelAvailabilityObservation | undefined;
691
+ nowMs: number;
692
+ maxAgeMs: number;
693
+ }): ModelAvailabilityV1 {
694
+ const { checkedAt, checkedAtMs } = observationTimestamp(input.observation);
695
+ const freshSuccessfulObservation =
696
+ input.observation?.status === "available" &&
697
+ input.observation.reason === null &&
698
+ checkedAtMs !== null &&
699
+ checkedAtMs <= input.nowMs &&
700
+ input.nowMs - checkedAtMs <= input.maxAgeMs;
701
+
702
+ if (freshSuccessfulObservation) {
703
+ return {
704
+ status: "available",
705
+ selectable: true,
706
+ reason: null,
707
+ checkedAt,
708
+ };
709
+ }
710
+
711
+ return {
712
+ status: "unavailable",
713
+ selectable: false,
714
+ reason:
715
+ input.observation?.status === "unavailable"
716
+ ? (input.observation.reason ?? "provider_unhealthy")
717
+ : "provider_unhealthy",
718
+ checkedAt,
719
+ };
720
+ }
721
+
722
+ function availabilityFor(input: {
723
+ model: ConfiguredModel;
724
+ credentialReadiness: ModelCredentialReadinessV1;
725
+ policyAllowed: boolean;
726
+ observation?: ModelAvailabilityObservation | undefined;
727
+ nowMs: number;
728
+ maxAgeMs: number;
729
+ }): ModelAvailabilityV1 {
730
+ if (!modelDefinitionRunnable(input.model)) {
731
+ return {
732
+ status: "unavailable",
733
+ selectable: false,
734
+ reason: "unsupported",
735
+ checkedAt: null,
736
+ };
737
+ }
738
+ if (input.credentialReadiness.status !== "ready") {
739
+ return {
740
+ status: "unavailable",
741
+ selectable: false,
742
+ reason:
743
+ input.credentialReadiness.reason === "missing_credential"
744
+ ? "missing_credential"
745
+ : input.credentialReadiness.reason === "needs_reauth"
746
+ ? "needs_reauth"
747
+ : "credential_not_ready",
748
+ checkedAt: input.credentialReadiness.checkedAt,
749
+ };
750
+ }
751
+ if (!input.policyAllowed) {
752
+ return {
753
+ status: "unavailable",
754
+ selectable: false,
755
+ reason: "policy_blocked",
756
+ checkedAt: null,
757
+ };
758
+ }
759
+ if (isXaiGrokModel(input.model)) {
760
+ return xaiGrokAvailabilityFor({
761
+ observation: input.observation,
762
+ nowMs: input.nowMs,
763
+ maxAgeMs: input.maxAgeMs,
764
+ });
765
+ }
766
+ if (!input.observation) {
767
+ return { status: "unknown", selectable: true, reason: null, checkedAt: null };
768
+ }
769
+ if (input.observation.status === "unavailable") {
770
+ return {
771
+ status: "unavailable",
772
+ selectable: false,
773
+ reason: input.observation.reason ?? "provider_unhealthy",
774
+ checkedAt: input.observation.checkedAt,
775
+ };
776
+ }
777
+ return {
778
+ status: input.observation.status,
779
+ selectable: true,
780
+ reason: null,
781
+ checkedAt: input.observation.checkedAt,
782
+ };
783
+ }
784
+
785
+ /**
786
+ * One shared picker/tool decision. Catalog membership, credential readiness,
787
+ * workspace policy, and optional provider-health observations are evaluated in
788
+ * configured catalog order so every consumer exposes the same selectable set.
789
+ */
790
+ export function resolveWorkspaceModelSelection(
791
+ input: WorkspaceModelSelectionInput,
792
+ ): WorkspaceModelSelection[] {
793
+ const codexSettings = input.settings.codexSubscriptionEnabled
794
+ ? withCodexCatalogProvider(input.settings)
795
+ : input.settings;
796
+ const xaiSettings = input.settings.supergrokSubscriptionEnabled
797
+ ? withXaiSubscriptionCatalogProvider(codexSettings)
798
+ : codexSettings;
799
+ const catalogSettings = withClaudeConnectionCatalog(
800
+ withClaudeConnectionCatalog(
801
+ withOrganizationOpenRouterCatalogProvider(
802
+ withOrganizationGatewayCatalogProvider(
803
+ withWorkspaceOpenRouterCatalogProvider(
804
+ withWorkspaceGatewayCatalogProvider(
805
+ xaiSettings,
806
+ input.workspaceGatewayCustomModels ?? [],
807
+ ),
808
+ input.workspaceOpenRouterCustomModels ?? [],
809
+ ),
810
+ input.organizationGatewayCustomModels ?? [],
811
+ ),
812
+ input.organizationOpenRouterCustomModels ?? [],
813
+ ),
814
+ input.claudeConnections ?? {},
815
+ ),
816
+ input.workspaceClaudeConnections ?? {},
817
+ "workspace",
818
+ );
819
+ const providers = new Map(
820
+ configuredProviders(catalogSettings).map((provider) => [provider.id, provider]),
821
+ );
822
+ const requestedNowMs = input.now?.getTime();
823
+ const nowMs =
824
+ typeof requestedNowMs === "number" && Number.isFinite(requestedNowMs)
825
+ ? requestedNowMs
826
+ : Date.now();
827
+ const maxAgeMs =
828
+ typeof input.credentialReadinessMaxAgeMs === "number" &&
829
+ Number.isFinite(input.credentialReadinessMaxAgeMs) &&
830
+ input.credentialReadinessMaxAgeMs >= 0
831
+ ? input.credentialReadinessMaxAgeMs
832
+ : MODEL_CREDENTIAL_READINESS_OBSERVATION_MAX_AGE_MS;
833
+
834
+ return configuredModels(catalogSettings)
835
+ .filter((model) => isModelAvailableForNewSelection(catalogSettings, model.id))
836
+ .map((model) => {
837
+ const provider = providers.get(model.providerId);
838
+ const policyAllowed =
839
+ evaluateWorkspaceModelPolicy(input.policy, {
840
+ providerId: model.providerId,
841
+ modelId: model.id,
842
+ }).allowed && modelAllowedByConnections(input.connectionModelRestrictions ?? {}, model.id);
843
+ const credentialReadiness = credentialReadinessFor({
844
+ model,
845
+ provider,
846
+ codexSubscriptionActive: input.codexSubscriptionActive,
847
+ xaiSubscriptionActive: input.xaiSubscriptionActive === true,
848
+ workspaceGatewayConnectionActive: input.workspaceGatewayConnectionActive === true,
849
+ workspaceOpenRouterConnectionActive: input.workspaceOpenRouterConnectionActive === true,
850
+ claudeConnections: input.claudeConnections,
851
+ workspaceClaudeConnections: input.workspaceClaudeConnections,
852
+ organizationGatewayConnectionActive: input.organizationGatewayConnectionActive === true,
853
+ organizationOpenRouterConnectionActive:
854
+ input.organizationOpenRouterConnectionActive === true,
855
+ observation: input.credentialReadinessObservations?.[model.definitionVersion],
856
+ nowMs,
857
+ maxAgeMs,
858
+ });
859
+ return {
860
+ model,
861
+ credentialReadiness,
862
+ policyAllowed,
863
+ availability: availabilityFor({
864
+ model,
865
+ credentialReadiness,
866
+ policyAllowed,
867
+ observation: input.observations?.[model.definitionVersion],
868
+ nowMs,
869
+ maxAgeMs,
870
+ }),
871
+ };
872
+ });
873
+ }