@opengeni/config 0.9.3 → 0.10.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opengeni/config",
3
- "version": "0.9.3",
3
+ "version": "0.10.1",
4
4
  "description": "OpenGeni runtime configuration: settings resolution, deployment knobs, and config validation shared across the server packages.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -34,7 +34,7 @@
34
34
  },
35
35
  "dependencies": {
36
36
  "@opengeni/codex": "^0.2.9",
37
- "@opengeni/contracts": "^0.30.0",
37
+ "@opengeni/contracts": "^0.31.1",
38
38
  "zod": "^4.2.1"
39
39
  },
40
40
  "engines": {
package/src/index.ts CHANGED
@@ -355,6 +355,10 @@ const SettingsSchema = z.object({
355
355
  openaiBaseUrl: z.string().optional(),
356
356
  openaiModel: z.string().default("gpt-5.6-sol"),
357
357
  openaiAllowedModels: z.string().default("gpt-5.6-sol,gpt-5.6-terra,gpt-5.6-luna"),
358
+ // OpenGeni-managed Vercel AI Gateway. When configured, the two reviewed
359
+ // Gateway models below are added to the managed-credit catalog. Workspace
360
+ // Gateway keys use the encrypted connection broker and never this secret.
361
+ vercelAiGatewayApiKey: z.string().optional(),
358
362
  // Native composer voice input (browser MediaRecorder → API transcription).
359
363
  // Provider credentials stay server-side; ClientConfig only projects availability
360
364
  // and hard ceilings. Selection happens once before audio is sent — never retry
@@ -480,6 +484,15 @@ const SettingsSchema = z.object({
480
484
  dockerWorkspaceBaseDir: z.string().min(1).optional(),
481
485
  modalAppName: z.string().default("opengeni-sandbox"),
482
486
  modalImageRef: z.string().optional(),
487
+ // Provider-native immutable Modal image ID for the exact logical
488
+ // `modalImageRef`. When set, the runtime uses ModalImageSelector.fromId and
489
+ // never asks Modal to parse or import the registry ref. The logical ref is
490
+ // still persisted on the sandbox lease for provenance and conflict fencing;
491
+ // the Modal session envelope persists the actual image ID.
492
+ modalImageId: z
493
+ .string()
494
+ .regex(/^im-[A-Za-z0-9]{22}$/)
495
+ .optional(),
483
496
  // Name of a Modal Secret (containing REGISTRY_USERNAME + REGISTRY_PASSWORD) used
484
497
  // to authenticate the pull of `modalImageRef` from a PRIVATE registry. When UNSET
485
498
  // (the default), the sandbox image is pulled UNAUTHENTICATED — i.e. it must be a
@@ -1169,6 +1182,9 @@ export const ModelCapabilitiesV1Schema = z
1169
1182
  responsesWebSocket: CapabilityStateV1Schema,
1170
1183
  realtimeAudio: CapabilityStateV1Schema,
1171
1184
  }),
1185
+ promptCaching: CapabilityStateV1Schema.extend({
1186
+ mode: z.enum(["implicit", "automatic", "none"]),
1187
+ }).optional(),
1172
1188
  latencyModes: z
1173
1189
  .array(
1174
1190
  z.object({
@@ -1273,7 +1289,12 @@ export type ModelProviderApi = z.infer<typeof ModelProviderApi>;
1273
1289
  * "codex-subscription" providers authenticate per-request with a ChatGPT/Codex
1274
1290
  * subscription token resolved at call time (no static key) — see @opengeni/codex.
1275
1291
  */
1276
- export const RegistryProviderKind = z.enum(["api-key", "codex-subscription"]);
1292
+ export const RegistryProviderKind = z.enum([
1293
+ "api-key",
1294
+ "codex-subscription",
1295
+ "vercel-gateway-managed",
1296
+ "vercel-gateway-workspace",
1297
+ ]);
1277
1298
  export type RegistryProviderKind = z.infer<typeof RegistryProviderKind>;
1278
1299
 
1279
1300
  /** A single model exposed by a registry provider. */
@@ -1326,7 +1347,7 @@ const RegistryModelSchema = z
1326
1347
 
1327
1348
  /** A non-built-in provider declared by the host via OPENGENI_MODEL_PROVIDERS_JSON. */
1328
1349
  const RegistryProviderSchema = z.object({
1329
- kind: RegistryProviderKind.default("api-key"), // "codex-subscription" => per-request token, no static key
1350
+ kind: RegistryProviderKind.default("api-key"),
1330
1351
  id: z.string().min(1).regex(registryId), // stable provider id, e.g. "fireworks"
1331
1352
  label: z.string().min(1).optional(),
1332
1353
  api: ModelProviderApi.default("chat"),
@@ -1392,6 +1413,12 @@ export interface ConfiguredModel {
1392
1413
  credentialSource: CredentialSourceV1;
1393
1414
  billing: BillingAttributionV1;
1394
1415
  capabilities: ModelCapabilitiesV1;
1416
+ requestPolicy?: {
1417
+ gateway: {
1418
+ only: [string];
1419
+ caching: "auto" | "none";
1420
+ };
1421
+ };
1395
1422
  pricing?: ModelPricingScheduleV1 | undefined;
1396
1423
  definitionVersion: string;
1397
1424
  contextWindowTokens?: number | undefined;
@@ -1402,6 +1429,32 @@ export interface ConfiguredModel {
1402
1429
  hostedWebSearch: boolean;
1403
1430
  }
1404
1431
 
1432
+ export const VERCEL_AI_GATEWAY_BASE_URL = "https://ai-gateway.vercel.sh/v1" as const;
1433
+ export const OPENGENI_GATEWAY_PROVIDER_ID = "opengeni-gateway" as const;
1434
+ export const WORKSPACE_GATEWAY_PROVIDER_ID = "workspace-gateway" as const;
1435
+ export const WORKSPACE_GATEWAY_MODEL_ID_PREFIX = "workspace-gateway/" as const;
1436
+ export const VERCEL_AI_GATEWAY_CONNECTION_DOMAIN = "ai-gateway.vercel.sh" as const;
1437
+ export const VERCEL_AI_GATEWAY_CONNECTION_ROLE = "vercel_ai_gateway" as const;
1438
+
1439
+ export const OPENGENI_GATEWAY_MODELS = {
1440
+ deepseek: {
1441
+ productId: "deepseek-v4-flash-0731",
1442
+ workspaceProductId: `${WORKSPACE_GATEWAY_MODEL_ID_PREFIX}deepseek-v4-flash-0731`,
1443
+ upstreamModelId: "deepseek/deepseek-v4-flash-0731",
1444
+ label: "DeepSeek V4 Flash 0731",
1445
+ provider: "deepinfra",
1446
+ implicitCaching: true,
1447
+ },
1448
+ kimi: {
1449
+ productId: "kimi-k3-fast",
1450
+ workspaceProductId: `${WORKSPACE_GATEWAY_MODEL_ID_PREFIX}kimi-k3-fast`,
1451
+ upstreamModelId: "moonshotai/kimi-k3-fast",
1452
+ label: "Kimi K3 Fast",
1453
+ provider: "wafer",
1454
+ implicitCaching: true,
1455
+ },
1456
+ } as const;
1457
+
1405
1458
  /**
1406
1459
  * Built-in OpenGeni credit pricing schedules.
1407
1460
  *
@@ -1476,6 +1529,27 @@ export const defaultModelPricing: Record<string, ModelPricingScheduleV1> = {
1476
1529
  },
1477
1530
  ],
1478
1531
  },
1532
+ // Vercel AI Gateway endpoint prices, provider-pinned in the runtime.
1533
+ // Snapshot: 2026-08-02. Both pinned routes returned discounted implicit
1534
+ // cache reads in live Gateway responses. Wafer/Kimi reported $0.45/M even
1535
+ // though the provider-discovery flag currently says otherwise; bill from
1536
+ // the response-backed rate, not that inconsistent boolean.
1537
+ [OPENGENI_GATEWAY_MODELS.deepseek.productId]: {
1538
+ default: {
1539
+ inputMicrosPerMillionTokens: 90_000,
1540
+ cachedInputMicrosPerMillionTokens: 18_000,
1541
+ outputMicrosPerMillionTokens: 180_000,
1542
+ marginBps: 2_500,
1543
+ },
1544
+ },
1545
+ [OPENGENI_GATEWAY_MODELS.kimi.productId]: {
1546
+ default: {
1547
+ inputMicrosPerMillionTokens: 4_500_000,
1548
+ cachedInputMicrosPerMillionTokens: 450_000,
1549
+ outputMicrosPerMillionTokens: 22_500_000,
1550
+ marginBps: 2_500,
1551
+ },
1552
+ },
1479
1553
  // Fireworks AI / GLM 5.2 — the first shipped non-OpenAI registry model. A
1480
1554
  // built-in default pricing entry makes managed billing work out of the box
1481
1555
  // for hosts that expose this model via OPENGENI_MODEL_PROVIDERS_JSON without
@@ -1647,6 +1721,7 @@ export function getSettings(): Settings {
1647
1721
  openaiBaseUrl: optional("OPENGENI_OPENAI_BASE_URL") ?? optional("OPENAI_BASE_URL"),
1648
1722
  openaiModel: optional("OPENGENI_OPENAI_MODEL"),
1649
1723
  openaiAllowedModels: optional("OPENGENI_OPENAI_ALLOWED_MODELS"),
1724
+ vercelAiGatewayApiKey: optional("OPENGENI_VERCEL_AI_GATEWAY_API_KEY"),
1650
1725
  voiceInputMaxDurationSeconds: optional("OPENGENI_VOICE_INPUT_MAX_DURATION_SECONDS"),
1651
1726
  voiceInputMaxSizeBytes: optional("OPENGENI_VOICE_INPUT_MAX_SIZE_BYTES"),
1652
1727
  voiceInputProviderOrder: optional("OPENGENI_VOICE_INPUT_PROVIDER_ORDER"),
@@ -1691,6 +1766,7 @@ export function getSettings(): Settings {
1691
1766
  dockerWorkspaceBaseDir: optional("OPENGENI_DOCKER_WORKSPACE_BASE_DIR"),
1692
1767
  modalAppName: optional("OPENGENI_MODAL_APP_NAME"),
1693
1768
  modalImageRef: optional("OPENGENI_MODAL_IMAGE_REF"),
1769
+ modalImageId: optional("OPENGENI_MODAL_IMAGE_ID"),
1694
1770
  modalImageRegistrySecret: optional("OPENGENI_MODAL_IMAGE_REGISTRY_SECRET"),
1695
1771
  modalTimeoutSeconds: optional("OPENGENI_MODAL_TIMEOUT_SECONDS"),
1696
1772
  modalTokenId: optional("OPENGENI_MODAL_TOKEN_ID"),
@@ -2192,6 +2268,128 @@ function legacyModelCapabilities(
2192
2268
  });
2193
2269
  }
2194
2270
 
2271
+ export function gatewayRequestPolicyForUpstreamModel(
2272
+ upstreamModelId: string,
2273
+ ): ConfiguredModel["requestPolicy"] {
2274
+ const model = Object.values(OPENGENI_GATEWAY_MODELS).find(
2275
+ (candidate) => candidate.upstreamModelId === upstreamModelId,
2276
+ );
2277
+ if (!model) {
2278
+ return undefined;
2279
+ }
2280
+ return {
2281
+ gateway: {
2282
+ only: [model.provider],
2283
+ caching: model.implicitCaching ? "auto" : "none",
2284
+ },
2285
+ };
2286
+ }
2287
+
2288
+ function gatewayModelCapabilities(
2289
+ settings: Settings,
2290
+ input: { implicitCaching: boolean; vision: boolean },
2291
+ ): ModelCapabilitiesV1 {
2292
+ const legacy = legacyModelCapabilities(settings, {
2293
+ reasoningEffort: true,
2294
+ hostedWebSearch: false,
2295
+ });
2296
+ return normalizeCapabilities({
2297
+ ...legacy,
2298
+ functionCalling: { upstream: "supported", runnable: true },
2299
+ inputModalities: input.vision ? ["text", "image"] : ["text"],
2300
+ transports: {
2301
+ ...legacy.transports,
2302
+ sse: { upstream: "supported", runnable: true },
2303
+ },
2304
+ promptCaching: input.implicitCaching
2305
+ ? { upstream: "supported", runnable: true, mode: "implicit" }
2306
+ : { upstream: "unsupported", runnable: false, mode: "none" },
2307
+ // "Fast" is part of Kimi's product name, not OpenGeni's separately billed
2308
+ // latency mode. Both Gateway products expose only standard here.
2309
+ latencyModes: [{ id: "standard", upstream: "supported", runnable: true }],
2310
+ });
2311
+ }
2312
+
2313
+ function gatewayRegistryProvider(
2314
+ settings: Settings,
2315
+ input:
2316
+ | { kind: "vercel-gateway-managed"; apiKey: string }
2317
+ | { kind: "vercel-gateway-workspace"; apiKey?: string },
2318
+ ): RegistryProvider {
2319
+ const workspace = input.kind === "vercel-gateway-workspace";
2320
+ const models = [OPENGENI_GATEWAY_MODELS.deepseek, OPENGENI_GATEWAY_MODELS.kimi].map((model) => ({
2321
+ id: workspace ? model.workspaceProductId : model.productId,
2322
+ upstreamModelId: model.upstreamModelId,
2323
+ label: model.label,
2324
+ capabilities: gatewayModelCapabilities(settings, {
2325
+ implicitCaching: model.implicitCaching,
2326
+ vision: model === OPENGENI_GATEWAY_MODELS.kimi,
2327
+ }),
2328
+ contextWindowTokens: 1_000_000,
2329
+ effectiveContextWindowTokens: 900_000,
2330
+ autoCompactTokenLimit: 850_000,
2331
+ toolOutputTruncationTokens: settings.modelToolOutputTruncationTokens,
2332
+ }));
2333
+ return {
2334
+ kind: input.kind,
2335
+ id: workspace ? WORKSPACE_GATEWAY_PROVIDER_ID : OPENGENI_GATEWAY_PROVIDER_ID,
2336
+ label: workspace ? "Your Gateway" : "OpenGeni",
2337
+ // Responses preserves vision, reasoning items, and provider-native usage.
2338
+ // Model-specific compatibility stays at the reviewed request fence rather
2339
+ // than downgrading the whole provider wire.
2340
+ api: "responses",
2341
+ baseUrl: VERCEL_AI_GATEWAY_BASE_URL,
2342
+ ...(input.apiKey ? { apiKey: input.apiKey } : {}),
2343
+ models,
2344
+ };
2345
+ }
2346
+
2347
+ function configuredRegistryProviders(settings: Settings): RegistryProvider[] {
2348
+ const providers = parseModelProvidersJson(settings.modelProvidersJson);
2349
+ if (!settings.vercelAiGatewayApiKey) {
2350
+ return providers;
2351
+ }
2352
+ if (providers.some((provider) => provider.id === OPENGENI_GATEWAY_PROVIDER_ID)) {
2353
+ throw new Error(
2354
+ `${OPENGENI_GATEWAY_PROVIDER_ID} is reserved for OPENGENI_VERCEL_AI_GATEWAY_API_KEY`,
2355
+ );
2356
+ }
2357
+ return [
2358
+ ...providers,
2359
+ gatewayRegistryProvider(settings, {
2360
+ kind: "vercel-gateway-managed",
2361
+ apiKey: settings.vercelAiGatewayApiKey,
2362
+ }),
2363
+ ];
2364
+ }
2365
+
2366
+ /** Static catalog overlay; it contains no concrete workspace credential. */
2367
+ export function withWorkspaceGatewayCatalogProvider(settings: Settings): Settings {
2368
+ const providers = parseModelProvidersJson(settings.modelProvidersJson);
2369
+ if (providers.some((provider) => provider.id === WORKSPACE_GATEWAY_PROVIDER_ID)) {
2370
+ return settings;
2371
+ }
2372
+ return {
2373
+ ...settings,
2374
+ modelProvidersJson: JSON.stringify([
2375
+ ...providers,
2376
+ gatewayRegistryProvider(settings, { kind: "vercel-gateway-workspace" }),
2377
+ ]),
2378
+ };
2379
+ }
2380
+
2381
+ /** Runtime overlay after the worker resolves the workspace's encrypted key. */
2382
+ export function withWorkspaceGatewayCredential(settings: Settings, apiKey: string): Settings {
2383
+ if (!apiKey.trim()) {
2384
+ throw new Error("workspace AI Gateway credential is empty");
2385
+ }
2386
+ const catalogSettings = withWorkspaceGatewayCatalogProvider(settings);
2387
+ const providers = parseModelProvidersJson(catalogSettings.modelProvidersJson).map((provider) =>
2388
+ provider.id === WORKSPACE_GATEWAY_PROVIDER_ID ? { ...provider, apiKey } : provider,
2389
+ );
2390
+ return { ...catalogSettings, modelProvidersJson: JSON.stringify(providers) };
2391
+ }
2392
+
2195
2393
  /** OpenAI GPT-5.6 Fast mode is 2× Standard list rates (service_tier fast/priority). */
2196
2394
  const GPT56_FAST_BILLING_MULTIPLIER_BPS = 20_000;
2197
2395
 
@@ -2246,6 +2444,17 @@ function builtinLatencyModesForModel(modelId: string): Array<{
2246
2444
  return [{ id: "standard", upstream: "unknown", runnable: true }];
2247
2445
  }
2248
2446
 
2447
+ function builtinPromptCachingForModel(
2448
+ modelId: string,
2449
+ ): NonNullable<ModelCapabilitiesV1["promptCaching"]> | undefined {
2450
+ const slug = modelId.startsWith(CODEX_MODEL_ID_PREFIX)
2451
+ ? modelId.slice(CODEX_MODEL_ID_PREFIX.length)
2452
+ : modelId;
2453
+ return slug.startsWith("gpt-5.6-")
2454
+ ? { upstream: "supported", runnable: true, mode: "implicit" }
2455
+ : undefined;
2456
+ }
2457
+
2249
2458
  /**
2250
2459
  * Map OpenGeni latency mode to the provider `service_tier` wire value.
2251
2460
  * Azure and Codex ChatGPT accept `priority`; OpenAI API accepts `fast` (alias of priority).
@@ -2302,15 +2511,23 @@ function assertLatencyModeRunnable(
2302
2511
  }
2303
2512
 
2304
2513
  function registryCredentialSource(provider: RegistryProvider): CredentialSourceV1 {
2305
- return provider.kind === "codex-subscription"
2306
- ? { kind: "connected_subscription", provider: "codex" }
2307
- : { kind: "deployment", mechanism: "api_key" };
2514
+ if (provider.kind === "codex-subscription") {
2515
+ return { kind: "connected_subscription", provider: "codex" };
2516
+ }
2517
+ if (provider.kind === "vercel-gateway-workspace") {
2518
+ return { kind: "workspace_connection", mechanism: "api_key" };
2519
+ }
2520
+ return { kind: "deployment", mechanism: "api_key" };
2308
2521
  }
2309
2522
 
2310
2523
  function registryBilling(provider: RegistryProvider): BillingAttributionV1 {
2311
- return provider.kind === "codex-subscription"
2312
- ? { upstreamPayer: "connected_subscription", metering: "external" }
2313
- : { upstreamPayer: "deployment", metering: "opengeni_credits" };
2524
+ if (provider.kind === "codex-subscription") {
2525
+ return { upstreamPayer: "connected_subscription", metering: "external" };
2526
+ }
2527
+ if (provider.kind === "vercel-gateway-workspace") {
2528
+ return { upstreamPayer: "workspace", metering: "external" };
2529
+ }
2530
+ return { upstreamPayer: "deployment", metering: "opengeni_credits" };
2314
2531
  }
2315
2532
 
2316
2533
  function builtinCredentialSource(settings: Settings): CredentialSourceV1 {
@@ -2393,6 +2610,7 @@ function definitionVersionFor(
2393
2610
  billing: model.billing,
2394
2611
  executionLimits: model.executionLimits,
2395
2612
  capabilities: model.capabilities,
2613
+ ...(model.requestPolicy ? { requestPolicy: model.requestPolicy } : {}),
2396
2614
  pricing: model.pricing ?? null,
2397
2615
  });
2398
2616
  return `sha256:${createHash("sha256")
@@ -2445,7 +2663,7 @@ export function configuredProviders(settings: Settings): ResolvedModelProvider[]
2445
2663
  : undefined;
2446
2664
  builtin.apiKey = settings.openaiApiKey;
2447
2665
  }
2448
- const registry = parseModelProvidersJson(settings.modelProvidersJson).map(
2666
+ const registry = configuredRegistryProviders(settings).map(
2449
2667
  (provider): ResolvedModelProvider => ({
2450
2668
  id: provider.id,
2451
2669
  label: provider.label ?? provider.id,
@@ -2488,6 +2706,11 @@ export function withCodexCatalogProvider(settings: Settings): Settings {
2488
2706
  reasoningEffort: true,
2489
2707
  hostedWebSearch: true,
2490
2708
  }),
2709
+ ...(builtinPromptCachingForModel(`${CODEX_MODEL_ID_PREFIX}${slug}`)
2710
+ ? {
2711
+ promptCaching: builtinPromptCachingForModel(`${CODEX_MODEL_ID_PREFIX}${slug}`)!,
2712
+ }
2713
+ : {}),
2491
2714
  latencyModes: builtinLatencyModesForModel(`${CODEX_MODEL_ID_PREFIX}${slug}`),
2492
2715
  };
2493
2716
  return {
@@ -2532,6 +2755,9 @@ export function policyProviderIdForModel(settings: Settings, modelId: string): s
2532
2755
  if (canonicalModelId.startsWith(CODEX_MODEL_ID_PREFIX)) {
2533
2756
  return CODEX_PROVIDER_ID;
2534
2757
  }
2758
+ if (canonicalModelId.startsWith(WORKSPACE_GATEWAY_MODEL_ID_PREFIX)) {
2759
+ return WORKSPACE_GATEWAY_PROVIDER_ID;
2760
+ }
2535
2761
  const configured = configuredModels(settings).find((model) => model.id === canonicalModelId);
2536
2762
  return configured?.providerId ?? builtinProviderId(settings);
2537
2763
  }
@@ -2561,9 +2787,14 @@ function finalizeConfiguredModel(
2561
2787
  provider: ResolvedModelProvider,
2562
2788
  input: Omit<ConfiguredModel, "schemaVersion" | "definitionVersion" | "executionLimits">,
2563
2789
  ): ConfiguredModel {
2790
+ const requestPolicy =
2791
+ provider.kind === "vercel-gateway-managed" || provider.kind === "vercel-gateway-workspace"
2792
+ ? gatewayRequestPolicyForUpstreamModel(input.upstreamModelId)
2793
+ : undefined;
2564
2794
  const modelWithoutVersion: Omit<ConfiguredModel, "definitionVersion"> = {
2565
2795
  schemaVersion: 1,
2566
2796
  ...input,
2797
+ ...(requestPolicy ? { requestPolicy } : {}),
2567
2798
  executionLimits: resolvedExecutionLimits(settings, input),
2568
2799
  };
2569
2800
  return {
@@ -2635,7 +2866,7 @@ export function configuredModels(settings: Settings): ConfiguredModel[] {
2635
2866
  // a codex/ id has NO codex provider injected (no active subscription) it then
2636
2867
  // resolves to nothing and getModel fails loud with
2637
2868
  // CodexSubscriptionUnavailableError instead of mis-routing to Azure.
2638
- const parsedRegistry = parseModelProvidersJson(settings.modelProvidersJson);
2869
+ const parsedRegistry = configuredRegistryProviders(settings);
2639
2870
  const registryOwnedIds = new Set(
2640
2871
  parsedRegistry.flatMap((provider) => provider.models.map((model) => model.id)),
2641
2872
  );
@@ -2661,6 +2892,9 @@ export function configuredModels(settings: Settings): ConfiguredModel[] {
2661
2892
  reasoningEffort: true,
2662
2893
  hostedWebSearch: settings.webSearchEnabled,
2663
2894
  }),
2895
+ ...(builtinPromptCachingForModel(id)
2896
+ ? { promptCaching: builtinPromptCachingForModel(id)! }
2897
+ : {}),
2664
2898
  latencyModes: builtinLatencyModesForModel(id),
2665
2899
  };
2666
2900
  return finalizeConfiguredModel(settings, builtinProvider, {
@@ -2794,9 +3028,13 @@ export type ResolveTurnExecutionPolicyV1Input = {
2794
3028
  };
2795
3029
 
2796
3030
  function settingsForTurnExecutionPolicy(settings: Settings, modelId: string): Settings {
2797
- return settings.codexSubscriptionEnabled && modelId.startsWith(CODEX_MODEL_ID_PREFIX)
2798
- ? withCodexCatalogProvider(settings)
2799
- : settings;
3031
+ if (settings.codexSubscriptionEnabled && modelId.startsWith(CODEX_MODEL_ID_PREFIX)) {
3032
+ return withCodexCatalogProvider(settings);
3033
+ }
3034
+ if (modelId.startsWith(WORKSPACE_GATEWAY_MODEL_ID_PREFIX)) {
3035
+ return withWorkspaceGatewayCatalogProvider(settings);
3036
+ }
3037
+ return settings;
2800
3038
  }
2801
3039
 
2802
3040
  /**
@@ -2914,7 +3152,7 @@ export function configuredModelPricingSchedules(
2914
3152
  ]),
2915
3153
  );
2916
3154
  const registry: Record<string, ModelPricingScheduleV1> = {};
2917
- for (const provider of parseModelProvidersJson(settings.modelProvidersJson)) {
3155
+ for (const provider of configuredRegistryProviders(settings)) {
2918
3156
  for (const model of provider.models) {
2919
3157
  if (model.pricing) {
2920
3158
  registry[model.id] = normalizeModelPricingSchedule(model.pricing);
@@ -4288,6 +4526,14 @@ function validateSettings(settings: Settings): void {
4288
4526
  const builtinId = builtinProviderId(settings);
4289
4527
  const providerIds = new Set<string>();
4290
4528
  for (const provider of registryProviders) {
4529
+ if (
4530
+ provider.kind === "vercel-gateway-managed" ||
4531
+ provider.kind === "vercel-gateway-workspace"
4532
+ ) {
4533
+ throw new Error(
4534
+ `OPENGENI_MODEL_PROVIDERS_JSON provider kind ${provider.kind} is reserved for the reviewed AI Gateway broker`,
4535
+ );
4536
+ }
4291
4537
  if (provider.id === builtinId) {
4292
4538
  throw new Error(
4293
4539
  `OPENGENI_MODEL_PROVIDERS_JSON provider id ${provider.id} collides with the built-in provider id`,
@@ -4299,7 +4545,7 @@ function validateSettings(settings: Settings): void {
4299
4545
  );
4300
4546
  }
4301
4547
  providerIds.add(provider.id);
4302
- if (!resolveProviderApiKey(provider)) {
4548
+ if (provider.kind !== "codex-subscription" && !resolveProviderApiKey(provider)) {
4303
4549
  throw new Error(
4304
4550
  `OPENGENI_MODEL_PROVIDERS_JSON provider ${provider.id} requires a resolvable API key (set apiKey or apiKeyEnv)`,
4305
4551
  );