@opengeni/api-router 0.7.3 → 0.9.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.
@@ -0,0 +1,337 @@
1
+ import {
2
+ configuredModels,
3
+ configuredProviders,
4
+ withCodexCatalogProvider,
5
+ type ConfiguredModel,
6
+ type Settings,
7
+ } from "@opengeni/config";
8
+ import {
9
+ ClientModel,
10
+ WorkspaceModelCatalogResponse,
11
+ evaluateWorkspaceModelPolicy,
12
+ type ModelAvailabilityV1,
13
+ type ModelCredentialReadinessV1,
14
+ type WorkspaceModelCatalogResponse as WorkspaceModelCatalogResponseType,
15
+ type WorkspaceModelPolicyContract,
16
+ } from "@opengeni/contracts";
17
+
18
+ export type ModelAvailabilityObservation = {
19
+ status: "available" | "degraded" | "unavailable";
20
+ reason: "not_entitled" | "provider_unhealthy" | null;
21
+ checkedAt: string;
22
+ };
23
+
24
+ export type ModelCredentialReadinessObservation =
25
+ | { status: "ready"; checkedAt: string }
26
+ | {
27
+ status: "not_ready";
28
+ reason: "prerequisites_missing" | "needs_reauth";
29
+ checkedAt: string;
30
+ }
31
+ | { status: "error"; reason: "resolver_error"; checkedAt: string };
32
+
33
+ export const MODEL_CREDENTIAL_READINESS_OBSERVATION_MAX_AGE_MS = 5 * 60_000;
34
+
35
+ /** Static, client-safe definition projection. No provider secret is reachable. */
36
+ export function projectClientModel(model: ConfiguredModel): ClientModel {
37
+ return ClientModel.parse({
38
+ id: model.id,
39
+ label: model.label,
40
+ provider: model.providerId,
41
+ providerLabel: model.providerLabel,
42
+ api: model.api,
43
+ ...(model.contextWindowTokens === undefined
44
+ ? {}
45
+ : { contextWindowTokens: model.contextWindowTokens }),
46
+ schemaVersion: model.schemaVersion,
47
+ aliases: model.aliases,
48
+ deployment: model.deployment,
49
+ executionLimits: model.executionLimits,
50
+ credentialSource: model.credentialSource,
51
+ billing: model.billing,
52
+ capabilities: model.capabilities,
53
+ ...(model.pricing === undefined ? {} : { pricing: model.pricing }),
54
+ definitionVersion: model.definitionVersion,
55
+ });
56
+ }
57
+
58
+ function modelDefinitionRunnable(model: ConfiguredModel): boolean {
59
+ return (
60
+ model.capabilities.inputModalities.includes("text") &&
61
+ model.capabilities.outputModalities.includes("text") &&
62
+ model.capabilities.transports.sse.runnable
63
+ );
64
+ }
65
+
66
+ function observedCredentialReadiness(input: {
67
+ observation: ModelCredentialReadinessObservation | undefined;
68
+ basis: "connection" | "resolver";
69
+ nowMs: number;
70
+ maxAgeMs: number;
71
+ }): ModelCredentialReadinessV1 {
72
+ if (!input.observation) {
73
+ return {
74
+ status: "not_ready",
75
+ reason: "prerequisites_missing",
76
+ basis: input.basis,
77
+ checkedAt: null,
78
+ };
79
+ }
80
+ const checkedAtMs = Date.parse(input.observation.checkedAt);
81
+ if (!Number.isFinite(checkedAtMs)) {
82
+ return {
83
+ status: "error",
84
+ reason: "resolver_error",
85
+ basis: input.basis,
86
+ checkedAt: null,
87
+ };
88
+ }
89
+ const checkedAt = new Date(checkedAtMs).toISOString();
90
+ if (Math.abs(input.nowMs - checkedAtMs) > input.maxAgeMs) {
91
+ return {
92
+ status: "not_ready",
93
+ reason: "observation_stale",
94
+ basis: input.basis,
95
+ checkedAt,
96
+ };
97
+ }
98
+ if (input.observation.status === "ready") {
99
+ return { status: "ready", reason: null, basis: input.basis, checkedAt };
100
+ }
101
+ if (input.observation.status === "not_ready") {
102
+ return {
103
+ status: "not_ready",
104
+ reason:
105
+ input.observation.reason === "needs_reauth" ? "needs_reauth" : "prerequisites_missing",
106
+ basis: input.basis,
107
+ checkedAt,
108
+ };
109
+ }
110
+ return {
111
+ status: "error",
112
+ reason: "resolver_error",
113
+ basis: input.basis,
114
+ checkedAt,
115
+ };
116
+ }
117
+
118
+ function credentialReadinessFor(input: {
119
+ model: ConfiguredModel;
120
+ provider: ReturnType<typeof configuredProviders>[number] | undefined;
121
+ codexSubscriptionActive: boolean;
122
+ observation: ModelCredentialReadinessObservation | undefined;
123
+ nowMs: number;
124
+ maxAgeMs: number;
125
+ }): ModelCredentialReadinessV1 {
126
+ const source = input.model.credentialSource;
127
+ if (source.kind === "connected_subscription") {
128
+ return input.codexSubscriptionActive
129
+ ? { status: "ready", reason: null, basis: "connection", checkedAt: null }
130
+ : {
131
+ status: "not_ready",
132
+ reason: "needs_reauth",
133
+ basis: "connection",
134
+ checkedAt: null,
135
+ };
136
+ }
137
+ if (source.kind === "deployment" && source.mechanism === "api_key") {
138
+ return input.provider?.apiKey
139
+ ? { status: "ready", reason: null, basis: "configuration", checkedAt: null }
140
+ : {
141
+ status: "not_ready",
142
+ reason: "missing_credential",
143
+ basis: "configuration",
144
+ checkedAt: null,
145
+ };
146
+ }
147
+ return observedCredentialReadiness({
148
+ observation: input.observation,
149
+ basis: source.kind === "workspace_connection" ? "connection" : "resolver",
150
+ nowMs: input.nowMs,
151
+ maxAgeMs: input.maxAgeMs,
152
+ });
153
+ }
154
+
155
+ function isXaiGrokModel(model: ConfiguredModel): boolean {
156
+ return model.providerId === "xai" && model.id.startsWith("xai/grok-");
157
+ }
158
+
159
+ function observationTimestamp(observation: ModelAvailabilityObservation | undefined): {
160
+ checkedAt: string | null;
161
+ checkedAtMs: number | null;
162
+ } {
163
+ if (!observation || typeof observation.checkedAt !== "string") {
164
+ return { checkedAt: null, checkedAtMs: null };
165
+ }
166
+ const checkedAtMs = Date.parse(observation.checkedAt);
167
+ if (!Number.isFinite(checkedAtMs)) {
168
+ return { checkedAt: null, checkedAtMs: null };
169
+ }
170
+ return { checkedAt: new Date(checkedAtMs).toISOString(), checkedAtMs };
171
+ }
172
+
173
+ function xaiGrokAvailabilityFor(input: {
174
+ observation: ModelAvailabilityObservation | undefined;
175
+ nowMs: number;
176
+ maxAgeMs: number;
177
+ }): ModelAvailabilityV1 {
178
+ const { checkedAt, checkedAtMs } = observationTimestamp(input.observation);
179
+ const freshSuccessfulObservation =
180
+ input.observation?.status === "available" &&
181
+ input.observation.reason === null &&
182
+ checkedAtMs !== null &&
183
+ checkedAtMs <= input.nowMs &&
184
+ input.nowMs - checkedAtMs <= input.maxAgeMs;
185
+
186
+ if (freshSuccessfulObservation) {
187
+ return {
188
+ status: "available",
189
+ selectable: true,
190
+ reason: null,
191
+ checkedAt,
192
+ };
193
+ }
194
+
195
+ return {
196
+ status: "unavailable",
197
+ selectable: false,
198
+ reason:
199
+ input.observation?.status === "unavailable"
200
+ ? (input.observation.reason ?? "provider_unhealthy")
201
+ : "provider_unhealthy",
202
+ checkedAt,
203
+ };
204
+ }
205
+
206
+ function availabilityFor(input: {
207
+ model: ConfiguredModel;
208
+ credentialReadiness: ModelCredentialReadinessV1;
209
+ policy: WorkspaceModelPolicyContract | null;
210
+ observation?: ModelAvailabilityObservation | undefined;
211
+ nowMs: number;
212
+ maxAgeMs: number;
213
+ }): ModelAvailabilityV1 {
214
+ if (!modelDefinitionRunnable(input.model)) {
215
+ return {
216
+ status: "unavailable",
217
+ selectable: false,
218
+ reason: "unsupported",
219
+ checkedAt: null,
220
+ };
221
+ }
222
+ if (input.credentialReadiness.status !== "ready") {
223
+ return {
224
+ status: "unavailable",
225
+ selectable: false,
226
+ reason:
227
+ input.credentialReadiness.reason === "missing_credential"
228
+ ? "missing_credential"
229
+ : input.credentialReadiness.reason === "needs_reauth"
230
+ ? "needs_reauth"
231
+ : "credential_not_ready",
232
+ checkedAt: input.credentialReadiness.checkedAt,
233
+ };
234
+ }
235
+ if (
236
+ !evaluateWorkspaceModelPolicy(input.policy, {
237
+ providerId: input.model.providerId,
238
+ modelId: input.model.id,
239
+ }).allowed
240
+ ) {
241
+ return {
242
+ status: "unavailable",
243
+ selectable: false,
244
+ reason: "policy_blocked",
245
+ checkedAt: null,
246
+ };
247
+ }
248
+ if (isXaiGrokModel(input.model)) {
249
+ return xaiGrokAvailabilityFor({
250
+ observation: input.observation,
251
+ nowMs: input.nowMs,
252
+ maxAgeMs: input.maxAgeMs,
253
+ });
254
+ }
255
+ if (!input.observation) {
256
+ // Credential readiness and policy are known-good, but no current
257
+ // provider-health observation is available. Unknown is intentionally
258
+ // selectable; the execution boundary rechecks all authoritative gates.
259
+ return { status: "unknown", selectable: true, reason: null, checkedAt: null };
260
+ }
261
+ if (input.observation.status === "unavailable") {
262
+ return {
263
+ status: "unavailable",
264
+ selectable: false,
265
+ reason: input.observation.reason ?? "provider_unhealthy",
266
+ checkedAt: input.observation.checkedAt,
267
+ };
268
+ }
269
+ return {
270
+ status: input.observation.status,
271
+ selectable: true,
272
+ reason: null,
273
+ checkedAt: input.observation.checkedAt,
274
+ };
275
+ }
276
+
277
+ /**
278
+ * Authenticated workspace catalog. Codex definitions are included only when
279
+ * the deployment enables that connection type; concrete readiness is supplied
280
+ * by the existing metadata-only DB seam. API-key presence establishes only
281
+ * local configuration readiness. Federated/token credentials require a fresh,
282
+ * successful typed resolver observation; credential health and provider health
283
+ * are separate inputs and neither is fabricated.
284
+ */
285
+ export function buildWorkspaceModelCatalog(input: {
286
+ settings: Settings;
287
+ policy: WorkspaceModelPolicyContract | null;
288
+ codexSubscriptionActive: boolean;
289
+ credentialReadinessObservations?:
290
+ | Readonly<Record<string, ModelCredentialReadinessObservation>>
291
+ | undefined;
292
+ observations?: Readonly<Record<string, ModelAvailabilityObservation>> | undefined;
293
+ now?: Date | undefined;
294
+ credentialReadinessMaxAgeMs?: number | undefined;
295
+ }): WorkspaceModelCatalogResponseType {
296
+ const catalogSettings = input.settings.codexSubscriptionEnabled
297
+ ? withCodexCatalogProvider(input.settings)
298
+ : input.settings;
299
+ const providers = new Map(
300
+ configuredProviders(catalogSettings).map((provider) => [provider.id, provider]),
301
+ );
302
+ const requestedNowMs = input.now?.getTime();
303
+ const nowMs =
304
+ typeof requestedNowMs === "number" && Number.isFinite(requestedNowMs)
305
+ ? requestedNowMs
306
+ : Date.now();
307
+ const maxAgeMs =
308
+ typeof input.credentialReadinessMaxAgeMs === "number" &&
309
+ Number.isFinite(input.credentialReadinessMaxAgeMs) &&
310
+ input.credentialReadinessMaxAgeMs >= 0
311
+ ? input.credentialReadinessMaxAgeMs
312
+ : MODEL_CREDENTIAL_READINESS_OBSERVATION_MAX_AGE_MS;
313
+ const models = configuredModels(catalogSettings).map((model) => {
314
+ const provider = providers.get(model.providerId);
315
+ const credentialReadiness = credentialReadinessFor({
316
+ model,
317
+ provider,
318
+ codexSubscriptionActive: input.codexSubscriptionActive,
319
+ observation: input.credentialReadinessObservations?.[model.definitionVersion],
320
+ nowMs,
321
+ maxAgeMs,
322
+ });
323
+ return {
324
+ ...projectClientModel(model),
325
+ credentialReadiness,
326
+ availability: availabilityFor({
327
+ model,
328
+ credentialReadiness,
329
+ policy: input.policy,
330
+ observation: input.observations?.[model.definitionVersion],
331
+ nowMs,
332
+ maxAgeMs,
333
+ }),
334
+ };
335
+ });
336
+ return WorkspaceModelCatalogResponse.parse({ models });
337
+ }