@opengeni/config 0.10.0 → 0.10.3

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.3",
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": {
@@ -33,8 +33,8 @@
33
33
  "prepublishOnly": "bash ../../scripts/prepublish-guard"
34
34
  },
35
35
  "dependencies": {
36
- "@opengeni/codex": "^0.2.9",
37
- "@opengeni/contracts": "^0.31.0",
36
+ "@opengeni/codex": "^0.2.10",
37
+ "@opengeni/contracts": "^0.32.0",
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
@@ -428,7 +432,7 @@ const SettingsSchema = z.object({
428
432
  // match the UI danger flip (UsageBar danger at pct >= 90). OPENGENI_CODEX_ROTATION_NEAR_EXHAUSTION_PCT.
429
433
  codexRotationNearExhaustionPct: z.coerce.number().int().min(1).max(100).default(90),
430
434
  openaiReasoningEffort: ReasoningEffort.default("low"),
431
- openaiAllowedReasoningEfforts: z.string().default("low,medium,high,xhigh"),
435
+ openaiAllowedReasoningEfforts: z.string().default("low,medium,high,xhigh,max"),
432
436
  openaiResponsesTransport: z.enum(["http", "websocket"]).default("http"),
433
437
  // Provider-assigned item ids (rs_/msg_/fc_…) in Responses API input are
434
438
  // resolved against the provider's server-side response store. That store is
@@ -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, ...string[]];
1419
+ caching: "auto" | "none";
1420
+ };
1421
+ };
1404
1422
  pricing?: ModelPricingScheduleV1 | undefined;
1405
1423
  definitionVersion: string;
1406
1424
  contextWindowTokens?: number | undefined;
