@opengeni/core 2.6.3-canary.0 → 2.6.4-canary.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.
package/src/index.ts CHANGED
@@ -49,6 +49,7 @@ export {
49
49
  type ManagedAuthActorMutationLeaseStamp,
50
50
  } from "./managed-session";
51
51
  export * from "./transcription";
52
+ export * from "./model-catalog";
52
53
 
53
54
  // Sandbox fleet/routing service — the closure of `domain/sessions.ts`
54
55
  // (`swapActiveSandbox` + `FleetContext`). apps/api re-imports these for its MCP
@@ -0,0 +1,565 @@
1
+ import {
2
+ applyModelCatalogDocument,
3
+ configuredGatewayWorkspaceProductModelIds,
4
+ configuredModels,
5
+ configuredModelNotes,
6
+ configuredOpenRouterWorkspaceProductModelIds,
7
+ configuredProviders,
8
+ validateModelCatalogSettings,
9
+ withCodexCatalogProvider,
10
+ withWorkspaceGatewayCatalogProvider,
11
+ withWorkspaceOpenRouterCatalogProvider,
12
+ withXaiSubscriptionCatalogProvider,
13
+ WORKSPACE_GATEWAY_MODEL_ID_PREFIX,
14
+ WORKSPACE_OPENROUTER_MODEL_ID_PREFIX,
15
+ WORKSPACE_OPENROUTER_PROVIDER_ID,
16
+ type ConfiguredModel,
17
+ type Settings,
18
+ } from "@opengeni/config";
19
+ import {
20
+ evaluateWorkspaceModelPolicy,
21
+ type ModelAvailabilityV1,
22
+ type ModelCredentialReadinessV1,
23
+ type WorkspaceModelPolicyContract,
24
+ } from "@opengeni/contracts";
25
+ import {
26
+ getDeploymentModelCatalog,
27
+ getWorkspaceGatewayCustomModelForExecution,
28
+ getWorkspaceOpenRouterCustomModelForExecution,
29
+ listWorkspaceGatewayCustomModels,
30
+ listWorkspaceOpenRouterCustomModels,
31
+ type Database,
32
+ } from "@opengeni/db";
33
+
34
+ export type ResolvedCatalogSettings = {
35
+ settings: Settings;
36
+ source: "code" | "database";
37
+ version: number | null;
38
+ modelNotes: Record<string, string>;
39
+ };
40
+
41
+ /**
42
+ * Curated workspace Gateway products and workspace-owned custom slugs share
43
+ * one public prefix. Only the latter have a mutable catalog row whose active
44
+ * generation must be rechecked at a fresh acceptance commit boundary.
45
+ */
46
+ export function isWorkspaceGatewayCustomModelId(settings: Settings, modelId: string): boolean {
47
+ return (
48
+ modelId.startsWith(WORKSPACE_GATEWAY_MODEL_ID_PREFIX) &&
49
+ !configuredGatewayWorkspaceProductModelIds(settings).includes(modelId)
50
+ );
51
+ }
52
+
53
+ export function isWorkspaceOpenRouterCustomModelId(settings: Settings, modelId: string): boolean {
54
+ return (
55
+ modelId.startsWith(WORKSPACE_OPENROUTER_MODEL_ID_PREFIX) &&
56
+ !configuredOpenRouterWorkspaceProductModelIds(settings).includes(modelId)
57
+ );
58
+ }
59
+
60
+ export type WorkspaceCustomModelReference = {
61
+ providerKind: "vercel_gateway" | "openrouter";
62
+ upstreamModelId: string;
63
+ };
64
+
65
+ export function workspaceCustomModelReference(
66
+ settings: Settings,
67
+ modelId: string,
68
+ ): WorkspaceCustomModelReference | null {
69
+ if (isWorkspaceGatewayCustomModelId(settings, modelId)) {
70
+ return {
71
+ providerKind: "vercel_gateway",
72
+ upstreamModelId: modelId.slice(WORKSPACE_GATEWAY_MODEL_ID_PREFIX.length),
73
+ };
74
+ }
75
+ if (isWorkspaceOpenRouterCustomModelId(settings, modelId)) {
76
+ return {
77
+ providerKind: "openrouter",
78
+ upstreamModelId: modelId.slice(WORKSPACE_OPENROUTER_MODEL_ID_PREFIX.length),
79
+ };
80
+ }
81
+ return null;
82
+ }
83
+
84
+ export function isWorkspaceCustomModelId(settings: Settings, modelId: string): boolean {
85
+ return workspaceCustomModelReference(settings, modelId) !== null;
86
+ }
87
+
88
+ /**
89
+ * Resolve the deployment catalog without making synchronous env settings read
90
+ * Postgres. Database mode fails closed when the singleton is absent or invalid;
91
+ * code mode preserves the already-validated env catalog.
92
+ */
93
+ export async function resolveCatalogSettings(
94
+ db: Database,
95
+ envSettings: Settings,
96
+ ): Promise<ResolvedCatalogSettings> {
97
+ if (envSettings.modelCatalogSource === "code") {
98
+ validateModelCatalogSettings(envSettings);
99
+ return {
100
+ settings: envSettings,
101
+ source: "code",
102
+ version: null,
103
+ modelNotes: configuredModelNotes(envSettings),
104
+ };
105
+ }
106
+
107
+ const row = await getDeploymentModelCatalog(db);
108
+ if (!row) {
109
+ throw new Error("database model catalog source is configured but the singleton row is missing");
110
+ }
111
+ const settings = applyModelCatalogDocument(envSettings, row.document);
112
+ validateModelCatalogSettings(settings);
113
+ return {
114
+ settings,
115
+ source: "database",
116
+ version: row.version,
117
+ modelNotes: configuredModelNotes(settings),
118
+ };
119
+ }
120
+
121
+ /**
122
+ * Resolve the deployment catalog and add only the custom Gateway slugs owned by
123
+ * one workspace. Use this at model-bearing workspace boundaries; public config
124
+ * and deployment-operator surfaces must continue to use `resolveCatalogSettings`.
125
+ */
126
+ export async function resolveWorkspaceCatalogSettings(
127
+ db: Database,
128
+ envSettings: Settings,
129
+ input: {
130
+ accountId: string;
131
+ workspaceId: string;
132
+ retainedProductModelId?: string | null;
133
+ retainedProductModelIds?: readonly (string | null | undefined)[];
134
+ },
135
+ ): Promise<ResolvedCatalogSettings> {
136
+ const retainedProductModelIds = [
137
+ ...(input.retainedProductModelIds ?? []),
138
+ input.retainedProductModelId,
139
+ ];
140
+ const retainedGatewayUpstreamModelIds = retainedProductModelIds.flatMap((productModelId) =>
141
+ productModelId?.startsWith(WORKSPACE_GATEWAY_MODEL_ID_PREFIX)
142
+ ? [productModelId.slice(WORKSPACE_GATEWAY_MODEL_ID_PREFIX.length)]
143
+ : [],
144
+ );
145
+ const retainedOpenRouterUpstreamModelIds = retainedProductModelIds.flatMap((productModelId) =>
146
+ productModelId?.startsWith(WORKSPACE_OPENROUTER_MODEL_ID_PREFIX)
147
+ ? [productModelId.slice(WORKSPACE_OPENROUTER_MODEL_ID_PREFIX.length)]
148
+ : [],
149
+ );
150
+ const [
151
+ resolved,
152
+ activeGatewayCustomModels,
153
+ activeOpenRouterCustomModels,
154
+ retainedGatewayCustomModels,
155
+ retainedOpenRouterCustomModels,
156
+ ] = await Promise.all([
157
+ resolveCatalogSettings(db, envSettings),
158
+ listWorkspaceGatewayCustomModels(db, {
159
+ accountId: input.accountId,
160
+ workspaceId: input.workspaceId,
161
+ }),
162
+ listWorkspaceOpenRouterCustomModels(db, {
163
+ accountId: input.accountId,
164
+ workspaceId: input.workspaceId,
165
+ }),
166
+ Promise.all(
167
+ [...new Set(retainedGatewayUpstreamModelIds)].map(
168
+ async (upstreamModelId) =>
169
+ await getWorkspaceGatewayCustomModelForExecution(db, {
170
+ accountId: input.accountId,
171
+ workspaceId: input.workspaceId,
172
+ upstreamModelId,
173
+ }),
174
+ ),
175
+ ),
176
+ Promise.all(
177
+ [...new Set(retainedOpenRouterUpstreamModelIds)].map(
178
+ async (upstreamModelId) =>
179
+ await getWorkspaceOpenRouterCustomModelForExecution(db, {
180
+ accountId: input.accountId,
181
+ workspaceId: input.workspaceId,
182
+ upstreamModelId,
183
+ }),
184
+ ),
185
+ ),
186
+ ]);
187
+ const includeRetainedModels = <T extends { upstreamModelId: string }>(
188
+ activeModels: readonly T[],
189
+ retainedModels: readonly (T | null)[],
190
+ ): T[] => {
191
+ const customModels = [...activeModels];
192
+ const includedUpstreamModelIds = new Set(activeModels.map((model) => model.upstreamModelId));
193
+ for (const retainedCustomModel of retainedModels) {
194
+ if (
195
+ retainedCustomModel &&
196
+ !includedUpstreamModelIds.has(retainedCustomModel.upstreamModelId)
197
+ ) {
198
+ customModels.push(retainedCustomModel);
199
+ includedUpstreamModelIds.add(retainedCustomModel.upstreamModelId);
200
+ }
201
+ }
202
+ return customModels;
203
+ };
204
+ const gatewayCustomModels = includeRetainedModels(
205
+ activeGatewayCustomModels,
206
+ retainedGatewayCustomModels,
207
+ );
208
+ const openRouterCustomModels = includeRetainedModels(
209
+ activeOpenRouterCustomModels,
210
+ retainedOpenRouterCustomModels,
211
+ );
212
+ return {
213
+ ...resolved,
214
+ settings: withWorkspaceOpenRouterCatalogProvider(
215
+ withWorkspaceGatewayCatalogProvider(resolved.settings, gatewayCustomModels),
216
+ openRouterCustomModels,
217
+ ),
218
+ };
219
+ }
220
+
221
+ export type ModelAvailabilityObservation = {
222
+ status: "available" | "degraded" | "unavailable";
223
+ reason: "not_entitled" | "provider_unhealthy" | null;
224
+ checkedAt: string;
225
+ };
226
+
227
+ export type ModelCredentialReadinessObservation =
228
+ | { status: "ready"; checkedAt: string }
229
+ | {
230
+ status: "not_ready";
231
+ reason: "prerequisites_missing" | "needs_reauth";
232
+ checkedAt: string;
233
+ }
234
+ | { status: "error"; reason: "resolver_error"; checkedAt: string };
235
+
236
+ export const MODEL_CREDENTIAL_READINESS_OBSERVATION_MAX_AGE_MS = 5 * 60_000;
237
+
238
+ export type WorkspaceModelSelectionInput = {
239
+ settings: Settings;
240
+ policy: WorkspaceModelPolicyContract | null;
241
+ codexSubscriptionActive: boolean;
242
+ xaiSubscriptionActive?: boolean;
243
+ workspaceGatewayConnectionActive?: boolean;
244
+ workspaceOpenRouterConnectionActive?: boolean;
245
+ workspaceGatewayCustomModels?: readonly {
246
+ upstreamModelId: string;
247
+ label?: string | null;
248
+ }[];
249
+ workspaceOpenRouterCustomModels?: readonly {
250
+ upstreamModelId: string;
251
+ label?: string | null;
252
+ }[];
253
+ credentialReadinessObservations?:
254
+ | Readonly<Record<string, ModelCredentialReadinessObservation>>
255
+ | undefined;
256
+ observations?: Readonly<Record<string, ModelAvailabilityObservation>> | undefined;
257
+ now?: Date | undefined;
258
+ credentialReadinessMaxAgeMs?: number | undefined;
259
+ };
260
+
261
+ export type WorkspaceModelSelection = {
262
+ model: ConfiguredModel;
263
+ credentialReadiness: ModelCredentialReadinessV1;
264
+ policyAllowed: boolean;
265
+ availability: ModelAvailabilityV1;
266
+ };
267
+
268
+ function modelDefinitionRunnable(model: ConfiguredModel): boolean {
269
+ return (
270
+ model.capabilities.inputModalities.includes("text") &&
271
+ model.capabilities.outputModalities.includes("text") &&
272
+ model.capabilities.transports.sse.runnable
273
+ );
274
+ }
275
+
276
+ function observedCredentialReadiness(input: {
277
+ observation: ModelCredentialReadinessObservation | undefined;
278
+ basis: "connection" | "resolver";
279
+ nowMs: number;
280
+ maxAgeMs: number;
281
+ }): ModelCredentialReadinessV1 {
282
+ if (!input.observation) {
283
+ return {
284
+ status: "not_ready",
285
+ reason: "prerequisites_missing",
286
+ basis: input.basis,
287
+ checkedAt: null,
288
+ };
289
+ }
290
+ const checkedAtMs = Date.parse(input.observation.checkedAt);
291
+ if (!Number.isFinite(checkedAtMs)) {
292
+ return {
293
+ status: "error",
294
+ reason: "resolver_error",
295
+ basis: input.basis,
296
+ checkedAt: null,
297
+ };
298
+ }
299
+ const checkedAt = new Date(checkedAtMs).toISOString();
300
+ if (Math.abs(input.nowMs - checkedAtMs) > input.maxAgeMs) {
301
+ return {
302
+ status: "not_ready",
303
+ reason: "observation_stale",
304
+ basis: input.basis,
305
+ checkedAt,
306
+ };
307
+ }
308
+ if (input.observation.status === "ready") {
309
+ return { status: "ready", reason: null, basis: input.basis, checkedAt };
310
+ }
311
+ if (input.observation.status === "not_ready") {
312
+ return {
313
+ status: "not_ready",
314
+ reason:
315
+ input.observation.reason === "needs_reauth" ? "needs_reauth" : "prerequisites_missing",
316
+ basis: input.basis,
317
+ checkedAt,
318
+ };
319
+ }
320
+ return {
321
+ status: "error",
322
+ reason: "resolver_error",
323
+ basis: input.basis,
324
+ checkedAt,
325
+ };
326
+ }
327
+
328
+ function credentialReadinessFor(input: {
329
+ model: ConfiguredModel;
330
+ provider: ReturnType<typeof configuredProviders>[number] | undefined;
331
+ codexSubscriptionActive: boolean;
332
+ xaiSubscriptionActive: boolean;
333
+ workspaceGatewayConnectionActive: boolean;
334
+ workspaceOpenRouterConnectionActive: boolean;
335
+ observation: ModelCredentialReadinessObservation | undefined;
336
+ nowMs: number;
337
+ maxAgeMs: number;
338
+ }): ModelCredentialReadinessV1 {
339
+ const source = input.model.credentialSource;
340
+ if (source.kind === "connected_subscription") {
341
+ const active =
342
+ source.provider === "xai" ? input.xaiSubscriptionActive : input.codexSubscriptionActive;
343
+ return active
344
+ ? { status: "ready", reason: null, basis: "connection", checkedAt: null }
345
+ : {
346
+ status: "not_ready",
347
+ reason: "needs_reauth",
348
+ basis: "connection",
349
+ checkedAt: null,
350
+ };
351
+ }
352
+ if (source.kind === "workspace_connection") {
353
+ const connectionActive =
354
+ input.model.providerId === WORKSPACE_OPENROUTER_PROVIDER_ID
355
+ ? input.workspaceOpenRouterConnectionActive
356
+ : input.workspaceGatewayConnectionActive;
357
+ return connectionActive
358
+ ? { status: "ready", reason: null, basis: "connection", checkedAt: null }
359
+ : {
360
+ status: "not_ready",
361
+ reason: "needs_reauth",
362
+ basis: "connection",
363
+ checkedAt: null,
364
+ };
365
+ }
366
+ if (source.kind === "deployment" && source.mechanism === "none") {
367
+ return { status: "ready", reason: null, basis: "configuration", checkedAt: null };
368
+ }
369
+ if (source.kind === "deployment" && source.mechanism === "api_key") {
370
+ return input.provider?.apiKey
371
+ ? { status: "ready", reason: null, basis: "configuration", checkedAt: null }
372
+ : {
373
+ status: "not_ready",
374
+ reason: "missing_credential",
375
+ basis: "configuration",
376
+ checkedAt: null,
377
+ };
378
+ }
379
+ return observedCredentialReadiness({
380
+ observation: input.observation,
381
+ basis: "resolver",
382
+ nowMs: input.nowMs,
383
+ maxAgeMs: input.maxAgeMs,
384
+ });
385
+ }
386
+
387
+ function isXaiGrokModel(model: ConfiguredModel): boolean {
388
+ return model.providerId === "xai" && model.id.startsWith("xai/grok-");
389
+ }
390
+
391
+ function observationTimestamp(observation: ModelAvailabilityObservation | undefined): {
392
+ checkedAt: string | null;
393
+ checkedAtMs: number | null;
394
+ } {
395
+ if (!observation || typeof observation.checkedAt !== "string") {
396
+ return { checkedAt: null, checkedAtMs: null };
397
+ }
398
+ const checkedAtMs = Date.parse(observation.checkedAt);
399
+ if (!Number.isFinite(checkedAtMs)) {
400
+ return { checkedAt: null, checkedAtMs: null };
401
+ }
402
+ return { checkedAt: new Date(checkedAtMs).toISOString(), checkedAtMs };
403
+ }
404
+
405
+ function xaiGrokAvailabilityFor(input: {
406
+ observation: ModelAvailabilityObservation | undefined;
407
+ nowMs: number;
408
+ maxAgeMs: number;
409
+ }): ModelAvailabilityV1 {
410
+ const { checkedAt, checkedAtMs } = observationTimestamp(input.observation);
411
+ const freshSuccessfulObservation =
412
+ input.observation?.status === "available" &&
413
+ input.observation.reason === null &&
414
+ checkedAtMs !== null &&
415
+ checkedAtMs <= input.nowMs &&
416
+ input.nowMs - checkedAtMs <= input.maxAgeMs;
417
+
418
+ if (freshSuccessfulObservation) {
419
+ return {
420
+ status: "available",
421
+ selectable: true,
422
+ reason: null,
423
+ checkedAt,
424
+ };
425
+ }
426
+
427
+ return {
428
+ status: "unavailable",
429
+ selectable: false,
430
+ reason:
431
+ input.observation?.status === "unavailable"
432
+ ? (input.observation.reason ?? "provider_unhealthy")
433
+ : "provider_unhealthy",
434
+ checkedAt,
435
+ };
436
+ }
437
+
438
+ function availabilityFor(input: {
439
+ model: ConfiguredModel;
440
+ credentialReadiness: ModelCredentialReadinessV1;
441
+ policyAllowed: boolean;
442
+ observation?: ModelAvailabilityObservation | undefined;
443
+ nowMs: number;
444
+ maxAgeMs: number;
445
+ }): ModelAvailabilityV1 {
446
+ if (!modelDefinitionRunnable(input.model)) {
447
+ return {
448
+ status: "unavailable",
449
+ selectable: false,
450
+ reason: "unsupported",
451
+ checkedAt: null,
452
+ };
453
+ }
454
+ if (input.credentialReadiness.status !== "ready") {
455
+ return {
456
+ status: "unavailable",
457
+ selectable: false,
458
+ reason:
459
+ input.credentialReadiness.reason === "missing_credential"
460
+ ? "missing_credential"
461
+ : input.credentialReadiness.reason === "needs_reauth"
462
+ ? "needs_reauth"
463
+ : "credential_not_ready",
464
+ checkedAt: input.credentialReadiness.checkedAt,
465
+ };
466
+ }
467
+ if (!input.policyAllowed) {
468
+ return {
469
+ status: "unavailable",
470
+ selectable: false,
471
+ reason: "policy_blocked",
472
+ checkedAt: null,
473
+ };
474
+ }
475
+ if (isXaiGrokModel(input.model)) {
476
+ return xaiGrokAvailabilityFor({
477
+ observation: input.observation,
478
+ nowMs: input.nowMs,
479
+ maxAgeMs: input.maxAgeMs,
480
+ });
481
+ }
482
+ if (!input.observation) {
483
+ return { status: "unknown", selectable: true, reason: null, checkedAt: null };
484
+ }
485
+ if (input.observation.status === "unavailable") {
486
+ return {
487
+ status: "unavailable",
488
+ selectable: false,
489
+ reason: input.observation.reason ?? "provider_unhealthy",
490
+ checkedAt: input.observation.checkedAt,
491
+ };
492
+ }
493
+ return {
494
+ status: input.observation.status,
495
+ selectable: true,
496
+ reason: null,
497
+ checkedAt: input.observation.checkedAt,
498
+ };
499
+ }
500
+
501
+ /**
502
+ * One shared picker/tool decision. Catalog membership, credential readiness,
503
+ * workspace policy, and optional provider-health observations are evaluated in
504
+ * configured catalog order so every consumer exposes the same selectable set.
505
+ */
506
+ export function resolveWorkspaceModelSelection(
507
+ input: WorkspaceModelSelectionInput,
508
+ ): WorkspaceModelSelection[] {
509
+ const codexSettings = input.settings.codexSubscriptionEnabled
510
+ ? withCodexCatalogProvider(input.settings)
511
+ : input.settings;
512
+ const xaiSettings = input.settings.supergrokSubscriptionEnabled
513
+ ? withXaiSubscriptionCatalogProvider(codexSettings)
514
+ : codexSettings;
515
+ const catalogSettings = withWorkspaceOpenRouterCatalogProvider(
516
+ withWorkspaceGatewayCatalogProvider(xaiSettings, input.workspaceGatewayCustomModels ?? []),
517
+ input.workspaceOpenRouterCustomModels ?? [],
518
+ );
519
+ const providers = new Map(
520
+ configuredProviders(catalogSettings).map((provider) => [provider.id, provider]),
521
+ );
522
+ const requestedNowMs = input.now?.getTime();
523
+ const nowMs =
524
+ typeof requestedNowMs === "number" && Number.isFinite(requestedNowMs)
525
+ ? requestedNowMs
526
+ : Date.now();
527
+ const maxAgeMs =
528
+ typeof input.credentialReadinessMaxAgeMs === "number" &&
529
+ Number.isFinite(input.credentialReadinessMaxAgeMs) &&
530
+ input.credentialReadinessMaxAgeMs >= 0
531
+ ? input.credentialReadinessMaxAgeMs
532
+ : MODEL_CREDENTIAL_READINESS_OBSERVATION_MAX_AGE_MS;
533
+
534
+ return configuredModels(catalogSettings).map((model) => {
535
+ const provider = providers.get(model.providerId);
536
+ const policyAllowed = evaluateWorkspaceModelPolicy(input.policy, {
537
+ providerId: model.providerId,
538
+ modelId: model.id,
539
+ }).allowed;
540
+ const credentialReadiness = credentialReadinessFor({
541
+ model,
542
+ provider,
543
+ codexSubscriptionActive: input.codexSubscriptionActive,
544
+ xaiSubscriptionActive: input.xaiSubscriptionActive === true,
545
+ workspaceGatewayConnectionActive: input.workspaceGatewayConnectionActive === true,
546
+ workspaceOpenRouterConnectionActive: input.workspaceOpenRouterConnectionActive === true,
547
+ observation: input.credentialReadinessObservations?.[model.definitionVersion],
548
+ nowMs,
549
+ maxAgeMs,
550
+ });
551
+ return {
552
+ model,
553
+ credentialReadiness,
554
+ policyAllowed,
555
+ availability: availabilityFor({
556
+ model,
557
+ credentialReadiness,
558
+ policyAllowed,
559
+ observation: input.observations?.[model.definitionVersion],
560
+ nowMs,
561
+ maxAgeMs,
562
+ }),
563
+ };
564
+ });
565
+ }