@opengeni/config 0.10.0 → 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.10.0",
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.31.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
@@ -1178,6 +1182,9 @@ export const ModelCapabilitiesV1Schema = z
1178
1182
  responsesWebSocket: CapabilityStateV1Schema,
1179
1183
  realtimeAudio: CapabilityStateV1Schema,
1180
1184
  }),
1185
+ promptCaching: CapabilityStateV1Schema.extend({
1186
+ mode: z.enum(["implicit", "automatic", "none"]),
1187
+ }).optional(),
1181
1188
  latencyModes: z
1182
1189
  .array(
1183
1190
  z.object({
@@ -1282,7 +1289,12 @@ export type ModelProviderApi = z.infer<typeof ModelProviderApi>;
1282
1289
  * "codex-subscription" providers authenticate per-request with a ChatGPT/Codex
1283
1290
  * subscription token resolved at call time (no static key) — see @opengeni/codex.
1284
1291
  */
1285
- 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
+ ]);
1286
1298
  export type RegistryProviderKind = z.infer<typeof RegistryProviderKind>;
1287
1299
 
1288
1300
  /** A single model exposed by a registry provider. */
@@ -1335,7 +1347,7 @@ const RegistryModelSchema = z
1335
1347
 
1336
1348
  /** A non-built-in provider declared by the host via OPENGENI_MODEL_PROVIDERS_JSON. */
1337
1349
  const RegistryProviderSchema = z.object({
1338
- kind: RegistryProviderKind.default("api-key"), // "codex-subscription" => per-request token, no static key
1350
+ kind: RegistryProviderKind.default("api-key"),
1339
1351
  id: z.string().min(1).regex(registryId), // stable provider id, e.g. "fireworks"
1340
1352
  label: z.string().min(1).optional(),
1341
1353
  api: ModelProviderApi.default("chat"),
@@ -1401,6 +1413,12 @@ export interface ConfiguredModel {
1401
1413
  credentialSource: CredentialSourceV1;
1402
1414
  billing: BillingAttributionV1;
1403
1415
  capabilities: ModelCapabilitiesV1;
1416
+ requestPolicy?: {
1417
+ gateway: {
1418
+ only: [string];
1419
+ caching: "auto" | "none";
1420
+ };
1421
+ };
1404
1422
  pricing?: ModelPricingScheduleV1 | undefined;
1405
1423
  definitionVersion: string;
1406
1424
  contextWindowTokens?: number | undefined;
@@ -1411,6 +1429,32 @@ export interface ConfiguredModel {
1411
1429
  hostedWebSearch: boolean;
1412
1430
  }
1413
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
+
1414
1458
  /**
1415
1459
  * Built-in OpenGeni credit pricing schedules.
1416
1460
  *
@@ -1485,6 +1529,27 @@ export const defaultModelPricing: Record<string, ModelPricingScheduleV1> = {
1485
1529
  },
1486
1530
  ],
1487
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
+ },
1488
1553
  // Fireworks AI / GLM 5.2 — the first shipped non-OpenAI registry model. A
1489
1554
  // built-in default pricing entry makes managed billing work out of the box
1490
1555
  // for hosts that expose this model via OPENGENI_MODEL_PROVIDERS_JSON without
@@ -1656,6 +1721,7 @@ export function getSettings(): Settings {
1656
1721
  openaiBaseUrl: optional("OPENGENI_OPENAI_BASE_URL") ?? optional("OPENAI_BASE_URL"),
1657
1722
  openaiModel: optional("OPENGENI_OPENAI_MODEL"),
1658
1723
  openaiAllowedModels: optional("OPENGENI_OPENAI_ALLOWED_MODELS"),
1724
+ vercelAiGatewayApiKey: optional("OPENGENI_VERCEL_AI_GATEWAY_API_KEY"),
1659
1725
  voiceInputMaxDurationSeconds: optional("OPENGENI_VOICE_INPUT_MAX_DURATION_SECONDS"),
1660
1726
  voiceInputMaxSizeBytes: optional("OPENGENI_VOICE_INPUT_MAX_SIZE_BYTES"),
1661
1727
  voiceInputProviderOrder: optional("OPENGENI_VOICE_INPUT_PROVIDER_ORDER"),
@@ -2202,6 +2268,128 @@ function legacyModelCapabilities(
2202
2268
  });
2203
2269
  }
2204
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
+
2205
2393
  /** OpenAI GPT-5.6 Fast mode is 2× Standard list rates (service_tier fast/priority). */
2206
2394
  const GPT56_FAST_BILLING_MULTIPLIER_BPS = 20_000;
2207
2395
 
@@ -2256,6 +2444,17 @@ function builtinLatencyModesForModel(modelId: string): Array<{
2256
2444
  return [{ id: "standard", upstream: "unknown", runnable: true }];
2257
2445
  }
2258
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
+
2259
2458
  /**
2260
2459
  * Map OpenGeni latency mode to the provider `service_tier` wire value.
2261
2460
  * Azure and Codex ChatGPT accept `priority`; OpenAI API accepts `fast` (alias of priority).
@@ -2312,15 +2511,23 @@ function assertLatencyModeRunnable(
2312
2511
  }
2313
2512
 
2314
2513
  function registryCredentialSource(provider: RegistryProvider): CredentialSourceV1 {
2315
- return provider.kind === "codex-subscription"
2316
- ? { kind: "connected_subscription", provider: "codex" }
2317
- : { 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" };
2318
2521
  }
2319
2522
 
2320
2523
  function registryBilling(provider: RegistryProvider): BillingAttributionV1 {
2321
- return provider.kind === "codex-subscription"
2322
- ? { upstreamPayer: "connected_subscription", metering: "external" }
2323
- : { 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" };
2324
2531
  }
2325
2532
 
2326
2533
  function builtinCredentialSource(settings: Settings): CredentialSourceV1 {
@@ -2403,6 +2610,7 @@ function definitionVersionFor(
2403
2610
  billing: model.billing,
2404
2611
  executionLimits: model.executionLimits,
2405
2612
  capabilities: model.capabilities,
2613
+ ...(model.requestPolicy ? { requestPolicy: model.requestPolicy } : {}),
2406
2614
  pricing: model.pricing ?? null,
2407
2615
  });
2408
2616
  return `sha256:${createHash("sha256")
@@ -2455,7 +2663,7 @@ export function configuredProviders(settings: Settings): ResolvedModelProvider[]
2455
2663
  : undefined;
2456
2664
  builtin.apiKey = settings.openaiApiKey;
2457
2665
  }
2458
- const registry = parseModelProvidersJson(settings.modelProvidersJson).map(
2666
+ const registry = configuredRegistryProviders(settings).map(
2459
2667
  (provider): ResolvedModelProvider => ({
2460
2668
  id: provider.id,
2461
2669
  label: provider.label ?? provider.id,
@@ -2498,6 +2706,11 @@ export function withCodexCatalogProvider(settings: Settings): Settings {
2498
2706
  reasoningEffort: true,
2499
2707
  hostedWebSearch: true,
2500
2708
  }),
2709
+ ...(builtinPromptCachingForModel(`${CODEX_MODEL_ID_PREFIX}${slug}`)
2710
+ ? {
2711
+ promptCaching: builtinPromptCachingForModel(`${CODEX_MODEL_ID_PREFIX}${slug}`)!,
2712
+ }
2713
+ : {}),
2501
2714
  latencyModes: builtinLatencyModesForModel(`${CODEX_MODEL_ID_PREFIX}${slug}`),
2502
2715
  };
2503
2716
  return {
@@ -2542,6 +2755,9 @@ export function policyProviderIdForModel(settings: Settings, modelId: string): s
2542
2755
  if (canonicalModelId.startsWith(CODEX_MODEL_ID_PREFIX)) {
2543
2756
  return CODEX_PROVIDER_ID;
2544
2757
  }
2758
+ if (canonicalModelId.startsWith(WORKSPACE_GATEWAY_MODEL_ID_PREFIX)) {
2759
+ return WORKSPACE_GATEWAY_PROVIDER_ID;
2760
+ }
2545
2761
  const configured = configuredModels(settings).find((model) => model.id === canonicalModelId);
2546
2762
  return configured?.providerId ?? builtinProviderId(settings);
2547
2763
  }
@@ -2571,9 +2787,14 @@ function finalizeConfiguredModel(
2571
2787
  provider: ResolvedModelProvider,
2572
2788
  input: Omit<ConfiguredModel, "schemaVersion" | "definitionVersion" | "executionLimits">,
2573
2789
  ): ConfiguredModel {
2790
+ const requestPolicy =
2791
+ provider.kind === "vercel-gateway-managed" || provider.kind === "vercel-gateway-workspace"
2792
+ ? gatewayRequestPolicyForUpstreamModel(input.upstreamModelId)
2793
+ : undefined;
2574
2794
  const modelWithoutVersion: Omit<ConfiguredModel, "definitionVersion"> = {
2575
2795
  schemaVersion: 1,
2576
2796
  ...input,
2797
+ ...(requestPolicy ? { requestPolicy } : {}),
2577
2798
  executionLimits: resolvedExecutionLimits(settings, input),
2578
2799
  };
2579
2800
  return {
@@ -2645,7 +2866,7 @@ export function configuredModels(settings: Settings): ConfiguredModel[] {
2645
2866
  // a codex/ id has NO codex provider injected (no active subscription) it then
2646
2867
  // resolves to nothing and getModel fails loud with
2647
2868
  // CodexSubscriptionUnavailableError instead of mis-routing to Azure.
2648
- const parsedRegistry = parseModelProvidersJson(settings.modelProvidersJson);
2869
+ const parsedRegistry = configuredRegistryProviders(settings);
2649
2870
  const registryOwnedIds = new Set(
2650
2871
  parsedRegistry.flatMap((provider) => provider.models.map((model) => model.id)),
2651
2872
  );
@@ -2671,6 +2892,9 @@ export function configuredModels(settings: Settings): ConfiguredModel[] {
2671
2892
  reasoningEffort: true,
2672
2893
  hostedWebSearch: settings.webSearchEnabled,
2673
2894
  }),
2895
+ ...(builtinPromptCachingForModel(id)
2896
+ ? { promptCaching: builtinPromptCachingForModel(id)! }
2897
+ : {}),
2674
2898
  latencyModes: builtinLatencyModesForModel(id),
2675
2899
  };
2676
2900
  return finalizeConfiguredModel(settings, builtinProvider, {
@@ -2804,9 +3028,13 @@ export type ResolveTurnExecutionPolicyV1Input = {
2804
3028
  };
2805
3029
 
2806
3030
  function settingsForTurnExecutionPolicy(settings: Settings, modelId: string): Settings {
2807
- return settings.codexSubscriptionEnabled && modelId.startsWith(CODEX_MODEL_ID_PREFIX)
2808
- ? withCodexCatalogProvider(settings)
2809
- : 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;
2810
3038
  }
2811
3039
 
2812
3040
  /**
@@ -2924,7 +3152,7 @@ export function configuredModelPricingSchedules(
2924
3152
  ]),
2925
3153
  );
2926
3154
  const registry: Record<string, ModelPricingScheduleV1> = {};
2927
- for (const provider of parseModelProvidersJson(settings.modelProvidersJson)) {
3155
+ for (const provider of configuredRegistryProviders(settings)) {
2928
3156
  for (const model of provider.models) {
2929
3157
  if (model.pricing) {
2930
3158
  registry[model.id] = normalizeModelPricingSchedule(model.pricing);
@@ -4298,6 +4526,14 @@ function validateSettings(settings: Settings): void {
4298
4526
  const builtinId = builtinProviderId(settings);
4299
4527
  const providerIds = new Set<string>();
4300
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
+ }
4301
4537
  if (provider.id === builtinId) {
4302
4538
  throw new Error(
4303
4539
  `OPENGENI_MODEL_PROVIDERS_JSON provider id ${provider.id} collides with the built-in provider id`,
@@ -4309,7 +4545,7 @@ function validateSettings(settings: Settings): void {
4309
4545
  );
4310
4546
  }
4311
4547
  providerIds.add(provider.id);
4312
- if (!resolveProviderApiKey(provider)) {
4548
+ if (provider.kind !== "codex-subscription" && !resolveProviderApiKey(provider)) {
4313
4549
  throw new Error(
4314
4550
  `OPENGENI_MODEL_PROVIDERS_JSON provider ${provider.id} requires a resolvable API key (set apiKey or apiKeyEnv)`,
4315
4551
  );