@@ -1411,6 +1429,79 @@ 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 VERCEL_AI_GATEWAY_AI_SDK_BASE_URL = "https://ai-gateway.vercel.sh/v4/ai" as const;
1434
+ export const OPENGENI_GATEWAY_PROVIDER_ID = "opengeni-gateway" as const;
1435
+ export const WORKSPACE_GATEWAY_PROVIDER_ID = "workspace-gateway" as const;
1436
+ export const WORKSPACE_GATEWAY_MODEL_ID_PREFIX = "workspace-gateway/" as const;
1437
+ export const VERCEL_AI_GATEWAY_CONNECTION_DOMAIN = "ai-gateway.vercel.sh" as const;
1438
+ export const VERCEL_AI_GATEWAY_CONNECTION_ROLE = "vercel_ai_gateway" as const;
1439
+
1440
+ export const CODEX_REALTIME_MODEL_ID = "gpt-live-1-boulder-alpha" as const;
1441
+ export const OPENGENI_REALTIME_MODEL_ID_PREFIX = "opengeni-gateway/" as const;
1442
+ export const WORKSPACE_REALTIME_MODEL_ID_PREFIX = "workspace-gateway/" as const;
1443
+
1444
+ /** Curated voice models exposed through AI Gateway's normalized realtime API. */
1445
+ export const AI_GATEWAY_REALTIME_MODELS = {
1446
+ openaiRealtime21: {
1447
+ upstreamModelId: "openai/gpt-realtime-2.1",
1448
+ managedModelId: `${OPENGENI_REALTIME_MODEL_ID_PREFIX}openai/gpt-realtime-2.1`,
1449
+ workspaceModelId: `${WORKSPACE_REALTIME_MODEL_ID_PREFIX}openai/gpt-realtime-2.1`,
1450
+ label: "GPT Realtime 2.1",
1451
+ description: "Best overall voice intelligence",
1452
+ },
1453
+ openaiRealtimeMini: {
1454
+ upstreamModelId: "openai/gpt-realtime-mini",
1455
+ managedModelId: `${OPENGENI_REALTIME_MODEL_ID_PREFIX}openai/gpt-realtime-mini`,
1456
+ workspaceModelId: `${WORKSPACE_REALTIME_MODEL_ID_PREFIX}openai/gpt-realtime-mini`,
1457
+ label: "GPT Realtime Mini",
1458
+ description: "Faster, lighter live voice",
1459
+ },
1460
+ grokVoiceThinkFast20: {
1461
+ upstreamModelId: "xai/grok-voice-think-fast-2.0",
1462
+ managedModelId: `${OPENGENI_REALTIME_MODEL_ID_PREFIX}xai/grok-voice-think-fast-2.0`,
1463
+ workspaceModelId: `${WORKSPACE_REALTIME_MODEL_ID_PREFIX}xai/grok-voice-think-fast-2.0`,
1464
+ label: "Grok Voice Think Fast 2.0",
1465
+ description: "Fast, natural xAI voice",
1466
+ },
1467
+ } as const;
1468
+
1469
+ export type AiGatewayRealtimeModel =
1470
+ (typeof AI_GATEWAY_REALTIME_MODELS)[keyof typeof AI_GATEWAY_REALTIME_MODELS];
1471
+
1472
+ export function resolveAiGatewayRealtimeModel(
1473
+ modelId: string,
1474
+ ): { source: "managed" | "workspace"; upstreamModelId: string } | null {
1475
+ for (const model of Object.values(AI_GATEWAY_REALTIME_MODELS)) {
1476
+ if (model.managedModelId === modelId) {
1477
+ return { source: "managed", upstreamModelId: model.upstreamModelId };
1478
+ }
1479
+ if (model.workspaceModelId === modelId) {
1480
+ return { source: "workspace", upstreamModelId: model.upstreamModelId };
1481
+ }
1482
+ }
1483
+ return null;
1484
+ }
1485
+
1486
+ export const OPENGENI_GATEWAY_MODELS = {
1487
+ deepseek: {
1488
+ productId: "deepseek-v4-flash-0731",
1489
+ workspaceProductId: `${WORKSPACE_GATEWAY_MODEL_ID_PREFIX}deepseek-v4-flash-0731`,
1490
+ upstreamModelId: "deepseek/deepseek-v4-flash-0731",
1491
+ label: "DeepSeek V4 Flash 0731",
1492
+ providers: ["baseten", "novita", "deepinfra"],
1493
+ implicitCaching: true,
1494
+ },
1495
+ kimi: {
1496
+ productId: "kimi-k3",
1497
+ workspaceProductId: `${WORKSPACE_GATEWAY_MODEL_ID_PREFIX}kimi-k3`,
1498
+ upstreamModelId: "moonshotai/kimi-k3",
1499
+ label: "Kimi K3",
1500
+ providers: ["baseten", "fireworks"],
1501
+ implicitCaching: true,
1502
+ },
1503
+ } as const;
1504
+
1414
1505
  /**
1415
1506
  * Built-in OpenGeni credit pricing schedules.
1416
1507
  *
@@ -1485,6 +1576,27 @@ export const defaultModelPricing: Record<string, ModelPricingScheduleV1> = {
1485
1576
  },
1486
1577
  ],
1487
1578
  },
1579
+ // Conservative Vercel AI Gateway fallback prices. Normal managed Gateway
1580
+ // billing uses the exact response Gateway `cost` / `inferenceCost` and applies
1581
+ // the same margin. These token rates are used only if that
1582
+ // metadata is absent. DeepSeek therefore carries the highest approved route
1583
+ // (Novita); both approved Kimi routes have the same list price.
1584
+ [OPENGENI_GATEWAY_MODELS.deepseek.productId]: {
1585
+ default: {
1586
+ inputMicrosPerMillionTokens: 140_000,
1587
+ cachedInputMicrosPerMillionTokens: 28_000,
1588
+ outputMicrosPerMillionTokens: 280_000,
1589
+ marginBps: 2_500,
1590
+ },
1591
+ },
1592
+ [OPENGENI_GATEWAY_MODELS.kimi.productId]: {
1593
+ default: {
1594
+ inputMicrosPerMillionTokens: 3_000_000,
1595
+ cachedInputMicrosPerMillionTokens: 300_000,
1596
+ outputMicrosPerMillionTokens: 15_000_000,
1597
+ marginBps: 2_500,
1598
+ },
1599
+ },
1488
1600
  // Fireworks AI / GLM 5.2 — the first shipped non-OpenAI registry model. A
1489
1601
  // built-in default pricing entry makes managed billing work out of the box
1490
1602
  // for hosts that expose this model via OPENGENI_MODEL_PROVIDERS_JSON without
@@ -1656,6 +1768,7 @@ export function getSettings(): Settings {
1656
1768
  openaiBaseUrl: optional("OPENGENI_OPENAI_BASE_URL") ?? optional("OPENAI_BASE_URL"),
1657
1769
  openaiModel: optional("OPENGENI_OPENAI_MODEL"),
1658
1770
  openaiAllowedModels: optional("OPENGENI_OPENAI_ALLOWED_MODELS"),
1771
+ vercelAiGatewayApiKey: optional("OPENGENI_VERCEL_AI_GATEWAY_API_KEY"),
1659
1772
  voiceInputMaxDurationSeconds: optional("OPENGENI_VOICE_INPUT_MAX_DURATION_SECONDS"),
1660
1773
  voiceInputMaxSizeBytes: optional("OPENGENI_VOICE_INPUT_MAX_SIZE_BYTES"),
1661
1774
  voiceInputProviderOrder: optional("OPENGENI_VOICE_INPUT_PROVIDER_ORDER"),
@@ -2202,6 +2315,131 @@ function legacyModelCapabilities(
2202
2315
  });
2203
2316
  }
2204
2317
 
2318
+ export function gatewayRequestPolicyForUpstreamModel(
2319
+ upstreamModelId: string,
2320
+ ): ConfiguredModel["requestPolicy"] {
2321
+ const model = Object.values(OPENGENI_GATEWAY_MODELS).find(
2322
+ (candidate) => candidate.upstreamModelId === upstreamModelId,
2323
+ );
2324
+ if (!model) {
2325
+ return undefined;
2326
+ }
2327
+ return {
2328
+ gateway: {
2329
+ only: [...model.providers] as [string, ...string[]],
2330
+ caching: model.implicitCaching ? "auto" : "none",
2331
+ },
2332
+ };
2333
+ }
2334
+
2335
+ function gatewayModelCapabilities(
2336
+ settings: Settings,
2337
+ input: { implicitCaching: boolean; vision: boolean },
2338
+ ): ModelCapabilitiesV1 {
2339
+ const legacy = legacyModelCapabilities(settings, {
2340
+ reasoningEffort: true,
2341
+ hostedWebSearch: false,
2342
+ });
2343
+ return normalizeCapabilities({
2344
+ ...legacy,
2345
+ functionCalling: { upstream: "supported", runnable: true },
2346
+ inputModalities: input.vision ? ["text", "image"] : ["text"],
2347
+ transports: {
2348
+ ...legacy.transports,
2349
+ sse: { upstream: "supported", runnable: true },
2350
+ },
2351
+ promptCaching: input.implicitCaching
2352
+ ? { upstream: "supported", runnable: true, mode: "implicit" }
2353
+ : { upstream: "unsupported", runnable: false, mode: "none" },
2354
+ // Both Gateway products expose one reviewed route policy and no separately
2355
+ // billed latency mode.
2356
+ latencyModes: [{ id: "standard", upstream: "supported", runnable: true }],
2357
+ });
2358
+ }
2359
+
2360
+ function gatewayRegistryProvider(
2361
+ settings: Settings,
2362
+ input:
2363
+ | { kind: "vercel-gateway-managed"; apiKey: string }
2364
+ | { kind: "vercel-gateway-workspace"; apiKey?: string },
2365
+ ): RegistryProvider {
2366
+ const workspace = input.kind === "vercel-gateway-workspace";
2367
+ const models = [OPENGENI_GATEWAY_MODELS.deepseek, OPENGENI_GATEWAY_MODELS.kimi].map((model) => {
2368
+ const kimi = model === OPENGENI_GATEWAY_MODELS.kimi;
2369
+ return {
2370
+ id: workspace ? model.workspaceProductId : model.productId,
2371
+ upstreamModelId: model.upstreamModelId,
2372
+ label: model.label,
2373
+ capabilities: gatewayModelCapabilities(settings, {
2374
+ implicitCaching: model.implicitCaching,
2375
+ vision: kimi,
2376
+ }),
2377
+ contextWindowTokens: 1_000_000,
2378
+ effectiveContextWindowTokens: 900_000,
2379
+ autoCompactTokenLimit: 850_000,
2380
+ toolOutputTruncationTokens: settings.modelToolOutputTruncationTokens,
2381
+ };
2382
+ });
2383
+ return {
2384
+ kind: input.kind,
2385
+ id: workspace ? WORKSPACE_GATEWAY_PROVIDER_ID : OPENGENI_GATEWAY_PROVIDER_ID,
2386
+ label: workspace ? "Your Gateway" : "OpenGeni",
2387
+ // Responses preserves vision, reasoning items, and provider-native usage.
2388
+ // Model-specific compatibility stays at the reviewed request fence rather
2389
+ // than downgrading the whole provider wire.
2390
+ api: "responses",
2391
+ baseUrl: VERCEL_AI_GATEWAY_BASE_URL,
2392
+ ...(input.apiKey ? { apiKey: input.apiKey } : {}),
2393
+ models,
2394
+ };
2395
+ }
2396
+
2397
+ function configuredRegistryProviders(settings: Settings): RegistryProvider[] {
2398
+ const providers = parseModelProvidersJson(settings.modelProvidersJson);
2399
+ if (!settings.vercelAiGatewayApiKey) {
2400
+ return providers;
2401
+ }
2402
+ if (providers.some((provider) => provider.id === OPENGENI_GATEWAY_PROVIDER_ID)) {
2403
+ throw new Error(
2404
+ `${OPENGENI_GATEWAY_PROVIDER_ID} is reserved for OPENGENI_VERCEL_AI_GATEWAY_API_KEY`,
2405
+ );
2406
+ }
2407
+ return [
2408
+ ...providers,
2409
+ gatewayRegistryProvider(settings, {
2410
+ kind: "vercel-gateway-managed",
2411
+ apiKey: settings.vercelAiGatewayApiKey,
2412
+ }),
2413
+ ];
2414
+ }
2415
+
2416
+ /** Static catalog overlay; it contains no concrete workspace credential. */
2417
+ export function withWorkspaceGatewayCatalogProvider(settings: Settings): Settings {
2418
+ const providers = parseModelProvidersJson(settings.modelProvidersJson);
2419
+ if (providers.some((provider) => provider.id === WORKSPACE_GATEWAY_PROVIDER_ID)) {
2420
+ return settings;
2421
+ }
2422
+ return {
2423
+ ...settings,
2424
+ modelProvidersJson: JSON.stringify([
2425
+ ...providers,
2426
+ gatewayRegistryProvider(settings, { kind: "vercel-gateway-workspace" }),
2427
+ ]),
2428
+ };
2429
+ }
2430
+
2431
+ /** Runtime overlay after the worker resolves the workspace's encrypted key. */
2432
+ export function withWorkspaceGatewayCredential(settings: Settings, apiKey: string): Settings {
2433
+ if (!apiKey.trim()) {
2434
+ throw new Error("workspace AI Gateway credential is empty");
2435
+ }
2436
+ const catalogSettings = withWorkspaceGatewayCatalogProvider(settings);
2437
+ const providers = parseModelProvidersJson(catalogSettings.modelProvidersJson).map((provider) =>
2438
+ provider.id === WORKSPACE_GATEWAY_PROVIDER_ID ? { ...provider, apiKey } : provider,
2439
+ );
2440
+ return { ...catalogSettings, modelProvidersJson: JSON.stringify(providers) };
2441
+ }
2442
+
2205
2443
  /** OpenAI GPT-5.6 Fast mode is 2× Standard list rates (service_tier fast/priority). */
2206
2444
  const GPT56_FAST_BILLING_MULTIPLIER_BPS = 20_000;
2207
2445
 
@@ -2256,6 +2494,17 @@ function builtinLatencyModesForModel(modelId: string): Array<{
2256
2494
  return [{ id: "standard", upstream: "unknown", runnable: true }];
2257
2495
  }
2258
2496
 
2497
+ function builtinPromptCachingForModel(
2498
+ modelId: string,
2499
+ ): NonNullable<ModelCapabilitiesV1["promptCaching"]> | undefined {
2500
+ const slug = modelId.startsWith(CODEX_MODEL_ID_PREFIX)
2501
+ ? modelId.slice(CODEX_MODEL_ID_PREFIX.length)
2502
+ : modelId;
2503
+ return slug.startsWith("gpt-5.6-")
2504
+ ? { upstream: "supported", runnable: true, mode: "implicit" }
2505
+ : undefined;
2506
+ }
2507
+
2259
2508
  /**
2260
2509
  * Map OpenGeni latency mode to the provider `service_tier` wire value.
2261
2510
  * Azure and Codex ChatGPT accept `priority`; OpenAI API accepts `fast` (alias of priority).
@@ -2312,15 +2561,23 @@ function assertLatencyModeRunnable(
2312
2561
  }
2313
2562
 
2314
2563
  function registryCredentialSource(provider: RegistryProvider): CredentialSourceV1 {
2315
- return provider.kind === "codex-subscription"
2316
- ? { kind: "connected_subscription", provider: "codex" }
2317
- : { kind: "deployment", mechanism: "api_key" };
2564
+ if (provider.kind === "codex-subscription") {
2565
+ return { kind: "connected_subscription", provider: "codex" };
2566
+ }
2567
+ if (provider.kind === "vercel-gateway-workspace") {
2568
+ return { kind: "workspace_connection", mechanism: "api_key" };
2569
+ }
2570
+ return { kind: "deployment", mechanism: "api_key" };
2318
2571
  }
2319
2572
 
2320
2573
  function registryBilling(provider: RegistryProvider): BillingAttributionV1 {
2321
- return provider.kind === "codex-subscription"
2322
- ? { upstreamPayer: "connected_subscription", metering: "external" }
2323
- : { upstreamPayer: "deployment", metering: "opengeni_credits" };
2574
+ if (provider.kind === "codex-subscription") {
2575
+ return { upstreamPayer: "connected_subscription", metering: "external" };
2576
+ }
2577
+ if (provider.kind === "vercel-gateway-workspace") {
2578
+ return { upstreamPayer: "workspace", metering: "external" };
2579
+ }
2580
+ return { upstreamPayer: "deployment", metering: "opengeni_credits" };
2324
2581
  }
2325
2582
 
2326
2583
  function builtinCredentialSource(settings: Settings): CredentialSourceV1 {
@@ -2403,6 +2660,7 @@ function definitionVersionFor(
2403
2660
  billing: model.billing,
2404
2661
  executionLimits: model.executionLimits,
2405
2662
  capabilities: model.capabilities,
2663
+ ...(model.requestPolicy ? { requestPolicy: model.requestPolicy } : {}),
2406
2664
  pricing: model.pricing ?? null,
2407
2665
  });
2408
2666
  return `sha256:${createHash("sha256")
@@ -2455,7 +2713,7 @@ export function configuredProviders(settings: Settings): ResolvedModelProvider[]
2455
2713
  : undefined;
2456
2714
  builtin.apiKey = settings.openaiApiKey;
2457
2715
  }
2458
- const registry = parseModelProvidersJson(settings.modelProvidersJson).map(
2716
+ const registry = configuredRegistryProviders(settings).map(
2459
2717
  (provider): ResolvedModelProvider => ({
2460
2718
  id: provider.id,
2461
2719
  label: provider.label ?? provider.id,
@@ -2498,6 +2756,11 @@ export function withCodexCatalogProvider(settings: Settings): Settings {
2498
2756
  reasoningEffort: true,
2499
2757
  hostedWebSearch: true,
2500
2758
  }),
2759
+ ...(builtinPromptCachingForModel(`${CODEX_MODEL_ID_PREFIX}${slug}`)
2760
+ ? {
2761
+ promptCaching: builtinPromptCachingForModel(`${CODEX_MODEL_ID_PREFIX}${slug}`)!,
2762
+ }
2763
+ : {}),
2501
2764
  latencyModes: builtinLatencyModesForModel(`${CODEX_MODEL_ID_PREFIX}${slug}`),
2502
2765
  };
2503
2766
  return {
@@ -2542,6 +2805,9 @@ export function policyProviderIdForModel(settings: Settings, modelId: string): s
2542
2805
  if (canonicalModelId.startsWith(CODEX_MODEL_ID_PREFIX)) {
2543
2806
  return CODEX_PROVIDER_ID;
2544
2807
  }
2808
+ if (canonicalModelId.startsWith(WORKSPACE_GATEWAY_MODEL_ID_PREFIX)) {
2809
+ return WORKSPACE_GATEWAY_PROVIDER_ID;
2810
+ }
2545
2811
  const configured = configuredModels(settings).find((model) => model.id === canonicalModelId);
2546
2812
  return configured?.providerId ?? builtinProviderId(settings);
2547
2813
  }
@@ -2571,9 +2837,14 @@ function finalizeConfiguredModel(
2571
2837
  provider: ResolvedModelProvider,
2572
2838
  input: Omit<ConfiguredModel, "schemaVersion" | "definitionVersion" | "executionLimits">,
2573
2839
  ): ConfiguredModel {
2840
+ const requestPolicy =
2841
+ provider.kind === "vercel-gateway-managed" || provider.kind === "vercel-gateway-workspace"
2842
+ ? gatewayRequestPolicyForUpstreamModel(input.upstreamModelId)
2843
+ : undefined;
2574
2844
  const modelWithoutVersion: Omit<ConfiguredModel, "definitionVersion"> = {
2575
2845
  schemaVersion: 1,
2576
2846
  ...input,
2847
+ ...(requestPolicy ? { requestPolicy } : {}),
2577
2848
  executionLimits: resolvedExecutionLimits(settings, input),
2578
2849
  };
2579
2850
  return {
@@ -2645,7 +2916,7 @@ export function configuredModels(settings: Settings): ConfiguredModel[] {
2645
2916
  // a codex/ id has NO codex provider injected (no active subscription) it then
2646
2917
  // resolves to nothing and getModel fails loud with
2647
2918
  // CodexSubscriptionUnavailableError instead of mis-routing to Azure.
2648
- const parsedRegistry = parseModelProvidersJson(settings.modelProvidersJson);
2919
+ const parsedRegistry = configuredRegistryProviders(settings);
2649
2920
  const registryOwnedIds = new Set(
2650
2921
  parsedRegistry.flatMap((provider) => provider.models.map((model) => model.id)),
2651
2922
  );
@@ -2671,6 +2942,9 @@ export function configuredModels(settings: Settings): ConfiguredModel[] {
2671
2942
  reasoningEffort: true,
2672
2943
  hostedWebSearch: settings.webSearchEnabled,
2673
2944
  }),
2945
+ ...(builtinPromptCachingForModel(id)
2946
+ ? { promptCaching: builtinPromptCachingForModel(id)! }
2947
+ : {}),
2674
2948
  latencyModes: builtinLatencyModesForModel(id),
2675
2949
  };
2676
2950
  return finalizeConfiguredModel(settings, builtinProvider, {
@@ -2804,9 +3078,13 @@ export type ResolveTurnExecutionPolicyV1Input = {
2804
3078
  };
2805
3079
 
2806
3080
  function settingsForTurnExecutionPolicy(settings: Settings, modelId: string): Settings {
2807
- return settings.codexSubscriptionEnabled && modelId.startsWith(CODEX_MODEL_ID_PREFIX)
2808
- ? withCodexCatalogProvider(settings)
2809
- : settings;
3081
+ if (settings.codexSubscriptionEnabled && modelId.startsWith(CODEX_MODEL_ID_PREFIX)) {
3082
+ return withCodexCatalogProvider(settings);
3083
+ }
3084
+ if (modelId.startsWith(WORKSPACE_GATEWAY_MODEL_ID_PREFIX)) {
3085
+ return withWorkspaceGatewayCatalogProvider(settings);
3086
+ }
3087
+ return settings;
2810
3088
  }
2811
3089
 
2812
3090
  /**
@@ -2924,7 +3202,7 @@ export function configuredModelPricingSchedules(
2924
3202
  ]),
2925
3203
  );
2926
3204
  const registry: Record<string, ModelPricingScheduleV1> = {};
2927
- for (const provider of parseModelProvidersJson(settings.modelProvidersJson)) {
3205
+ for (const provider of configuredRegistryProviders(settings)) {
2928
3206
  for (const model of provider.models) {
2929
3207
  if (model.pricing) {
2930
3208
  registry[model.id] = normalizeModelPricingSchedule(model.pricing);
@@ -3088,6 +3366,39 @@ export function calculateModelUsageCostMicros(
3088
3366
  return total;
3089
3367
  }
3090
3368
 
3369
+ /**
3370
+ * Convert AI Gateway's exact USD inference cost to OpenGeni credit micros and
3371
+ * apply the configured model margin. Decimal arithmetic is integer-only so a
3372
+ * sub-micro provider charge cannot be lost to floating-point rounding.
3373
+ */
3374
+ export function calculateGatewayReportedCostMicros(
3375
+ settings: Settings,
3376
+ model: string,
3377
+ inferenceCostUsd: string,
3378
+ options?: { inputTokens?: number },
3379
+ ): number {
3380
+ const schedule = configuredModelPricingSchedules(settings)[model];
3381
+ if (!schedule) {
3382
+ throw new Error(`Missing model pricing for ${model}`);
3383
+ }
3384
+ const pricing = selectModelPricing(schedule, positiveInt(options?.inputTokens));
3385
+ const match = /^(0|[1-9]\d*)(?:\.(\d{1,18}))?$/.exec(inferenceCostUsd);
3386
+ if (!match) {
3387
+ throw new Error("Invalid AI Gateway inference cost");
3388
+ }
3389
+ const fraction = match[2] ?? "";
3390
+ const decimalDigits = BigInt(`${match[1]}${fraction}`);
3391
+ const decimalScale = 10n ** BigInt(fraction.length);
3392
+ const marginBps = BigInt(10_000 + (pricing.marginBps ?? 0));
3393
+ const numerator = decimalDigits * 1_000_000n * marginBps;
3394
+ const denominator = decimalScale * 10_000n;
3395
+ const micros = (numerator + denominator - 1n) / denominator;
3396
+ if (micros > BigInt(Number.MAX_SAFE_INTEGER)) {
3397
+ throw new Error("AI Gateway inference cost exceeds the supported billing range");
3398
+ }
3399
+ return Number(micros);
3400
+ }
3401
+
3091
3402
  export function configuredAllowedReasoningEfforts(
3092
3403
  settings: Settings,
3093
3404
  ): Array<z.infer<typeof ReasoningEffort>> {
@@ -4298,6 +4609,14 @@ function validateSettings(settings: Settings): void {
4298
4609
  const builtinId = builtinProviderId(settings);
4299
4610
  const providerIds = new Set<string>();
4300
4611
  for (const provider of registryProviders) {
4612
+ if (
4613
+ provider.kind === "vercel-gateway-managed" ||
4614
+ provider.kind === "vercel-gateway-workspace"
4615
+ ) {
4616
+ throw new Error(
4617
+ `OPENGENI_MODEL_PROVIDERS_JSON provider kind ${provider.kind} is reserved for the reviewed AI Gateway broker`,
4618
+ );
4619
+ }
4301
4620
  if (provider.id === builtinId) {
4302
4621
  throw new Error(
4303
4622
  `OPENGENI_MODEL_PROVIDERS_JSON provider id ${provider.id} collides with the built-in provider id`,
@@ -4309,7 +4628,7 @@ function validateSettings(settings: Settings): void {
4309
4628
  );
4310
4629
  }
4311
4630
  providerIds.add(provider.id);
4312
- if (!resolveProviderApiKey(provider)) {
4631
+ if (provider.kind !== "codex-subscription" && !resolveProviderApiKey(provider)) {
4313
4632
  throw new Error(
4314
4633
  `OPENGENI_MODEL_PROVIDERS_JSON provider ${provider.id} requires a resolvable API key (set apiKey or apiKeyEnv)`,
4315
4634
  );