@opengeni/config 0.22.5 → 0.23.2-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
@@ -50,6 +50,11 @@ import { z } from "zod";
50
50
 
51
51
  const envName = /^[A-Za-z_][A-Za-z0-9_]*$/;
52
52
  const registryId = /^[A-Za-z0-9_-]+$/;
53
+ export const DEFAULT_OPENROUTER_MODEL_ID =
54
+ "openrouter/nvidia/nemotron-3-super-120b-a12b:free" as const;
55
+ export const DEFAULT_MODEL_COST_POLICY_JSON = JSON.stringify({
56
+ [DEFAULT_OPENROUTER_MODEL_ID]: "free",
57
+ });
53
58
 
54
59
  // Archive capture claims are also the admission/teardown fence around a
55
60
  // provider snapshot. Keep a real settlement window after the provider request;
@@ -238,10 +243,18 @@ export const McpServerConnectionRefSchema = z
238
243
  }
239
244
  })
240
245
  .optional(),
246
+ authoritySource: z.literal("host").optional(),
241
247
  subjectScope: z.enum(["workspace", "subject"]).optional(),
242
248
  })
243
249
  .strict()
244
250
  .superRefine((reference, context) => {
251
+ if (reference.authoritySource === "host" && !reference.connectionId) {
252
+ context.addIssue({
253
+ code: "custom",
254
+ message: "host authority requires connectionId",
255
+ path: ["connectionId"],
256
+ });
257
+ }
245
258
  if (!reference.selectedResources) return;
246
259
  if (!reference.connectionId) {
247
260
  context.addIssue({
@@ -502,6 +515,13 @@ const SettingsSchema = z.object({
502
515
  // into @opengeni/db once at boot.
503
516
  // Env: OPENGENI_CHILD_LIFECYCLE_NOTICES_ENABLED.
504
517
  childLifecycleNoticesEnabled: EnvBoolean.default(false),
518
+ // Explicit host-owned MCP connection authority is a rolling protocol
519
+ // activation. Keep it off while any API, worker, or browser bundle predates
520
+ // the authority discriminator; enable it only after the whole fleet runs an
521
+ // image that understands host refs. Legacy markerless non-UUID refs remain a
522
+ // separate compatibility lane for already-persisted embedding integrations.
523
+ // Env: OPENGENI_HOST_MCP_AUTHORITY_SOURCE_ADMISSION_ENABLED.
524
+ hostMcpAuthoritySourceAdmissionEnabled: EnvBoolean.default(false),
505
525
  // Per-channel and per-DM Slack workspace routing. Default ON. A channel does
506
526
  // not count a personal workspace as a candidate, so an organization with one
507
527
  // shared workspace resolves it as the sole candidate and never asks; the
@@ -684,6 +704,24 @@ const SettingsSchema = z.object({
684
704
  // keep subscription model routing while disabling Codex voice input.
685
705
  voiceInputCodexExperimentalEnabled: EnvBoolean.default(false),
686
706
  modelPricingJson: z.string().default("{}"),
707
+ // Supported-model membership source. Database mode is resolved by the async
708
+ // core overlay; getSettings remains synchronous and env-only.
709
+ modelCatalogSource: z.enum(["code", "database"]).default("code"),
710
+ // Deployment-owned workspace-facing price policy. This is deliberately
711
+ // separate from catalog membership and upstream credential ownership.
712
+ // Shape: { "product/model-id": "free" | "credits" }.
713
+ modelCostPolicyJson: z.string().default("{}"),
714
+ // Optional per-product agent guidance. Database mode replaces this with the
715
+ // singleton document's validated modelNotes map.
716
+ modelNotesJson: z.string().default("{}"),
717
+ // Managed OpenRouter credential. The curated model table is injected in
718
+ // code/catalog-document resolution and never read from host provider JSON.
719
+ openrouterApiKey: z.string().optional(),
720
+ // Internal, secret-free catalog overlays populated only by
721
+ // applyModelCatalogDocument. They intentionally have no OPENGENI_* env
722
+ // binding so database mode cannot be bypassed with a second source.
723
+ resolvedGatewayModelsJson: z.string().optional(),
724
+ resolvedOpenRouterModelsJson: z.string().optional(),
687
725
  // Extra (non-built-in) model providers, declared by the host as a JSON
688
726
  // provider registry. Each entry carries its own base URL, API key, wire API
689
727
  // ("responses" | "chat") and the models it exposes. The models a client may
@@ -1151,6 +1189,18 @@ const SettingsSchema = z.object({
1151
1189
  .positive()
1152
1190
  .max(SANDBOX_SNAPSHOT_MAX_TIMEOUT_MS)
1153
1191
  .default(60_000),
1192
+ // A zero-holder drain may need substantially longer than a best-effort
1193
+ // mid-turn/turn-end snapshot for a very large workspace. Keep that provider
1194
+ // budget independent so increasing drain recovery headroom cannot pin an
1195
+ // ordinary turn finalizer for the same duration. Unset preserves the legacy
1196
+ // single-budget behavior. Knob:
1197
+ // OPENGENI_SANDBOX_DRAIN_SNAPSHOT_TIMEOUT_MS.
1198
+ sandboxDrainSnapshotTimeoutMs: z.coerce
1199
+ .number()
1200
+ .int()
1201
+ .positive()
1202
+ .max(SANDBOX_SNAPSHOT_MAX_TIMEOUT_MS)
1203
+ .optional(),
1154
1204
  // Begin a controlled snapshot/quiesce/drain/rematerialize transition this far
1155
1205
  // ahead of a finite provider deadline. Modal's 24h creation clock cannot be
1156
1206
  // extended; the logical sandbox outlives it by moving to one successor box.
@@ -1575,6 +1625,7 @@ export type TemporalConnectionOptions = {
1575
1625
  export type ModelPricing = {
1576
1626
  inputMicrosPerMillionTokens: number;
1577
1627
  cachedInputMicrosPerMillionTokens?: number | undefined;
1628
+ cacheWriteMicrosPerMillionTokens?: number | undefined;
1578
1629
  outputMicrosPerMillionTokens: number;
1579
1630
  marginBps?: number | undefined;
1580
1631
  };
@@ -1608,6 +1659,7 @@ export type EntitlementsConfig = Entitlements;
1608
1659
  const ModelPricingSchema = z.object({
1609
1660
  inputMicrosPerMillionTokens: z.number().int().nonnegative(),
1610
1661
  cachedInputMicrosPerMillionTokens: z.number().int().nonnegative().optional(),
1662
+ cacheWriteMicrosPerMillionTokens: z.number().int().nonnegative().optional(),
1611
1663
  outputMicrosPerMillionTokens: z.number().int().nonnegative(),
1612
1664
  marginBps: z.number().int().min(0).max(100_000).optional(),
1613
1665
  });
@@ -1772,10 +1824,11 @@ export type ModelExecutionLimitsV1 = {
1772
1824
  export type CredentialSourceV1 =
1773
1825
  | { kind: "deployment"; mechanism: "api_key" | "azure_ad_bearer" | "none" }
1774
1826
  | { kind: "connected_subscription"; provider: "codex" | "xai" }
1775
- | { kind: "workspace_connection"; mechanism: "api_key" };
1827
+ | { kind: "workspace_connection"; mechanism: "api_key" }
1828
+ | { kind: "organization_connection"; mechanism: "api_key" };
1776
1829
 
1777
1830
  export type BillingAttributionV1 = {
1778
- upstreamPayer: "deployment" | "workspace" | "connected_subscription";
1831
+ upstreamPayer: "deployment" | "workspace" | "organization" | "connected_subscription";
1779
1832
  metering: "opengeni_credits" | "external";
1780
1833
  };
1781
1834
 
@@ -1810,6 +1863,9 @@ export const RegistryProviderKind = z.enum([
1810
1863
  "xai-subscription",
1811
1864
  "vercel-gateway-managed",
1812
1865
  "vercel-gateway-workspace",
1866
+ "vercel-gateway-organization",
1867
+ "openrouter-workspace",
1868
+ "openrouter-organization",
1813
1869
  ]);
1814
1870
  export type RegistryProviderKind = z.infer<typeof RegistryProviderKind>;
1815
1871
 
@@ -1932,6 +1988,350 @@ const RegistryProviderSchema = z
1932
1988
  });
1933
1989
  export type RegistryProvider = z.infer<typeof RegistryProviderSchema>;
1934
1990
 
1991
+ export const OPENGENI_GATEWAY_PROVIDER_ID = "opengeni-gateway" as const;
1992
+ export const WORKSPACE_GATEWAY_PROVIDER_ID = "workspace-gateway" as const;
1993
+ export const WORKSPACE_GATEWAY_MODEL_ID_PREFIX = "workspace-gateway/" as const;
1994
+ export const ORGANIZATION_GATEWAY_PROVIDER_ID = "organization-gateway" as const;
1995
+ export const ORGANIZATION_GATEWAY_MODEL_ID_PREFIX = "organization-gateway/" as const;
1996
+ export const OPENROUTER_PROVIDER_ID = "openrouter" as const;
1997
+ export const OPENROUTER_MODEL_ID_PREFIX = "openrouter/" as const;
1998
+ export const WORKSPACE_OPENROUTER_PROVIDER_ID = "workspace-openrouter" as const;
1999
+ export const WORKSPACE_OPENROUTER_MODEL_ID_PREFIX = "workspace-openrouter/" as const;
2000
+ export const ORGANIZATION_OPENROUTER_PROVIDER_ID = "organization-openrouter" as const;
2001
+ export const ORGANIZATION_OPENROUTER_MODEL_ID_PREFIX = "organization-openrouter/" as const;
2002
+ export const OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1" as const;
2003
+
2004
+ const RESERVED_MODEL_PROVIDER_IDS = new Set<string>([
2005
+ "openai",
2006
+ "azure",
2007
+ CODEX_PROVIDER_ID,
2008
+ XAI_SUBSCRIPTION_PROVIDER_ID,
2009
+ OPENGENI_GATEWAY_PROVIDER_ID,
2010
+ WORKSPACE_GATEWAY_PROVIDER_ID,
2011
+ ORGANIZATION_GATEWAY_PROVIDER_ID,
2012
+ OPENROUTER_PROVIDER_ID,
2013
+ WORKSPACE_OPENROUTER_PROVIDER_ID,
2014
+ ORGANIZATION_OPENROUTER_PROVIDER_ID,
2015
+ ]);
2016
+
2017
+ export const ModelCostClass = z.enum(["free", "credits"]);
2018
+ export type ModelCostClass = z.infer<typeof ModelCostClass>;
2019
+
2020
+ export const ConfiguredModelCostClass = z.enum([
2021
+ "free",
2022
+ "credits",
2023
+ "subscription",
2024
+ "workspace",
2025
+ "organization",
2026
+ ]);
2027
+ export type ConfiguredModelCostClass = z.infer<typeof ConfiguredModelCostClass>;
2028
+
2029
+ const ModelNote = z
2030
+ .string()
2031
+ .max(500)
2032
+ .refine((value) => !/[\r\n|]/u.test(value), {
2033
+ message: "model notes must not contain newlines or the | field separator",
2034
+ });
2035
+
2036
+ export function parseModelCostPolicyJson(raw: string): Record<string, ModelCostClass> {
2037
+ let parsed: unknown;
2038
+ try {
2039
+ parsed = JSON.parse(raw);
2040
+ } catch (error) {
2041
+ throw new Error(
2042
+ `OPENGENI_MODEL_COST_POLICY_JSON must be valid JSON: ${error instanceof Error ? error.message : String(error)}`,
2043
+ { cause: error },
2044
+ );
2045
+ }
2046
+ return z.record(z.string().min(1), ModelCostClass).parse(parsed);
2047
+ }
2048
+
2049
+ export function parseModelNotesJson(raw: string): Record<string, string> {
2050
+ let parsed: unknown;
2051
+ try {
2052
+ parsed = JSON.parse(raw);
2053
+ } catch (error) {
2054
+ throw new Error(
2055
+ `OPENGENI_MODEL_NOTES_JSON must be valid JSON: ${error instanceof Error ? error.message : String(error)}`,
2056
+ { cause: error },
2057
+ );
2058
+ }
2059
+ return z.record(z.string().min(1), ModelNote).parse(parsed);
2060
+ }
2061
+
2062
+ export function configuredModelNotes(
2063
+ settings: Pick<Settings, "modelNotesJson">,
2064
+ ): Record<string, string> {
2065
+ return parseModelNotesJson(settings.modelNotesJson);
2066
+ }
2067
+
2068
+ export const GatewayCatalogModel = z
2069
+ .object({
2070
+ productId: z.string().min(1),
2071
+ workspaceProductId: z.string().min(1).startsWith(WORKSPACE_GATEWAY_MODEL_ID_PREFIX),
2072
+ upstreamModelId: z.string().min(1),
2073
+ label: z.string().min(1),
2074
+ shortLabel: z.string().min(1).max(64).optional(),
2075
+ providers: z.array(z.string().min(1)).min(1),
2076
+ implicitCaching: z.boolean().default(false),
2077
+ vision: z.boolean().default(false),
2078
+ inputFileMediaTypes: z.array(z.string().min(1)).default([]),
2079
+ contextWindowTokens: z.number().int().positive().default(1_000_000),
2080
+ effectiveContextWindowTokens: z.number().int().positive().default(900_000),
2081
+ autoCompactTokenLimit: z.number().int().positive().default(850_000),
2082
+ pricing: z.union([ModelPricingSchema, ModelPricingScheduleSchema]).optional(),
2083
+ credentialSource: z.never().optional(),
2084
+ billing: z.never().optional(),
2085
+ apiKey: z.never().optional(),
2086
+ })
2087
+ .strict();
2088
+ export type GatewayCatalogModel = z.infer<typeof GatewayCatalogModel>;
2089
+
2090
+ export const OpenRouterCatalogModel = z
2091
+ .object({
2092
+ upstreamModelId: z.string().min(1).endsWith(":free"),
2093
+ label: z.string().min(1),
2094
+ shortLabel: z.string().min(1).max(64).optional(),
2095
+ aliases: z.array(z.string().min(1)).default([]),
2096
+ capabilities: ModelCapabilitiesV1Schema,
2097
+ contextWindowTokens: z.number().int().positive().optional(),
2098
+ effectiveContextWindowTokens: z.number().int().positive().optional(),
2099
+ autoCompactTokenLimit: z.number().int().positive().optional(),
2100
+ toolOutputTruncationTokens: z.number().int().positive().optional(),
2101
+ credentialSource: z.never().optional(),
2102
+ billing: z.never().optional(),
2103
+ pricing: z.never().optional(),
2104
+ apiKey: z.never().optional(),
2105
+ })
2106
+ .strict();
2107
+ export type OpenRouterCatalogModel = z.infer<typeof OpenRouterCatalogModel>;
2108
+
2109
+ const DeploymentRegistryBaseUrl = z
2110
+ .string()
2111
+ .url()
2112
+ .superRefine((value, context) => {
2113
+ const url = new URL(value);
2114
+ if (url.username || url.password) {
2115
+ context.addIssue({
2116
+ code: "custom",
2117
+ message: "database catalog provider baseUrl must not contain userinfo",
2118
+ });
2119
+ }
2120
+ if (url.search) {
2121
+ context.addIssue({
2122
+ code: "custom",
2123
+ message: "database catalog provider baseUrl must not contain a query",
2124
+ });
2125
+ }
2126
+ if (url.hash) {
2127
+ context.addIssue({
2128
+ code: "custom",
2129
+ message: "database catalog provider baseUrl must not contain a fragment",
2130
+ });
2131
+ }
2132
+ });
2133
+
2134
+ const DeploymentRegistryProviderKind = z.enum(["api-key", "anonymous"]);
2135
+
2136
+ const DeploymentRegistryModelSchema = RegistryModelSchema.safeExtend({
2137
+ pricing: z.never().optional(),
2138
+ }).strict();
2139
+
2140
+ const DeploymentRegistryProviderSchema = RegistryProviderSchema.safeExtend({
2141
+ kind: DeploymentRegistryProviderKind.default("api-key"),
2142
+ baseUrl: DeploymentRegistryBaseUrl,
2143
+ models: z.array(DeploymentRegistryModelSchema).min(1),
2144
+ apiKey: z.never().optional(),
2145
+ apiKeyEnv: z.never().optional(),
2146
+ defaultHeaders: z.never().optional(),
2147
+ defaultQuery: z.never().optional(),
2148
+ publicDefaultHeaderNames: z.never().optional(),
2149
+ publicDefaultQueryNames: z.never().optional(),
2150
+ }).strict();
2151
+
2152
+ const DeploymentGatewayCatalogModelSchema = GatewayCatalogModel.safeExtend({
2153
+ pricing: z.never().optional(),
2154
+ }).strict();
2155
+
2156
+ export const ModelCatalogDocument = z
2157
+ .object({
2158
+ schemaVersion: z.literal(1),
2159
+ /** Canonical deployment default. Omission preserves the V1 first-built-in
2160
+ * fallback for existing documents; operators should set this explicitly
2161
+ * when cutting over a registry or connected-subscription default. */
2162
+ defaultModel: z.string().min(1).optional(),
2163
+ builtInModels: z.array(z.string().min(1)).min(1),
2164
+ registryProviders: z.array(DeploymentRegistryProviderSchema).default([]),
2165
+ gatewayModels: z.array(DeploymentGatewayCatalogModelSchema).default([]),
2166
+ openrouterModels: z.array(OpenRouterCatalogModel).default([]),
2167
+ modelNotes: z.record(z.string().min(1), ModelNote).default({}),
2168
+ billing: z.never().optional(),
2169
+ enabled: z.never().optional(),
2170
+ apiKey: z.never().optional(),
2171
+ bands: z.never().optional(),
2172
+ })
2173
+ .strict()
2174
+ .superRefine((document, context) => {
2175
+ const productIds = new Set<string>();
2176
+ const providerIds = new Set<string>();
2177
+ const gatewayUpstreamIds = new Set<string>();
2178
+ const add = (id: string, path: Array<string | number>): void => {
2179
+ if (/[\u000A\u000D|]/u.test(id)) {
2180
+ context.addIssue({
2181
+ code: "custom",
2182
+ path,
2183
+ message: "catalog product ids must not contain newlines or the | field separator",
2184
+ });
2185
+ }
2186
+ if (productIds.has(id)) {
2187
+ context.addIssue({
2188
+ code: "custom",
2189
+ path,
2190
+ message: `duplicate product id ${id}`,
2191
+ });
2192
+ }
2193
+ productIds.add(id);
2194
+ };
2195
+ document.builtInModels.forEach((id, index) => add(id, ["builtInModels", index]));
2196
+ document.registryProviders.forEach((provider, providerIndex) => {
2197
+ if (RESERVED_MODEL_PROVIDER_IDS.has(provider.id)) {
2198
+ context.addIssue({
2199
+ code: "custom",
2200
+ path: ["registryProviders", providerIndex, "id"],
2201
+ message: `provider id ${provider.id} is reserved for a reviewed OpenGeni provider`,
2202
+ });
2203
+ }
2204
+ if (providerIds.has(provider.id)) {
2205
+ context.addIssue({
2206
+ code: "custom",
2207
+ path: ["registryProviders", providerIndex, "id"],
2208
+ message: `duplicate provider id ${provider.id}`,
2209
+ });
2210
+ }
2211
+ providerIds.add(provider.id);
2212
+ provider.models.forEach((model, modelIndex) =>
2213
+ add(model.id, ["registryProviders", providerIndex, "models", modelIndex, "id"]),
2214
+ );
2215
+ });
2216
+ document.gatewayModels.forEach((model, index) => {
2217
+ if (gatewayUpstreamIds.has(model.upstreamModelId)) {
2218
+ context.addIssue({
2219
+ code: "custom",
2220
+ path: ["gatewayModels", index, "upstreamModelId"],
2221
+ message: `duplicate Gateway upstream model id ${model.upstreamModelId}`,
2222
+ });
2223
+ }
2224
+ gatewayUpstreamIds.add(model.upstreamModelId);
2225
+ add(model.productId, ["gatewayModels", index, "productId"]);
2226
+ add(model.workspaceProductId, ["gatewayModels", index, "workspaceProductId"]);
2227
+ });
2228
+ document.openrouterModels.forEach((model, index) =>
2229
+ add(`${OPENROUTER_MODEL_ID_PREFIX}${model.upstreamModelId}`, [
2230
+ "openrouterModels",
2231
+ index,
2232
+ "upstreamModelId",
2233
+ ]),
2234
+ );
2235
+ if (document.defaultModel && /[\u000A\u000D|]/u.test(document.defaultModel)) {
2236
+ context.addIssue({
2237
+ code: "custom",
2238
+ path: ["defaultModel"],
2239
+ message: "catalog default model must not contain newlines or the | field separator",
2240
+ });
2241
+ }
2242
+ if (
2243
+ document.defaultModel &&
2244
+ !productIds.has(document.defaultModel) &&
2245
+ !document.defaultModel.startsWith(CODEX_MODEL_ID_PREFIX) &&
2246
+ !document.defaultModel.startsWith(XAI_SUBSCRIPTION_MODEL_ID_PREFIX)
2247
+ ) {
2248
+ context.addIssue({
2249
+ code: "custom",
2250
+ path: ["defaultModel"],
2251
+ message:
2252
+ "catalog default model must reference deployment catalog membership or a connected-subscription product",
2253
+ });
2254
+ }
2255
+ for (const productId of Object.keys(document.modelNotes)) {
2256
+ if (!productIds.has(productId)) {
2257
+ context.addIssue({
2258
+ code: "custom",
2259
+ path: ["modelNotes", productId],
2260
+ message: "model note references a product id outside the deployment catalog",
2261
+ });
2262
+ }
2263
+ }
2264
+ });
2265
+ export type ModelCatalogDocument = z.infer<typeof ModelCatalogDocument>;
2266
+
2267
+ export function parseModelCatalogDocument(value: unknown): ModelCatalogDocument {
2268
+ return ModelCatalogDocument.parse(value);
2269
+ }
2270
+
2271
+ function deploymentRegistryProvidersWithHostCredentials(
2272
+ settings: Settings,
2273
+ providers: readonly z.infer<typeof DeploymentRegistryProviderSchema>[],
2274
+ ): RegistryProvider[] {
2275
+ const hostProviders = new Map(
2276
+ parseModelProvidersJson(settings.modelProvidersJson).map((provider) => [provider.id, provider]),
2277
+ );
2278
+ return providers.map((provider) => {
2279
+ if (provider.kind !== "api-key") return provider;
2280
+ const host = hostProviders.get(provider.id);
2281
+ if (!host || host.kind !== "api-key") {
2282
+ throw new Error(
2283
+ `database model catalog provider ${provider.id} has no matching host-authorized api-key transport`,
2284
+ );
2285
+ }
2286
+ const transportIdentity = (candidate: typeof provider | RegistryProvider) => ({
2287
+ kind: candidate.kind,
2288
+ baseUrl: candidate.baseUrl,
2289
+ api: candidate.api,
2290
+ wireProfile: candidate.wireProfile,
2291
+ });
2292
+ if (canonicalJson(transportIdentity(provider)) !== canonicalJson(transportIdentity(host))) {
2293
+ throw new Error(
2294
+ `database model catalog provider ${provider.id} does not match its host-authorized transport`,
2295
+ );
2296
+ }
2297
+ return {
2298
+ ...provider,
2299
+ ...(host.defaultHeaders === undefined ? {} : { defaultHeaders: host.defaultHeaders }),
2300
+ ...(host.defaultQuery === undefined ? {} : { defaultQuery: host.defaultQuery }),
2301
+ ...(host.publicDefaultHeaderNames === undefined
2302
+ ? {}
2303
+ : { publicDefaultHeaderNames: host.publicDefaultHeaderNames }),
2304
+ ...(host.publicDefaultQueryNames === undefined
2305
+ ? {}
2306
+ : { publicDefaultQueryNames: host.publicDefaultQueryNames }),
2307
+ ...(host.apiKey === undefined ? {} : { apiKey: host.apiKey }),
2308
+ ...(host.apiKeyEnv === undefined ? {} : { apiKeyEnv: host.apiKeyEnv }),
2309
+ };
2310
+ });
2311
+ }
2312
+
2313
+ /** Pure secret-free database catalog overlay. getSettings remains env-only. */
2314
+ export function applyModelCatalogDocument(settings: Settings, rawDocument: unknown): Settings {
2315
+ const document = parseModelCatalogDocument(rawDocument);
2316
+ const defaultModel = document.defaultModel ?? document.builtInModels[0]!;
2317
+ const resolved = {
2318
+ ...settings,
2319
+ openaiModel: defaultModel,
2320
+ // Keep the complete built-in membership, including the default. The worker
2321
+ // replaces openaiModel with the exact turn model; the run-scoped router
2322
+ // needs one stable built-in id in this allow-list so a bare provider model
2323
+ // is not temporarily claimed by OpenAI/Azure during name re-resolution.
2324
+ openaiAllowedModels: document.builtInModels.join(","),
2325
+ modelProvidersJson: JSON.stringify(
2326
+ deploymentRegistryProvidersWithHostCredentials(settings, document.registryProviders),
2327
+ ),
2328
+ resolvedGatewayModelsJson: JSON.stringify(document.gatewayModels),
2329
+ resolvedOpenRouterModelsJson: JSON.stringify(document.openrouterModels),
2330
+ modelNotesJson: JSON.stringify(document.modelNotes),
2331
+ };
2332
+ return resolved;
2333
+ }
2334
+
1935
2335
  export const IntegrationOAuthClientConfigSchema = z.object({
1936
2336
  clientId: z.string().min(1),
1937
2337
  clientSecret: z.string().min(1).optional(),
@@ -1951,7 +2351,7 @@ export type IntegrationOAuthClientConfig = z.infer<typeof IntegrationOAuthClient
1951
2351
  export interface ResolvedModelProvider {
1952
2352
  id: string; // "openai" | "azure" | registry id
1953
2353
  label: string;
1954
- kind: RegistryProviderKind; // "api-key" (built-ins + most registry) | "anonymous" | subscription
2354
+ kind: RegistryProviderKind | "openrouter-managed";
1955
2355
  api: ModelProviderApi;
1956
2356
  wireProfile: ModelProviderWireProfile;
1957
2357
  builtin: boolean;
@@ -1965,6 +2365,10 @@ export interface ResolvedModelProvider {
1965
2365
  billing: BillingAttributionV1;
1966
2366
  }
1967
2367
 
2368
+ type InternalRegistryProvider = Omit<RegistryProvider, "kind"> & {
2369
+ kind: RegistryProviderKind | "openrouter-managed";
2370
+ };
2371
+
1968
2372
  /** A single exposed model + the provider that serves it. */
1969
2373
  export interface ConfiguredModel {
1970
2374
  schemaVersion: 1;
@@ -1981,6 +2385,8 @@ export interface ConfiguredModel {
1981
2385
  executionLimits: ModelExecutionLimitsV1;
1982
2386
  credentialSource: CredentialSourceV1;
1983
2387
  billing: BillingAttributionV1;
2388
+ /** Workspace-facing funding policy, independent of upstream settlement. */
2389
+ cost: ConfiguredModelCostClass;
1984
2390
  capabilities: ModelCapabilitiesV1;
1985
2391
  requestPolicy?: {
1986
2392
  gateway: {
@@ -2000,11 +2406,10 @@ export interface ConfiguredModel {
2000
2406
 
2001
2407
  export const VERCEL_AI_GATEWAY_BASE_URL = "https://ai-gateway.vercel.sh/v1" as const;
2002
2408
  export const VERCEL_AI_GATEWAY_AI_SDK_BASE_URL = "https://ai-gateway.vercel.sh/v4/ai" as const;
2003
- export const OPENGENI_GATEWAY_PROVIDER_ID = "opengeni-gateway" as const;
2004
- export const WORKSPACE_GATEWAY_PROVIDER_ID = "workspace-gateway" as const;
2005
- export const WORKSPACE_GATEWAY_MODEL_ID_PREFIX = "workspace-gateway/" as const;
2006
2409
  export const VERCEL_AI_GATEWAY_CONNECTION_DOMAIN = "ai-gateway.vercel.sh" as const;
2007
2410
  export const VERCEL_AI_GATEWAY_CONNECTION_ROLE = "vercel_ai_gateway" as const;
2411
+ export const WORKSPACE_OPENROUTER_CONNECTION_DOMAIN = "openrouter.ai" as const;
2412
+ export const WORKSPACE_OPENROUTER_CONNECTION_ROLE = "openrouter" as const;
2008
2413
 
2009
2414
  export const CODEX_REALTIME_MODEL_ID = "gpt-live-1-boulder-alpha" as const;
2010
2415
  export const SUPERGROK_REALTIME_MODEL_ID = "supergrok/grok-voice-think-fast-2.0" as const;
@@ -2074,16 +2479,133 @@ export const OPENGENI_GATEWAY_MODELS = {
2074
2479
  },
2075
2480
  } as const;
2076
2481
 
2482
+ export const OPENGENI_OPENROUTER_MODELS: readonly OpenRouterCatalogModel[] = [
2483
+ OpenRouterCatalogModel.parse({
2484
+ upstreamModelId: "nvidia/nemotron-3-super-120b-a12b:free",
2485
+ label: "Nemotron 3 Super 120B",
2486
+ shortLabel: "Nemotron 3 Super",
2487
+ aliases: [],
2488
+ capabilities: {
2489
+ reasoning: {
2490
+ upstream: "supported",
2491
+ // OpenRouter advertises the reasoning controls, but the catalogue does
2492
+ // not publish this model's accepted effort vocabulary. Preserve that
2493
+ // upstream fact without exposing an unverified runnable selector.
2494
+ runnable: false,
2495
+ efforts: [],
2496
+ defaultEffort: null,
2497
+ required: false,
2498
+ },
2499
+ functionCalling: { upstream: "supported", runnable: true },
2500
+ structuredOutput: { upstream: "supported", runnable: true },
2501
+ hostedTools: {
2502
+ webSearch: { upstream: "unknown", runnable: false },
2503
+ xSearch: { upstream: "unknown", runnable: false },
2504
+ codeExecution: { upstream: "unknown", runnable: false },
2505
+ imageGeneration: { upstream: "unknown", runnable: false },
2506
+ },
2507
+ inputModalities: ["text"],
2508
+ inputFileMediaTypes: [],
2509
+ outputModalities: ["text"],
2510
+ transports: {
2511
+ sse: { upstream: "supported", runnable: true },
2512
+ responsesWebSocket: { upstream: "unknown", runnable: false },
2513
+ realtimeAudio: { upstream: "unsupported", runnable: false },
2514
+ },
2515
+ latencyModes: [{ id: "standard", upstream: "unknown", runnable: true }],
2516
+ },
2517
+ contextWindowTokens: 262_144,
2518
+ effectiveContextWindowTokens: 235_929,
2519
+ autoCompactTokenLimit: 220_000,
2520
+ }),
2521
+ ];
2522
+
2523
+ function defaultGatewayCatalogModels(): GatewayCatalogModel[] {
2524
+ return [
2525
+ {
2526
+ ...OPENGENI_GATEWAY_MODELS.deepseek,
2527
+ vision: false,
2528
+ inputFileMediaTypes: [],
2529
+ contextWindowTokens: 1_000_000,
2530
+ effectiveContextWindowTokens: 900_000,
2531
+ autoCompactTokenLimit: 850_000,
2532
+ },
2533
+ {
2534
+ ...OPENGENI_GATEWAY_MODELS.kimi,
2535
+ vision: true,
2536
+ inputFileMediaTypes: ["application/pdf"],
2537
+ contextWindowTokens: 1_000_000,
2538
+ effectiveContextWindowTokens: 900_000,
2539
+ autoCompactTokenLimit: 850_000,
2540
+ },
2541
+ ].map((model) => GatewayCatalogModel.parse(model));
2542
+ }
2543
+
2544
+ function configuredGatewayCatalogModels(settings: Settings): GatewayCatalogModel[] {
2545
+ if (settings.resolvedGatewayModelsJson === undefined) {
2546
+ return defaultGatewayCatalogModels();
2547
+ }
2548
+ return z.array(GatewayCatalogModel).parse(JSON.parse(settings.resolvedGatewayModelsJson));
2549
+ }
2550
+
2551
+ export function configuredGatewayUpstreamModelIds(settings: Settings): string[] {
2552
+ return configuredGatewayCatalogModels(settings).map((model) => model.upstreamModelId);
2553
+ }
2554
+
2555
+ export function configuredGatewayWorkspaceProductModelIds(settings: Settings): string[] {
2556
+ return configuredGatewayCatalogModels(settings).map((model) => model.workspaceProductId);
2557
+ }
2558
+
2559
+ export function configuredGatewayOrganizationProductModelIds(settings: Settings): string[] {
2560
+ void settings;
2561
+ return [];
2562
+ }
2563
+
2564
+ export function configuredModelInputIdentities(settings: Settings): string[] {
2565
+ return configuredModels(settings).flatMap((model) => [model.id, ...model.aliases]);
2566
+ }
2567
+
2568
+ function configuredOpenRouterCatalogModels(settings: Settings): OpenRouterCatalogModel[] {
2569
+ if (settings.resolvedOpenRouterModelsJson === undefined) {
2570
+ return [...OPENGENI_OPENROUTER_MODELS];
2571
+ }
2572
+ return z.array(OpenRouterCatalogModel).parse(JSON.parse(settings.resolvedOpenRouterModelsJson));
2573
+ }
2574
+
2575
+ export function configuredOpenRouterUpstreamModelIds(settings: Settings): string[] {
2576
+ return configuredOpenRouterCatalogModels(settings).map((model) => model.upstreamModelId);
2577
+ }
2578
+
2579
+ function workspaceOpenRouterProductId(modelId: string): string {
2580
+ return `${WORKSPACE_OPENROUTER_MODEL_ID_PREFIX}${
2581
+ modelId.startsWith(OPENROUTER_MODEL_ID_PREFIX)
2582
+ ? modelId.slice(OPENROUTER_MODEL_ID_PREFIX.length)
2583
+ : modelId
2584
+ }`;
2585
+ }
2586
+
2587
+ export function configuredOpenRouterWorkspaceProductModelIds(settings: Settings): string[] {
2588
+ return configuredOpenRouterCatalogModels(settings).flatMap((model) =>
2589
+ [model.upstreamModelId, ...model.aliases].map(workspaceOpenRouterProductId),
2590
+ );
2591
+ }
2592
+
2593
+ export function configuredOpenRouterOrganizationProductModelIds(settings: Settings): string[] {
2594
+ void settings;
2595
+ return [];
2596
+ }
2597
+
2077
2598
  /**
2078
2599
  * Built-in OpenGeni credit pricing schedules.
2079
2600
  *
2080
2601
  * Rates are provider list prices in USD micros per 1M tokens. Debit applies
2081
- * `marginBps` (2_500 = +25%) on top. Long-context tiers follow OpenAI's
2602
+ * `marginBps` (500 = +5%) on top. Long-context tiers follow OpenAI's
2082
2603
  * ">272K input tokens" rule (threshold exclusive of 272_000).
2083
2604
  *
2084
2605
  * GPT-5.4 and older families are intentionally omitted — they are no longer
2085
- * offered. Codex / connected-subscription turns use `metering: external` and
2086
- * never consult this map.
2606
+ * offered. Codex / connected-subscription turns use `metering: external`, so
2607
+ * this map never debits them, but it does provide their equivalent OpenGeni
2608
+ * credit price when a matching product model is configured.
2087
2609
  *
2088
2610
  * When adding or changing a billed model, run `bun run check:model-pricing`
2089
2611
  * (see docs/model-providers.md § Price audit). That compares this map to
@@ -2092,20 +2614,23 @@ export const OPENGENI_GATEWAY_MODELS = {
2092
2614
  export const defaultModelPricing: Record<string, ModelPricingScheduleV1> = {
2093
2615
  "gpt-5.6-sol": {
2094
2616
  default: {
2095
- inputMicrosPerMillionTokens: 5_000_000,
2096
- cachedInputMicrosPerMillionTokens: 500_000,
2097
- outputMicrosPerMillionTokens: 30_000_000,
2098
- marginBps: 2_500,
2617
+ // Promotional OpenAI pricing, guaranteed through at least 2026-11-21.
2618
+ inputMicrosPerMillionTokens: 4_000_000,
2619
+ cachedInputMicrosPerMillionTokens: 400_000,
2620
+ cacheWriteMicrosPerMillionTokens: 5_000_000,
2621
+ outputMicrosPerMillionTokens: 20_000_000,
2622
+ marginBps: 500,
2099
2623
  },
2100
2624
  inputTokenTiers: [
2101
2625
  {
2102
2626
  // OpenAI: prompts with >272K input tokens use the long-context rate.
2103
2627
  minimumInputTokens: 272_001,
2104
2628
  pricing: {
2105
- inputMicrosPerMillionTokens: 10_000_000,
2106
- cachedInputMicrosPerMillionTokens: 1_000_000,
2107
- outputMicrosPerMillionTokens: 45_000_000,
2108
- marginBps: 2_500,
2629
+ inputMicrosPerMillionTokens: 8_000_000,
2630
+ cachedInputMicrosPerMillionTokens: 800_000,
2631
+ cacheWriteMicrosPerMillionTokens: 10_000_000,
2632
+ outputMicrosPerMillionTokens: 30_000_000,
2633
+ marginBps: 500,
2109
2634
  },
2110
2635
  },
2111
2636
  ],
@@ -2114,8 +2639,9 @@ export const defaultModelPricing: Record<string, ModelPricingScheduleV1> = {
2114
2639
  default: {
2115
2640
  inputMicrosPerMillionTokens: 2_000_000,
2116
2641
  cachedInputMicrosPerMillionTokens: 200_000,
2642
+ cacheWriteMicrosPerMillionTokens: 2_500_000,
2117
2643
  outputMicrosPerMillionTokens: 12_000_000,
2118
- marginBps: 2_500,
2644
+ marginBps: 500,
2119
2645
  },
2120
2646
  inputTokenTiers: [
2121
2647
  {
@@ -2123,8 +2649,9 @@ export const defaultModelPricing: Record<string, ModelPricingScheduleV1> = {
2123
2649
  pricing: {
2124
2650
  inputMicrosPerMillionTokens: 4_000_000,
2125
2651
  cachedInputMicrosPerMillionTokens: 400_000,
2652
+ cacheWriteMicrosPerMillionTokens: 5_000_000,
2126
2653
  outputMicrosPerMillionTokens: 18_000_000,
2127
- marginBps: 2_500,
2654
+ marginBps: 500,
2128
2655
  },
2129
2656
  },
2130
2657
  ],
@@ -2133,8 +2660,9 @@ export const defaultModelPricing: Record<string, ModelPricingScheduleV1> = {
2133
2660
  default: {
2134
2661
  inputMicrosPerMillionTokens: 200_000,
2135
2662
  cachedInputMicrosPerMillionTokens: 20_000,
2663
+ cacheWriteMicrosPerMillionTokens: 250_000,
2136
2664
  outputMicrosPerMillionTokens: 1_200_000,
2137
- marginBps: 2_500,
2665
+ marginBps: 500,
2138
2666
  },
2139
2667
  inputTokenTiers: [
2140
2668
  {
@@ -2142,8 +2670,9 @@ export const defaultModelPricing: Record<string, ModelPricingScheduleV1> = {
2142
2670
  pricing: {
2143
2671
  inputMicrosPerMillionTokens: 400_000,
2144
2672
  cachedInputMicrosPerMillionTokens: 40_000,
2673
+ cacheWriteMicrosPerMillionTokens: 500_000,
2145
2674
  outputMicrosPerMillionTokens: 1_800_000,
2146
- marginBps: 2_500,
2675
+ marginBps: 500,
2147
2676
  },
2148
2677
  },
2149
2678
  ],
@@ -2158,7 +2687,7 @@ export const defaultModelPricing: Record<string, ModelPricingScheduleV1> = {
2158
2687
  inputMicrosPerMillionTokens: 140_000,
2159
2688
  cachedInputMicrosPerMillionTokens: 28_000,
2160
2689
  outputMicrosPerMillionTokens: 280_000,
2161
- marginBps: 2_500,
2690
+ marginBps: 500,
2162
2691
  },
2163
2692
  },
2164
2693
  [OPENGENI_GATEWAY_MODELS.kimi.productId]: {
@@ -2166,7 +2695,7 @@ export const defaultModelPricing: Record<string, ModelPricingScheduleV1> = {
2166
2695
  inputMicrosPerMillionTokens: 3_000_000,
2167
2696
  cachedInputMicrosPerMillionTokens: 300_000,
2168
2697
  outputMicrosPerMillionTokens: 15_000_000,
2169
- marginBps: 2_500,
2698
+ marginBps: 500,
2170
2699
  },
2171
2700
  },
2172
2701
  // Fireworks AI / GLM 5.2 — the first shipped non-OpenAI registry model. A
@@ -2178,7 +2707,7 @@ export const defaultModelPricing: Record<string, ModelPricingScheduleV1> = {
2178
2707
  inputMicrosPerMillionTokens: 1_400_000,
2179
2708
  cachedInputMicrosPerMillionTokens: 140_000,
2180
2709
  outputMicrosPerMillionTokens: 4_400_000,
2181
- marginBps: 2_500,
2710
+ marginBps: 500,
2182
2711
  },
2183
2712
  },
2184
2713
  };
@@ -2269,12 +2798,17 @@ function objectStorageConfiguredForWorkspaceArchives(settings: Settings): boolea
2269
2798
  }
2270
2799
  }
2271
2800
 
2272
- function optional(name: string): string | undefined {
2273
- const value = process.env[name];
2801
+ function optionalEnvironmentValue(name: string, source: NodeJS.ProcessEnv): string | undefined {
2802
+ const value = source[name];
2274
2803
  return value && value.trim().length > 0 ? value : undefined;
2275
2804
  }
2276
2805
 
2277
- export function getSettings(): Settings {
2806
+ export function getSettings(source: NodeJS.ProcessEnv = process.env): Settings {
2807
+ const optional = (name: string): string | undefined => optionalEnvironmentValue(name, source);
2808
+ const modelCatalogSource = optional("OPENGENI_MODEL_CATALOG_SOURCE");
2809
+ const modelCostPolicyJson =
2810
+ optional("OPENGENI_MODEL_COST_POLICY_JSON") ??
2811
+ (modelCatalogSource === "database" ? "{}" : DEFAULT_MODEL_COST_POLICY_JSON);
2278
2812
  const raw = {
2279
2813
  serviceName: optional("OPENGENI_SERVICE_NAME"),
2280
2814
  environment: optional("OPENGENI_ENVIRONMENT"),
@@ -2395,6 +2929,9 @@ export function getSettings(): Settings {
2395
2929
  goalIdleBackoffMs: optional("OPENGENI_GOAL_IDLE_BACKOFF_MS"),
2396
2930
  goalIdleBackoffMaxMs: optional("OPENGENI_GOAL_IDLE_BACKOFF_MAX_MS"),
2397
2931
  childLifecycleNoticesEnabled: optional("OPENGENI_CHILD_LIFECYCLE_NOTICES_ENABLED"),
2932
+ hostMcpAuthoritySourceAdmissionEnabled: optional(
2933
+ "OPENGENI_HOST_MCP_AUTHORITY_SOURCE_ADMISSION_ENABLED",
2934
+ ),
2398
2935
  slackWorkspaceRoutingEnabled: optional("OPENGENI_SLACK_WORKSPACE_ROUTING_ENABLED"),
2399
2936
  agentMaxModelCallsPerTurn: optional("OPENGENI_AGENT_MAX_MODEL_CALLS_PER_TURN"),
2400
2937
  contextWindowTokens: optional("OPENGENI_CONTEXT_WINDOW_TOKENS"),
@@ -2464,6 +3001,10 @@ export function getSettings(): Settings {
2464
3001
  voiceInputAzureAdToken: optional("OPENGENI_VOICE_INPUT_AZURE_AD_TOKEN"),
2465
3002
  voiceInputCodexExperimentalEnabled: optional("OPENGENI_VOICE_INPUT_CODEX_EXPERIMENTAL"),
2466
3003
  modelPricingJson: optional("OPENGENI_MODEL_PRICING_JSON"),
3004
+ modelCatalogSource,
3005
+ modelCostPolicyJson,
3006
+ modelNotesJson: optional("OPENGENI_MODEL_NOTES_JSON"),
3007
+ openrouterApiKey: optional("OPENGENI_OPENROUTER_API_KEY"),
2467
3008
  modelProvidersJson: optional("OPENGENI_MODEL_PROVIDERS_JSON"),
2468
3009
  codexSubscriptionEnabled: optional("OPENGENI_CODEX_SUBSCRIPTION_ENABLED"),
2469
3010
  supergrokSubscriptionEnabled: optional("OPENGENI_SUPERGROK_SUBSCRIPTION_ENABLED"),
@@ -2600,6 +3141,7 @@ export function getSettings(): Settings {
2600
3141
  sandboxIdleGraceMs: optional("OPENGENI_SANDBOX_IDLE_GRACE_MS"),
2601
3142
  sandboxSnapshotIntervalMs: optional("OPENGENI_SANDBOX_SNAPSHOT_INTERVAL_MS"),
2602
3143
  sandboxSnapshotTimeoutMs: optional("OPENGENI_SANDBOX_SNAPSHOT_TIMEOUT_MS"),
3144
+ sandboxDrainSnapshotTimeoutMs: optional("OPENGENI_SANDBOX_DRAIN_SNAPSHOT_TIMEOUT_MS"),
2603
3145
  sandboxRotationLeadMs: optional("OPENGENI_SANDBOX_ROTATION_LEAD_MS"),
2604
3146
  sandboxRotationBatchSize: optional("OPENGENI_SANDBOX_ROTATION_BATCH_SIZE"),
2605
3147
  sandboxLeaseTtlMs: optional("OPENGENI_SANDBOX_LEASE_TTL_MS"),
@@ -2695,7 +3237,7 @@ export function getSettings(): Settings {
2695
3237
  : parsed.sandboxRotationLeadMs,
2696
3238
  mcpServers: ensureBuiltInMcpServers(parsed),
2697
3239
  };
2698
- validateSettings(settings);
3240
+ validateSettings(settings, source);
2699
3241
  return settings;
2700
3242
  }
2701
3243
 
@@ -2819,10 +3361,23 @@ export function sandboxArchiveCaptureTimeoutMs(
2819
3361
  );
2820
3362
  }
2821
3363
 
3364
+ /** Provider operation budget used only by zero-holder drain/rotation capture.
3365
+ * Unset preserves the historical shared snapshot budget exactly. */
3366
+ export function effectiveSandboxDrainSnapshotTimeoutMs(
3367
+ settings: Pick<Settings, "sandboxSnapshotTimeoutMs" | "sandboxDrainSnapshotTimeoutMs">,
3368
+ ): number {
3369
+ return settings.sandboxDrainSnapshotTimeoutMs ?? settings.sandboxSnapshotTimeoutMs;
3370
+ }
3371
+
2822
3372
  export function sandboxLifecycleTransitionWaitMs(
2823
- settings: Pick<Settings, "sandboxSnapshotTimeoutMs" | "sandboxLeaseReaperPeriodMs">,
3373
+ settings: Pick<
3374
+ Settings,
3375
+ "sandboxSnapshotTimeoutMs" | "sandboxDrainSnapshotTimeoutMs" | "sandboxLeaseReaperPeriodMs"
3376
+ >,
2824
3377
  ): number {
2825
- const captureTimeoutMs = sandboxArchiveCaptureTimeoutMs(settings);
3378
+ const captureTimeoutMs = sandboxArchiveCaptureTimeoutMs({
3379
+ sandboxSnapshotTimeoutMs: effectiveSandboxDrainSnapshotTimeoutMs(settings),
3380
+ });
2826
3381
  return Math.min(
2827
3382
  SANDBOX_LIFECYCLE_TRANSITION_MAX_WAIT_MS,
2828
3383
  settings.sandboxLeaseReaperPeriodMs +
@@ -3140,10 +3695,9 @@ function legacyModelCapabilities(
3140
3695
 
3141
3696
  export function gatewayRequestPolicyForUpstreamModel(
3142
3697
  upstreamModelId: string,
3698
+ models: readonly GatewayCatalogModel[] = defaultGatewayCatalogModels(),
3143
3699
  ): ConfiguredModel["requestPolicy"] {
3144
- const model = Object.values(OPENGENI_GATEWAY_MODELS).find(
3145
- (candidate) => candidate.upstreamModelId === upstreamModelId,
3146
- );
3700
+ const model = models.find((candidate) => candidate.upstreamModelId === upstreamModelId);
3147
3701
  if (!model) {
3148
3702
  return undefined;
3149
3703
  }
@@ -3157,7 +3711,11 @@ export function gatewayRequestPolicyForUpstreamModel(
3157
3711
 
3158
3712
  function gatewayModelCapabilities(
3159
3713
  settings: Settings,
3160
- input: { implicitCaching: boolean; vision: boolean; inputFileMediaTypes?: string[] },
3714
+ input: {
3715
+ implicitCaching: boolean;
3716
+ vision: boolean;
3717
+ inputFileMediaTypes?: string[];
3718
+ },
3161
3719
  ): ModelCapabilitiesV1 {
3162
3720
  const legacy = legacyModelCapabilities(settings, {
3163
3721
  reasoningEffort: true,
@@ -3181,35 +3739,112 @@ function gatewayModelCapabilities(
3181
3739
  });
3182
3740
  }
3183
3741
 
3742
+ function openRouterCustomModelCapabilities(settings: Settings): ModelCapabilitiesV1 {
3743
+ const legacy = legacyModelCapabilities(settings, {
3744
+ reasoningEffort: false,
3745
+ hostedWebSearch: false,
3746
+ });
3747
+ return normalizeCapabilities({
3748
+ ...legacy,
3749
+ functionCalling: { upstream: "supported", runnable: true },
3750
+ inputModalities: ["text"],
3751
+ inputFileMediaTypes: [],
3752
+ transports: {
3753
+ ...legacy.transports,
3754
+ sse: { upstream: "supported", runnable: true },
3755
+ },
3756
+ promptCaching: { upstream: "unsupported", runnable: false, mode: "none" },
3757
+ latencyModes: [{ id: "standard", upstream: "supported", runnable: true }],
3758
+ });
3759
+ }
3760
+
3184
3761
  function gatewayRegistryProvider(
3185
3762
  settings: Settings,
3186
3763
  input:
3187
3764
  | { kind: "vercel-gateway-managed"; apiKey: string }
3188
- | { kind: "vercel-gateway-workspace"; apiKey?: string },
3189
- ): RegistryProvider {
3765
+ | {
3766
+ kind: "vercel-gateway-workspace" | "vercel-gateway-organization";
3767
+ apiKey?: string;
3768
+ customModels?: readonly {
3769
+ upstreamModelId: string;
3770
+ label?: string | null;
3771
+ }[];
3772
+ },
3773
+ ): InternalRegistryProvider {
3190
3774
  const workspace = input.kind === "vercel-gateway-workspace";
3191
- const models = [OPENGENI_GATEWAY_MODELS.deepseek, OPENGENI_GATEWAY_MODELS.kimi].map((model) => {
3192
- const kimi = model === OPENGENI_GATEWAY_MODELS.kimi;
3775
+ const organization = input.kind === "vercel-gateway-organization";
3776
+ const scoped = workspace || organization;
3777
+ const curated = organization ? [] : configuredGatewayCatalogModels(settings);
3778
+ const upstreamIds = new Set(curated.map((model) => model.upstreamModelId));
3779
+ const productIds = new Set(
3780
+ parseModelProvidersJson(settings.modelProvidersJson)
3781
+ .filter(
3782
+ (provider) =>
3783
+ provider.id !== WORKSPACE_GATEWAY_PROVIDER_ID &&
3784
+ provider.id !== ORGANIZATION_GATEWAY_PROVIDER_ID,
3785
+ )
3786
+ .flatMap((provider) =>
3787
+ provider.models.flatMap((model) => [model.id, ...(model.aliases ?? [])]),
3788
+ ),
3789
+ );
3790
+ const models = curated.map((model) => {
3791
+ const id = workspace
3792
+ ? model.workspaceProductId
3793
+ : organization
3794
+ ? `${ORGANIZATION_GATEWAY_MODEL_ID_PREFIX}${model.upstreamModelId}`
3795
+ : model.productId;
3796
+ productIds.add(id);
3193
3797
  return {
3194
- id: workspace ? model.workspaceProductId : model.productId,
3798
+ id,
3195
3799
  upstreamModelId: model.upstreamModelId,
3196
3800
  label: model.label,
3197
- shortLabel: model.shortLabel,
3801
+ ...(model.shortLabel ? { shortLabel: model.shortLabel } : {}),
3198
3802
  capabilities: gatewayModelCapabilities(settings, {
3199
3803
  implicitCaching: model.implicitCaching,
3200
- vision: kimi,
3201
- inputFileMediaTypes: kimi ? ["application/pdf"] : [],
3804
+ vision: model.vision,
3805
+ inputFileMediaTypes: model.inputFileMediaTypes,
3202
3806
  }),
3203
- contextWindowTokens: 1_000_000,
3204
- effectiveContextWindowTokens: 900_000,
3205
- autoCompactTokenLimit: 850_000,
3807
+ contextWindowTokens: model.contextWindowTokens,
3808
+ effectiveContextWindowTokens: model.effectiveContextWindowTokens,
3809
+ autoCompactTokenLimit: model.autoCompactTokenLimit,
3206
3810
  toolOutputTruncationTokens: settings.modelToolOutputTruncationTokens,
3811
+ ...(model.pricing === undefined ? {} : { pricing: model.pricing }),
3207
3812
  };
3208
3813
  });
3814
+ if (scoped) {
3815
+ for (const custom of input.customModels ?? []) {
3816
+ const productId = `${workspace ? WORKSPACE_GATEWAY_MODEL_ID_PREFIX : ORGANIZATION_GATEWAY_MODEL_ID_PREFIX}${custom.upstreamModelId}`;
3817
+ // Deployment membership wins over an older or concurrently-created
3818
+ // workspace row with the same upstream identity or generated product id.
3819
+ // This keeps runtime routing deterministic and prevents a legacy/admin
3820
+ // row from making the entire workspace catalog fail uniqueness checks.
3821
+ if (upstreamIds.has(custom.upstreamModelId) || productIds.has(productId)) continue;
3822
+ upstreamIds.add(custom.upstreamModelId);
3823
+ productIds.add(productId);
3824
+ models.push({
3825
+ id: productId,
3826
+ upstreamModelId: custom.upstreamModelId,
3827
+ label: custom.label?.trim() || custom.upstreamModelId,
3828
+ capabilities: gatewayModelCapabilities(settings, {
3829
+ implicitCaching: false,
3830
+ vision: false,
3831
+ inputFileMediaTypes: [],
3832
+ }),
3833
+ contextWindowTokens: 1_000_000,
3834
+ effectiveContextWindowTokens: 900_000,
3835
+ autoCompactTokenLimit: 850_000,
3836
+ toolOutputTruncationTokens: settings.modelToolOutputTruncationTokens,
3837
+ });
3838
+ }
3839
+ }
3209
3840
  return {
3210
3841
  kind: input.kind,
3211
- id: workspace ? WORKSPACE_GATEWAY_PROVIDER_ID : OPENGENI_GATEWAY_PROVIDER_ID,
3212
- label: workspace ? "Your Gateway" : "OpenGeni",
3842
+ id: workspace
3843
+ ? WORKSPACE_GATEWAY_PROVIDER_ID
3844
+ : organization
3845
+ ? ORGANIZATION_GATEWAY_PROVIDER_ID
3846
+ : OPENGENI_GATEWAY_PROVIDER_ID,
3847
+ label: workspace ? "Your Gateway" : organization ? "Organization Gateway" : "OpenGeni",
3213
3848
  // Responses preserves vision, reasoning items, and provider-native usage.
3214
3849
  // Model-specific compatibility stays at the reviewed request fence rather
3215
3850
  // than downgrading the whole provider wire.
@@ -3221,52 +3856,274 @@ function gatewayRegistryProvider(
3221
3856
  };
3222
3857
  }
3223
3858
 
3224
- function configuredRegistryProviders(settings: Settings): RegistryProvider[] {
3225
- const providers = parseModelProvidersJson(settings.modelProvidersJson);
3226
- if (!settings.vercelAiGatewayApiKey) {
3227
- return providers;
3859
+ function openRouterRegistryProvider(
3860
+ settings: Settings,
3861
+ input:
3862
+ | { kind: "openrouter-managed"; apiKey: string }
3863
+ | {
3864
+ kind: "openrouter-workspace" | "openrouter-organization";
3865
+ apiKey?: string;
3866
+ customModels?: readonly {
3867
+ upstreamModelId: string;
3868
+ label?: string | null;
3869
+ }[];
3870
+ },
3871
+ ): InternalRegistryProvider | null {
3872
+ const workspace = input.kind === "openrouter-workspace";
3873
+ const organization = input.kind === "openrouter-organization";
3874
+ const scoped = workspace || organization;
3875
+ const curated = organization ? [] : configuredOpenRouterCatalogModels(settings);
3876
+ const upstreamIds = new Set(curated.map((model) => model.upstreamModelId));
3877
+ const productIds = new Set(
3878
+ parseModelProvidersJson(settings.modelProvidersJson)
3879
+ .filter(
3880
+ (provider) =>
3881
+ provider.id !== WORKSPACE_OPENROUTER_PROVIDER_ID &&
3882
+ provider.id !== ORGANIZATION_OPENROUTER_PROVIDER_ID,
3883
+ )
3884
+ .flatMap((provider) =>
3885
+ provider.models.flatMap((model) => [model.id, ...(model.aliases ?? [])]),
3886
+ ),
3887
+ );
3888
+ const models: RegistryProvider["models"] = curated.map((model) => {
3889
+ const id = workspace
3890
+ ? workspaceOpenRouterProductId(model.upstreamModelId)
3891
+ : organization
3892
+ ? `${ORGANIZATION_OPENROUTER_MODEL_ID_PREFIX}${model.upstreamModelId}`
3893
+ : `${OPENROUTER_MODEL_ID_PREFIX}${model.upstreamModelId}`;
3894
+ const aliases = workspace
3895
+ ? model.aliases.map(workspaceOpenRouterProductId)
3896
+ : organization
3897
+ ? model.aliases.map((alias) => `${ORGANIZATION_OPENROUTER_MODEL_ID_PREFIX}${alias}`)
3898
+ : model.aliases;
3899
+ productIds.add(id);
3900
+ for (const alias of aliases) productIds.add(alias);
3901
+ return {
3902
+ id,
3903
+ upstreamModelId: model.upstreamModelId,
3904
+ aliases,
3905
+ label: model.label,
3906
+ ...(model.shortLabel ? { shortLabel: model.shortLabel } : {}),
3907
+ capabilities: model.capabilities,
3908
+ ...(model.contextWindowTokens === undefined
3909
+ ? {}
3910
+ : { contextWindowTokens: model.contextWindowTokens }),
3911
+ ...(model.effectiveContextWindowTokens === undefined
3912
+ ? {}
3913
+ : { effectiveContextWindowTokens: model.effectiveContextWindowTokens }),
3914
+ ...(model.autoCompactTokenLimit === undefined
3915
+ ? {}
3916
+ : { autoCompactTokenLimit: model.autoCompactTokenLimit }),
3917
+ toolOutputTruncationTokens:
3918
+ model.toolOutputTruncationTokens ?? settings.modelToolOutputTruncationTokens,
3919
+ };
3920
+ });
3921
+ if (scoped) {
3922
+ for (const custom of input.customModels ?? []) {
3923
+ const productId = `${workspace ? WORKSPACE_OPENROUTER_MODEL_ID_PREFIX : ORGANIZATION_OPENROUTER_MODEL_ID_PREFIX}${custom.upstreamModelId}`;
3924
+ if (upstreamIds.has(custom.upstreamModelId) || productIds.has(productId)) continue;
3925
+ upstreamIds.add(custom.upstreamModelId);
3926
+ productIds.add(productId);
3927
+ models.push({
3928
+ id: productId,
3929
+ upstreamModelId: custom.upstreamModelId,
3930
+ aliases: [],
3931
+ label: custom.label?.trim() || custom.upstreamModelId,
3932
+ capabilities: openRouterCustomModelCapabilities(settings),
3933
+ toolOutputTruncationTokens: settings.modelToolOutputTruncationTokens,
3934
+ });
3935
+ }
3228
3936
  }
3229
- if (providers.some((provider) => provider.id === OPENGENI_GATEWAY_PROVIDER_ID)) {
3230
- throw new Error(
3231
- `${OPENGENI_GATEWAY_PROVIDER_ID} is reserved for OPENGENI_VERCEL_AI_GATEWAY_API_KEY`,
3937
+ if (models.length === 0) return null;
3938
+ const defaultHeaders: Record<string, string> = {
3939
+ "x-title": "OpenGeni",
3940
+ ...(settings.publicBaseUrl ? { "http-referer": settings.publicBaseUrl } : {}),
3941
+ };
3942
+ return {
3943
+ kind: input.kind,
3944
+ id: workspace
3945
+ ? WORKSPACE_OPENROUTER_PROVIDER_ID
3946
+ : organization
3947
+ ? ORGANIZATION_OPENROUTER_PROVIDER_ID
3948
+ : OPENROUTER_PROVIDER_ID,
3949
+ label: workspace ? "Your OpenRouter" : organization ? "Organization OpenRouter" : "OpenRouter",
3950
+ api: "chat",
3951
+ wireProfile: "openai",
3952
+ baseUrl: OPENROUTER_BASE_URL,
3953
+ ...(input.apiKey ? { apiKey: input.apiKey } : {}),
3954
+ defaultHeaders,
3955
+ publicDefaultHeaderNames: Object.keys(defaultHeaders),
3956
+ models,
3957
+ };
3958
+ }
3959
+
3960
+ function configuredRegistryProviders(settings: Settings): InternalRegistryProvider[] {
3961
+ const providers = parseModelProvidersJson(settings.modelProvidersJson);
3962
+ const injected: InternalRegistryProvider[] = [...providers];
3963
+ if (settings.vercelAiGatewayApiKey && configuredGatewayCatalogModels(settings).length > 0) {
3964
+ injected.push(
3965
+ gatewayRegistryProvider(settings, {
3966
+ kind: "vercel-gateway-managed",
3967
+ apiKey: settings.vercelAiGatewayApiKey,
3968
+ }),
3232
3969
  );
3233
3970
  }
3234
- return [
3235
- ...providers,
3236
- gatewayRegistryProvider(settings, {
3237
- kind: "vercel-gateway-managed",
3238
- apiKey: settings.vercelAiGatewayApiKey,
3239
- }),
3240
- ];
3971
+ const openrouter = settings.openrouterApiKey
3972
+ ? openRouterRegistryProvider(settings, {
3973
+ kind: "openrouter-managed",
3974
+ apiKey: settings.openrouterApiKey,
3975
+ })
3976
+ : null;
3977
+ if (openrouter) injected.push(openrouter);
3978
+ return injected;
3241
3979
  }
3242
3980
 
3243
3981
  /** Static catalog overlay; it contains no concrete workspace credential. */
3244
- export function withWorkspaceGatewayCatalogProvider(settings: Settings): Settings {
3982
+ export function withWorkspaceGatewayCatalogProvider(
3983
+ settings: Settings,
3984
+ customModels: readonly {
3985
+ upstreamModelId: string;
3986
+ label?: string | null;
3987
+ }[] = [],
3988
+ ): Settings {
3245
3989
  const providers = parseModelProvidersJson(settings.modelProvidersJson);
3246
- if (providers.some((provider) => provider.id === WORKSPACE_GATEWAY_PROVIDER_ID)) {
3247
- return settings;
3248
- }
3990
+ const withoutWorkspace = providers.filter(
3991
+ (provider) => provider.id !== WORKSPACE_GATEWAY_PROVIDER_ID,
3992
+ );
3993
+ const curatedCount = configuredGatewayCatalogModels(settings).length;
3994
+ if (curatedCount === 0 && customModels.length === 0) return settings;
3249
3995
  return {
3250
3996
  ...settings,
3251
3997
  modelProvidersJson: JSON.stringify([
3252
- ...providers,
3253
- gatewayRegistryProvider(settings, { kind: "vercel-gateway-workspace" }),
3998
+ ...withoutWorkspace,
3999
+ gatewayRegistryProvider(settings, {
4000
+ kind: "vercel-gateway-workspace",
4001
+ customModels,
4002
+ }),
3254
4003
  ]),
3255
4004
  };
3256
4005
  }
3257
4006
 
3258
4007
  /** Runtime overlay after the worker resolves the workspace's encrypted key. */
3259
- export function withWorkspaceGatewayCredential(settings: Settings, apiKey: string): Settings {
4008
+ export function withWorkspaceGatewayCredential(
4009
+ settings: Settings,
4010
+ apiKey: string,
4011
+ customModels: readonly {
4012
+ upstreamModelId: string;
4013
+ label?: string | null;
4014
+ }[] = [],
4015
+ ): Settings {
3260
4016
  if (!apiKey.trim()) {
3261
4017
  throw new Error("workspace AI Gateway credential is empty");
3262
4018
  }
3263
- const catalogSettings = withWorkspaceGatewayCatalogProvider(settings);
4019
+ const catalogSettings = withWorkspaceGatewayCatalogProvider(settings, customModels);
3264
4020
  const providers = parseModelProvidersJson(catalogSettings.modelProvidersJson).map((provider) =>
3265
4021
  provider.id === WORKSPACE_GATEWAY_PROVIDER_ID ? { ...provider, apiKey } : provider,
3266
4022
  );
3267
4023
  return { ...catalogSettings, modelProvidersJson: JSON.stringify(providers) };
3268
4024
  }
3269
4025
 
4026
+ /** Static OpenRouter catalog overlay; it contains no concrete workspace credential. */
4027
+ export function withWorkspaceOpenRouterCatalogProvider(
4028
+ settings: Settings,
4029
+ customModels: readonly {
4030
+ upstreamModelId: string;
4031
+ label?: string | null;
4032
+ }[] = [],
4033
+ ): Settings {
4034
+ const providers = parseModelProvidersJson(settings.modelProvidersJson);
4035
+ const withoutWorkspace = providers.filter(
4036
+ (provider) => provider.id !== WORKSPACE_OPENROUTER_PROVIDER_ID,
4037
+ );
4038
+ const provider = openRouterRegistryProvider(settings, {
4039
+ kind: "openrouter-workspace",
4040
+ customModels,
4041
+ });
4042
+ if (!provider) return settings;
4043
+ return {
4044
+ ...settings,
4045
+ modelProvidersJson: JSON.stringify([...withoutWorkspace, provider]),
4046
+ };
4047
+ }
4048
+
4049
+ /** Runtime overlay after the worker resolves the workspace's encrypted OpenRouter key. */
4050
+ export function withWorkspaceOpenRouterCredential(
4051
+ settings: Settings,
4052
+ apiKey: string,
4053
+ customModels: readonly {
4054
+ upstreamModelId: string;
4055
+ label?: string | null;
4056
+ }[] = [],
4057
+ ): Settings {
4058
+ if (!apiKey.trim()) {
4059
+ throw new Error("workspace OpenRouter credential is empty");
4060
+ }
4061
+ const catalogSettings = withWorkspaceOpenRouterCatalogProvider(settings, customModels);
4062
+ const providers = parseModelProvidersJson(catalogSettings.modelProvidersJson).map((provider) =>
4063
+ provider.id === WORKSPACE_OPENROUTER_PROVIDER_ID ? { ...provider, apiKey } : provider,
4064
+ );
4065
+ return { ...catalogSettings, modelProvidersJson: JSON.stringify(providers) };
4066
+ }
4067
+
4068
+ /** Secret-free organization Vercel AI Gateway catalog overlay. */
4069
+ export function withOrganizationGatewayCatalogProvider(
4070
+ settings: Settings,
4071
+ customModels: readonly { upstreamModelId: string; label?: string | null }[] = [],
4072
+ ): Settings {
4073
+ if (customModels.length === 0) return settings;
4074
+ const providers = parseModelProvidersJson(settings.modelProvidersJson).filter(
4075
+ (provider) => provider.id !== ORGANIZATION_GATEWAY_PROVIDER_ID,
4076
+ );
4077
+ const provider = gatewayRegistryProvider(settings, {
4078
+ kind: "vercel-gateway-organization",
4079
+ customModels,
4080
+ });
4081
+ return { ...settings, modelProvidersJson: JSON.stringify([...providers, provider]) };
4082
+ }
4083
+
4084
+ export function withOrganizationGatewayCredential(
4085
+ settings: Settings,
4086
+ apiKey: string,
4087
+ customModels: readonly { upstreamModelId: string; label?: string | null }[] = [],
4088
+ ): Settings {
4089
+ if (!apiKey.trim()) throw new Error("organization AI Gateway credential is empty");
4090
+ const catalog = withOrganizationGatewayCatalogProvider(settings, customModels);
4091
+ const providers = parseModelProvidersJson(catalog.modelProvidersJson).map((provider) =>
4092
+ provider.id === ORGANIZATION_GATEWAY_PROVIDER_ID ? { ...provider, apiKey } : provider,
4093
+ );
4094
+ return { ...catalog, modelProvidersJson: JSON.stringify(providers) };
4095
+ }
4096
+
4097
+ /** Secret-free organization OpenRouter catalog overlay. */
4098
+ export function withOrganizationOpenRouterCatalogProvider(
4099
+ settings: Settings,
4100
+ customModels: readonly { upstreamModelId: string; label?: string | null }[] = [],
4101
+ ): Settings {
4102
+ const providers = parseModelProvidersJson(settings.modelProvidersJson).filter(
4103
+ (provider) => provider.id !== ORGANIZATION_OPENROUTER_PROVIDER_ID,
4104
+ );
4105
+ const provider = openRouterRegistryProvider(settings, {
4106
+ kind: "openrouter-organization",
4107
+ customModels,
4108
+ });
4109
+ return provider
4110
+ ? { ...settings, modelProvidersJson: JSON.stringify([...providers, provider]) }
4111
+ : settings;
4112
+ }
4113
+
4114
+ export function withOrganizationOpenRouterCredential(
4115
+ settings: Settings,
4116
+ apiKey: string,
4117
+ customModels: readonly { upstreamModelId: string; label?: string | null }[] = [],
4118
+ ): Settings {
4119
+ if (!apiKey.trim()) throw new Error("organization OpenRouter credential is empty");
4120
+ const catalog = withOrganizationOpenRouterCatalogProvider(settings, customModels);
4121
+ const providers = parseModelProvidersJson(catalog.modelProvidersJson).map((provider) =>
4122
+ provider.id === ORGANIZATION_OPENROUTER_PROVIDER_ID ? { ...provider, apiKey } : provider,
4123
+ );
4124
+ return { ...catalog, modelProvidersJson: JSON.stringify(providers) };
4125
+ }
4126
+
3270
4127
  /** OpenAI GPT-5.6 Fast mode is 2× Standard list rates (service_tier fast/priority). */
3271
4128
  const GPT56_FAST_BILLING_MULTIPLIER_BPS = 20_000;
3272
4129
 
@@ -3466,33 +4323,71 @@ function assertLatencyModeRunnable(
3466
4323
  }
3467
4324
  }
3468
4325
 
3469
- function registryCredentialSource(provider: RegistryProvider): CredentialSourceV1 {
3470
- if (provider.kind === "anonymous") {
3471
- return { kind: "deployment", mechanism: "none" };
3472
- }
3473
- if (provider.kind === "codex-subscription") {
3474
- return { kind: "connected_subscription", provider: "codex" };
3475
- }
3476
- if (provider.kind === "xai-subscription") {
3477
- return { kind: "connected_subscription", provider: "xai" };
3478
- }
3479
- if (provider.kind === "vercel-gateway-workspace") {
3480
- return { kind: "workspace_connection", mechanism: "api_key" };
4326
+ function registryCredentialSource(provider: InternalRegistryProvider): CredentialSourceV1 {
4327
+ switch (provider.kind) {
4328
+ case "anonymous":
4329
+ return { kind: "deployment", mechanism: "none" };
4330
+ case "codex-subscription":
4331
+ return { kind: "connected_subscription", provider: "codex" };
4332
+ case "xai-subscription":
4333
+ return { kind: "connected_subscription", provider: "xai" };
4334
+ case "vercel-gateway-workspace":
4335
+ case "openrouter-workspace":
4336
+ return { kind: "workspace_connection", mechanism: "api_key" };
4337
+ case "vercel-gateway-organization":
4338
+ case "openrouter-organization":
4339
+ return { kind: "organization_connection", mechanism: "api_key" };
4340
+ case "api-key":
4341
+ case "vercel-gateway-managed":
4342
+ case "openrouter-managed":
4343
+ return { kind: "deployment", mechanism: "api_key" };
4344
+ default: {
4345
+ const _exhaustive: never = provider.kind;
4346
+ return _exhaustive;
4347
+ }
3481
4348
  }
3482
- return { kind: "deployment", mechanism: "api_key" };
3483
4349
  }
3484
4350
 
3485
- function registryBilling(provider: RegistryProvider): BillingAttributionV1 {
3486
- if (provider.kind === "anonymous") {
3487
- return { upstreamPayer: "deployment", metering: "external" };
3488
- }
3489
- if (provider.kind === "codex-subscription" || provider.kind === "xai-subscription") {
3490
- return { upstreamPayer: "connected_subscription", metering: "external" };
3491
- }
3492
- if (provider.kind === "vercel-gateway-workspace") {
3493
- return { upstreamPayer: "workspace", metering: "external" };
4351
+ function registryBilling(provider: InternalRegistryProvider): BillingAttributionV1 {
4352
+ switch (provider.kind) {
4353
+ case "anonymous":
4354
+ case "openrouter-managed":
4355
+ return { upstreamPayer: "deployment", metering: "external" };
4356
+ case "codex-subscription":
4357
+ case "xai-subscription":
4358
+ return { upstreamPayer: "connected_subscription", metering: "external" };
4359
+ case "vercel-gateway-workspace":
4360
+ case "openrouter-workspace":
4361
+ return { upstreamPayer: "workspace", metering: "external" };
4362
+ case "vercel-gateway-organization":
4363
+ case "openrouter-organization":
4364
+ return { upstreamPayer: "organization", metering: "external" };
4365
+ case "api-key":
4366
+ case "vercel-gateway-managed":
4367
+ return { upstreamPayer: "deployment", metering: "opengeni_credits" };
4368
+ default: {
4369
+ const _exhaustive: never = provider.kind;
4370
+ return _exhaustive;
4371
+ }
3494
4372
  }
3495
- return { upstreamPayer: "deployment", metering: "opengeni_credits" };
4373
+ }
4374
+
4375
+ function configuredCostForModel(
4376
+ settings: Settings,
4377
+ productModelId: string,
4378
+ credentialSource: CredentialSourceV1,
4379
+ ): ConfiguredModelCostClass {
4380
+ if (credentialSource.kind === "workspace_connection") return "workspace";
4381
+ if (credentialSource.kind === "organization_connection") return "organization";
4382
+ if (credentialSource.kind === "connected_subscription") return "subscription";
4383
+ return parseModelCostPolicyJson(settings.modelCostPolicyJson)[productModelId] ?? "credits";
4384
+ }
4385
+
4386
+ export function modelCostClassForConfiguredModel(
4387
+ _settings: Settings,
4388
+ model: Pick<ConfiguredModel, "cost">,
4389
+ ): ConfiguredModelCostClass {
4390
+ return model.cost;
3496
4391
  }
3497
4392
 
3498
4393
  function builtinCredentialSource(settings: Settings): CredentialSourceV1 {
@@ -3578,6 +4473,10 @@ function definitionVersionFor(
3578
4473
  billing: model.billing,
3579
4474
  executionLimits: model.executionLimits,
3580
4475
  capabilities: model.capabilities,
4476
+ // Workspace-facing free/credits classification is a separate live
4477
+ // deployment policy. Operators must drain/fence accepted turns before
4478
+ // changing it; it is intentionally not a second executable-definition
4479
+ // freeze inside TurnExecutionPolicyV1.
3581
4480
  ...(model.requestPolicy ? { requestPolicy: model.requestPolicy } : {}),
3582
4481
  pricing: model.pricing ?? null,
3583
4482
  });
@@ -3593,7 +4492,9 @@ function legacyImplicitOpenAiDefinitionVersionFor(
3593
4492
  ): string | null {
3594
4493
  if (provider.wireProfile !== "openai") return null;
3595
4494
  const { definitionVersion: _definitionVersion, ...modelWithoutVersion } = model;
3596
- return definitionVersionFor(modelWithoutVersion, provider, { includeWireProfile: false });
4495
+ return definitionVersionFor(modelWithoutVersion, provider, {
4496
+ includeWireProfile: false,
4497
+ });
3597
4498
  }
3598
4499
 
3599
4500
  /**
@@ -3619,7 +4520,10 @@ function builtinProviderLabel(settings: Pick<Settings, "openaiProvider">): strin
3619
4520
  * registry entry for the rest. Registry ids may not collide with the built-in
3620
4521
  * id — validateSettings rejects that at boot.
3621
4522
  */
3622
- export function configuredProviders(settings: Settings): ResolvedModelProvider[] {
4523
+ export function configuredProviders(
4524
+ settings: Settings,
4525
+ source: NodeJS.ProcessEnv = process.env,
4526
+ ): ResolvedModelProvider[] {
3623
4527
  const credentialSource = builtinCredentialSource(settings);
3624
4528
  const builtin: ResolvedModelProvider = {
3625
4529
  id: builtinProviderId(settings),
@@ -3650,7 +4554,7 @@ export function configuredProviders(settings: Settings): ResolvedModelProvider[]
3650
4554
  wireProfile: provider.wireProfile,
3651
4555
  builtin: false,
3652
4556
  baseUrl: provider.baseUrl,
3653
- apiKey: resolveProviderApiKey(provider),
4557
+ apiKey: resolveProviderApiKey(provider, source),
3654
4558
  defaultQuery: provider.defaultQuery,
3655
4559
  defaultHeaders: provider.defaultHeaders,
3656
4560
  publicDefaultQueryNames: provider.publicDefaultQueryNames,
@@ -3750,8 +4654,14 @@ export function withXaiSubscriptionCatalogProvider(settings: Settings): Settings
3750
4654
  { id: "standard", upstream: "supported", runnable: true },
3751
4655
  { id: "fast", upstream: "supported", runnable: true },
3752
4656
  ];
3753
- capabilities.hostedTools.xSearch = { upstream: "supported", runnable: true };
3754
- capabilities.hostedTools.imageGeneration = { upstream: "supported", runnable: true };
4657
+ capabilities.hostedTools.xSearch = {
4658
+ upstream: "supported",
4659
+ runnable: true,
4660
+ };
4661
+ capabilities.hostedTools.imageGeneration = {
4662
+ upstream: "supported",
4663
+ runnable: true,
4664
+ };
3755
4665
  return {
3756
4666
  id: `${XAI_SUBSCRIPTION_MODEL_ID_PREFIX}${slug}`,
3757
4667
  upstreamModelId: slug,
@@ -3769,7 +4679,10 @@ export function withXaiSubscriptionCatalogProvider(settings: Settings): Settings
3769
4679
  };
3770
4680
  }),
3771
4681
  };
3772
- return { ...settings, modelProvidersJson: JSON.stringify([...providers, provider]) };
4682
+ return {
4683
+ ...settings,
4684
+ modelProvidersJson: JSON.stringify([...providers, provider]),
4685
+ };
3773
4686
  }
3774
4687
 
3775
4688
  /**
@@ -3796,6 +4709,9 @@ export function policyProviderIdForModel(settings: Settings, modelId: string): s
3796
4709
  if (canonicalModelId.startsWith(WORKSPACE_GATEWAY_MODEL_ID_PREFIX)) {
3797
4710
  return WORKSPACE_GATEWAY_PROVIDER_ID;
3798
4711
  }
4712
+ if (canonicalModelId.startsWith(WORKSPACE_OPENROUTER_MODEL_ID_PREFIX)) {
4713
+ return WORKSPACE_OPENROUTER_PROVIDER_ID;
4714
+ }
3799
4715
  const configured = configuredModels(settings).find((model) => model.id === canonicalModelId);
3800
4716
  return configured?.providerId ?? builtinProviderId(settings);
3801
4717
  }
@@ -3823,15 +4739,21 @@ function resolvedExecutionLimits(
3823
4739
  function finalizeConfiguredModel(
3824
4740
  settings: Settings,
3825
4741
  provider: ResolvedModelProvider,
3826
- input: Omit<ConfiguredModel, "schemaVersion" | "definitionVersion" | "executionLimits">,
4742
+ input: Omit<ConfiguredModel, "schemaVersion" | "definitionVersion" | "executionLimits" | "cost">,
3827
4743
  ): ConfiguredModel {
3828
4744
  const requestPolicy =
3829
- provider.kind === "vercel-gateway-managed" || provider.kind === "vercel-gateway-workspace"
3830
- ? gatewayRequestPolicyForUpstreamModel(input.upstreamModelId)
4745
+ provider.kind === "vercel-gateway-managed" ||
4746
+ provider.kind === "vercel-gateway-workspace" ||
4747
+ provider.kind === "vercel-gateway-organization"
4748
+ ? gatewayRequestPolicyForUpstreamModel(
4749
+ input.upstreamModelId,
4750
+ configuredGatewayCatalogModels(settings),
4751
+ )
3831
4752
  : undefined;
3832
4753
  const modelWithoutVersion: Omit<ConfiguredModel, "definitionVersion"> = {
3833
4754
  schemaVersion: 1,
3834
4755
  ...input,
4756
+ cost: configuredCostForModel(settings, input.id, input.credentialSource),
3835
4757
  ...(requestPolicy ? { requestPolicy } : {}),
3836
4758
  executionLimits: resolvedExecutionLimits(settings, input),
3837
4759
  };
@@ -3882,10 +4804,13 @@ function assertUniqueModelIdentities(models: ConfiguredModel[]): void {
3882
4804
  * default false). De-duplicated by id (first wins) so the default model stays
3883
4805
  * first and the built-in allow-list takes precedence over registry entries.
3884
4806
  */
3885
- export function configuredModels(settings: Settings): ConfiguredModel[] {
4807
+ export function configuredModels(
4808
+ settings: Settings,
4809
+ source: NodeJS.ProcessEnv = process.env,
4810
+ ): ConfiguredModel[] {
3886
4811
  const builtinId = builtinProviderId(settings);
3887
4812
  const builtinLabel = builtinProviderLabel(settings);
3888
- const providers = configuredProviders(settings);
4813
+ const providers = configuredProviders(settings, source);
3889
4814
  const providerById = new Map(providers.map((provider) => [provider.id, provider]));
3890
4815
  const pricingSchedules = configuredModelPricingSchedules(settings);
3891
4816
  // The built-in (OpenAI/Azure) provider must NEVER claim a registry-namespaced
@@ -4016,7 +4941,12 @@ export function configuredModels(settings: Settings): ConfiguredModel[] {
4016
4941
  }
4017
4942
  }
4018
4943
  assertUniqueModelIdentities(out);
4019
- return out;
4944
+ const defaultIndex = out.findIndex(
4945
+ (model) => model.id === settings.openaiModel || model.aliases.includes(settings.openaiModel),
4946
+ );
4947
+ return defaultIndex > 0
4948
+ ? [out[defaultIndex]!, ...out.slice(0, defaultIndex), ...out.slice(defaultIndex + 1)]
4949
+ : out;
4020
4950
  }
4021
4951
 
4022
4952
  /** Resolve a known canonical id or alias. Unknown strings are returned unchanged. */
@@ -4087,11 +5017,50 @@ function settingsForTurnExecutionPolicy(settings: Settings, modelId: string): Se
4087
5017
  return withXaiSubscriptionCatalogProvider(settings);
4088
5018
  }
4089
5019
  if (modelId.startsWith(WORKSPACE_GATEWAY_MODEL_ID_PREFIX)) {
5020
+ // API/worker workspace boundaries may already have overlaid durable custom
5021
+ // rows and, at execution time, the decrypted workspace key. Re-applying an
5022
+ // empty static overlay would silently discard both. Only synthesize the
5023
+ // curated fallback when this exact model is not already executable.
5024
+ if (resolveModelProvider(settings, modelId)) {
5025
+ return settings;
5026
+ }
4090
5027
  return withWorkspaceGatewayCatalogProvider(settings);
4091
5028
  }
5029
+ if (modelId.startsWith(WORKSPACE_OPENROUTER_MODEL_ID_PREFIX)) {
5030
+ if (resolveModelProvider(settings, modelId)) {
5031
+ return settings;
5032
+ }
5033
+ return withWorkspaceOpenRouterCatalogProvider(settings);
5034
+ }
5035
+ if (modelId.startsWith(ORGANIZATION_GATEWAY_MODEL_ID_PREFIX)) {
5036
+ return resolveModelProvider(settings, modelId)
5037
+ ? settings
5038
+ : withOrganizationGatewayCatalogProvider(settings);
5039
+ }
5040
+ if (modelId.startsWith(ORGANIZATION_OPENROUTER_MODEL_ID_PREFIX)) {
5041
+ return resolveModelProvider(settings, modelId)
5042
+ ? settings
5043
+ : withOrganizationOpenRouterCatalogProvider(settings);
5044
+ }
4092
5045
  return settings;
4093
5046
  }
4094
5047
 
5048
+ /**
5049
+ * Resolve the static catalog identity used by an accepted turn. Subscription
5050
+ * overlays contain no account or bearer and do not prove connection readiness;
5051
+ * callers must keep their live credential/readiness gate authoritative.
5052
+ */
5053
+ export function resolveModelProviderForTurn(
5054
+ settings: Settings,
5055
+ modelId: string,
5056
+ ): ReturnType<typeof resolveModelProvider> {
5057
+ const catalogSettings = settingsForTurnExecutionPolicy(settings, modelId);
5058
+ return resolveModelProvider(
5059
+ catalogSettings,
5060
+ canonicalizeConfiguredModelId(catalogSettings, modelId),
5061
+ );
5062
+ }
5063
+
4095
5064
  /**
4096
5065
  * Build a trusted, secret-safe execution policy from the normalized catalog.
4097
5066
  * The Codex overlay here contains static product/provider identity only; it
@@ -4203,10 +5172,9 @@ export function assertTurnExecutionPolicyMatchesConfigV1(
4203
5172
 
4204
5173
  /**
4205
5174
  * Effective per-model pricing schedules. Merge order (later wins): built-in
4206
- * flat defaults → registry model flat/scheduled pricing → explicit legacy flat
4207
- * OPENGENI_MODEL_PRICING_JSON. The explicit legacy map intentionally replaces
4208
- * a registry schedule with one flat default so its historical precedence stays
4209
- * exact.
5175
+ * flat defaults → registry model flat/scheduled pricing → explicit
5176
+ * OPENGENI_MODEL_PRICING_JSON. Explicit entries may be legacy flat prices or a
5177
+ * complete schedule, and always replace the lower-precedence schedule.
4210
5178
  */
4211
5179
  export function configuredModelPricingSchedules(
4212
5180
  settings: Settings,
@@ -4228,7 +5196,7 @@ export function configuredModelPricingSchedules(
4228
5196
  const configured = Object.fromEntries(
4229
5197
  Object.entries(parseModelPricingJson(settings.modelPricingJson)).map(([model, pricing]) => [
4230
5198
  model,
4231
- { default: pricing },
5199
+ normalizeModelPricingSchedule(pricing),
4232
5200
  ]),
4233
5201
  );
4234
5202
  return {
@@ -4409,6 +5377,37 @@ export function calculateGatewayReportedCostMicros(
4409
5377
  .creditCostMicros;
4410
5378
  }
4411
5379
 
5380
+ type GatewayReportedCostDecimal = {
5381
+ providerNumerator: bigint;
5382
+ decimalScale: bigint;
5383
+ providerCostMicros: number;
5384
+ };
5385
+
5386
+ function parseGatewayReportedCostDecimal(inferenceCostUsd: string): GatewayReportedCostDecimal {
5387
+ const match = /^(0|[1-9]\d*)(?:\.(\d{1,18}))?$/.exec(inferenceCostUsd);
5388
+ if (!match) {
5389
+ throw new Error("Invalid AI Gateway inference cost");
5390
+ }
5391
+ const fraction = match[2] ?? "";
5392
+ const decimalDigits = BigInt(`${match[1]}${fraction}`);
5393
+ const decimalScale = 10n ** BigInt(fraction.length);
5394
+ const providerNumerator = decimalDigits * 1_000_000n;
5395
+ const providerCostMicros = (providerNumerator + decimalScale - 1n) / decimalScale;
5396
+ if (providerCostMicros > BigInt(Number.MAX_SAFE_INTEGER)) {
5397
+ throw new Error("AI Gateway inference cost exceeds the supported billing range");
5398
+ }
5399
+ return {
5400
+ providerNumerator,
5401
+ decimalScale,
5402
+ providerCostMicros: Number(providerCostMicros),
5403
+ };
5404
+ }
5405
+
5406
+ /** Exact provider-reported Gateway cost without requiring an OpenGeni price schedule. */
5407
+ export function calculateGatewayReportedProviderCostMicros(inferenceCostUsd: string): number {
5408
+ return parseGatewayReportedCostDecimal(inferenceCostUsd).providerCostMicros;
5409
+ }
5410
+
4412
5411
  export function calculateGatewayReportedCostBreakdown(
4413
5412
  settings: Settings,
4414
5413
  model: string,
@@ -4420,27 +5419,17 @@ export function calculateGatewayReportedCostBreakdown(
4420
5419
  throw new Error(`Missing model pricing for ${model}`);
4421
5420
  }
4422
5421
  const pricing = selectModelPricing(schedule, positiveInt(options?.inputTokens));
4423
- const match = /^(0|[1-9]\d*)(?:\.(\d{1,18}))?$/.exec(inferenceCostUsd);
4424
- if (!match) {
4425
- throw new Error("Invalid AI Gateway inference cost");
4426
- }
4427
- const fraction = match[2] ?? "";
4428
- const decimalDigits = BigInt(`${match[1]}${fraction}`);
4429
- const decimalScale = 10n ** BigInt(fraction.length);
4430
- const providerNumerator = decimalDigits * 1_000_000n;
4431
- const providerMicros = (providerNumerator + decimalScale - 1n) / decimalScale;
5422
+ const { providerNumerator, decimalScale, providerCostMicros } =
5423
+ parseGatewayReportedCostDecimal(inferenceCostUsd);
4432
5424
  const marginBps = BigInt(10_000 + (pricing.marginBps ?? 0));
4433
5425
  const numerator = providerNumerator * marginBps;
4434
5426
  const denominator = decimalScale * 10_000n;
4435
5427
  const creditMicros = (numerator + denominator - 1n) / denominator;
4436
- if (
4437
- providerMicros > BigInt(Number.MAX_SAFE_INTEGER) ||
4438
- creditMicros > BigInt(Number.MAX_SAFE_INTEGER)
4439
- ) {
5428
+ if (creditMicros > BigInt(Number.MAX_SAFE_INTEGER)) {
4440
5429
  throw new Error("AI Gateway inference cost exceeds the supported billing range");
4441
5430
  }
4442
5431
  return {
4443
- providerCostMicros: Number(providerMicros),
5432
+ providerCostMicros,
4444
5433
  creditCostMicros: Number(creditMicros),
4445
5434
  };
4446
5435
  }
@@ -4890,7 +5879,9 @@ export function parseMcpServers(raw: string | undefined): unknown[] | undefined
4890
5879
  }
4891
5880
  }
4892
5881
 
4893
- export function parseModelPricingJson(raw: string): Record<string, ModelPricing> {
5882
+ export function parseModelPricingJson(
5883
+ raw: string,
5884
+ ): Record<string, ModelPricing | ModelPricingScheduleV1> {
4894
5885
  if (!raw.trim() || raw.trim() === "{}") {
4895
5886
  return {};
4896
5887
  }
@@ -4904,12 +5895,12 @@ export function parseModelPricingJson(raw: string): Record<string, ModelPricing>
4904
5895
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
4905
5896
  throw new Error("OPENGENI_MODEL_PRICING_JSON must be a JSON object keyed by model name");
4906
5897
  }
4907
- const out: Record<string, ModelPricing> = {};
5898
+ const out: Record<string, ModelPricing | ModelPricingScheduleV1> = {};
4908
5899
  for (const [model, value] of Object.entries(parsed)) {
4909
5900
  if (!model.trim()) {
4910
5901
  throw new Error("OPENGENI_MODEL_PRICING_JSON contains an empty model name");
4911
5902
  }
4912
- out[model] = ModelPricingSchema.parse(value);
5903
+ out[model] = z.union([ModelPricingSchema, ModelPricingScheduleSchema]).parse(value);
4913
5904
  }
4914
5905
  return out;
4915
5906
  }
@@ -5120,12 +6111,19 @@ function calculateEntryCostMicros(pricing: ModelPricing, entry: ModelUsageInput)
5120
6111
  const inputTokens = positiveInt(entry.inputTokens);
5121
6112
  const outputTokens = positiveInt(entry.outputTokens);
5122
6113
  const cachedTokens = Math.min(inputTokens, cachedInputTokens(entry));
5123
- const uncachedInputTokens = Math.max(0, inputTokens - cachedTokens);
6114
+ const cacheWriteTokens = Math.min(
6115
+ Math.max(0, inputTokens - cachedTokens),
6116
+ cacheWriteInputTokens(entry),
6117
+ );
6118
+ const uncachedInputTokens = Math.max(0, inputTokens - cachedTokens - cacheWriteTokens);
5124
6119
  const cachedInputRate =
5125
6120
  pricing.cachedInputMicrosPerMillionTokens ?? pricing.inputMicrosPerMillionTokens;
6121
+ const cacheWriteRate =
6122
+ pricing.cacheWriteMicrosPerMillionTokens ?? pricing.inputMicrosPerMillionTokens;
5126
6123
  return (
5127
6124
  Math.ceil((uncachedInputTokens * pricing.inputMicrosPerMillionTokens) / 1_000_000) +
5128
6125
  Math.ceil((cachedTokens * cachedInputRate) / 1_000_000) +
6126
+ Math.ceil((cacheWriteTokens * cacheWriteRate) / 1_000_000) +
5129
6127
  Math.ceil((outputTokens * pricing.outputMicrosPerMillionTokens) / 1_000_000)
5130
6128
  );
5131
6129
  }
@@ -5146,6 +6144,19 @@ function cachedInputTokens(entry: ModelUsageInput): number {
5146
6144
  return total;
5147
6145
  }
5148
6146
 
6147
+ function cacheWriteInputTokens(entry: ModelUsageInput): number {
6148
+ const details = Array.isArray(entry.inputTokensDetails)
6149
+ ? entry.inputTokensDetails
6150
+ : entry.inputTokensDetails
6151
+ ? [entry.inputTokensDetails]
6152
+ : [];
6153
+ let total = 0;
6154
+ for (const detail of details) {
6155
+ total += positiveInt(detail.cache_write_tokens ?? detail.cacheWriteTokens);
6156
+ }
6157
+ return total;
6158
+ }
6159
+
5149
6160
  function positiveInt(value: unknown): number {
5150
6161
  return typeof value === "number" && Number.isFinite(value) && value > 0 ? Math.floor(value) : 0;
5151
6162
  }
@@ -5314,7 +6325,7 @@ function isDigestPinnedModalDesktopImage(settings: Settings): boolean {
5314
6325
  );
5315
6326
  }
5316
6327
 
5317
- function validateSettings(settings: Settings): void {
6328
+ function validateSettings(settings: Settings, source: NodeJS.ProcessEnv = process.env): void {
5318
6329
  temporalConnectionOptions(settings);
5319
6330
  if (settings.goalIdleBackoffMs.some((delayMs) => delayMs > settings.goalIdleBackoffMaxMs)) {
5320
6331
  throw new Error(
@@ -5620,15 +6631,6 @@ function validateSettings(settings: Settings): void {
5620
6631
  if (settings.productAccessMode !== "managed" && settings.billingMode === "stripe") {
5621
6632
  throw new Error("OPENGENI_BILLING_MODE=stripe requires OPENGENI_PRODUCT_ACCESS_MODE=managed");
5622
6633
  }
5623
- if (settings.billingMode === "stripe" || settings.usageLimitsMode === "managed") {
5624
- const pricing = configuredModelPricing(settings);
5625
- const missing = configuredAllowedModels(settings).filter((model) => !pricing[model]);
5626
- if (missing.length > 0) {
5627
- throw new Error(
5628
- `Missing model pricing for managed billing model(s): ${missing.join(", ")}. Set OPENGENI_MODEL_PRICING_JSON.`,
5629
- );
5630
- }
5631
- }
5632
6634
  if (settings.usageLimitsMode === "static") {
5633
6635
  const limits = configuredStaticUsageLimits(settings);
5634
6636
  if (Object.keys(limits).length === 0) {
@@ -5819,6 +6821,14 @@ function validateSettings(settings: Settings): void {
5819
6821
  throw new Error(`OPENGENI_MCP_SERVERS contains duplicate id ${server.id}`);
5820
6822
  }
5821
6823
  serverIds.add(server.id);
6824
+ if (
6825
+ server.connectionRef?.authoritySource === "host" &&
6826
+ !settings.hostMcpAuthoritySourceAdmissionEnabled
6827
+ ) {
6828
+ throw new Error(
6829
+ "OPENGENI_MCP_SERVERS host-owned connection refs require OPENGENI_HOST_MCP_AUTHORITY_SOURCE_ADMISSION_ENABLED=true after the whole API/worker fleet is upgraded",
6830
+ );
6831
+ }
5822
6832
  }
5823
6833
  // --- sandbox lease cadence invariant (fail fast at boot) ---
5824
6834
  // Holder TTLs are provider-neutral. Modal's finite hard/idle clocks and
@@ -5842,11 +6852,46 @@ function validateSettings(settings: Settings): void {
5842
6852
  `more often than the controller-heartbeat horizon.`,
5843
6853
  );
5844
6854
  }
6855
+ if (settings.sandboxDrainSnapshotTimeoutMs !== undefined) {
6856
+ const drainCaptureTimeoutMs = sandboxArchiveCaptureTimeoutMs({
6857
+ sandboxSnapshotTimeoutMs: effectiveSandboxDrainSnapshotTimeoutMs(settings),
6858
+ });
6859
+ const requiredTransitionWaitMs =
6860
+ reaperPeriod + drainCaptureTimeoutMs + SANDBOX_LIFECYCLE_RETRY_HANDOFF_GRACE_MS;
6861
+ if (requiredTransitionWaitMs > SANDBOX_LIFECYCLE_TRANSITION_MAX_WAIT_MS) {
6862
+ throw new Error(
6863
+ `OPENGENI_SANDBOX_DRAIN_SNAPSHOT_TIMEOUT_MS (${settings.sandboxDrainSnapshotTimeoutMs}) ` +
6864
+ `requires a sandbox lifecycle transition wait of ${requiredTransitionWaitMs}ms after ` +
6865
+ `one reaper period and provider settlement, exceeding the ` +
6866
+ `${SANDBOX_LIFECYCLE_TRANSITION_MAX_WAIT_MS}ms limit. Lower the drain snapshot timeout ` +
6867
+ `or OPENGENI_SANDBOX_LEASE_REAPER_PERIOD_MS.`,
6868
+ );
6869
+ }
6870
+ }
6871
+ // A backend rollout does not rewrite or synchronously drain existing
6872
+ // leases. Preserve enough deadline-rotation headroom for historical Modal
6873
+ // leases even when the deployment default has moved to another backend.
6874
+ const rotationLeadMs = settings.sandboxRotationLeadMs;
6875
+ const ordinaryCaptureTimeoutMs = sandboxArchiveCaptureTimeoutMs(settings);
6876
+ const drainCaptureTimeoutMs = sandboxArchiveCaptureTimeoutMs({
6877
+ sandboxSnapshotTimeoutMs: effectiveSandboxDrainSnapshotTimeoutMs(settings),
6878
+ });
6879
+ const providerDeadlineCaptureTimeoutMs = Math.max(
6880
+ ordinaryCaptureTimeoutMs,
6881
+ drainCaptureTimeoutMs,
6882
+ );
6883
+ if (!(rotationLeadMs > providerDeadlineCaptureTimeoutMs + reaperPeriod)) {
6884
+ throw new Error(
6885
+ `OPENGENI_SANDBOX_ROTATION_LEAD_MS (${rotationLeadMs}) must exceed the ` +
6886
+ `largest durable snapshot or drain capture timeout plus one reaper period ` +
6887
+ `(${providerDeadlineCaptureTimeoutMs + reaperPeriod}), including for persisted Modal ` +
6888
+ `leases after a default-backend rollout.`,
6889
+ );
6890
+ }
5845
6891
  if (settings.sandboxBackend === "modal") {
5846
6892
  const idleGraceMs = settings.sandboxIdleGraceMs;
5847
6893
  const lifecycle = effectiveSandboxLifecycle(settings, "modal");
5848
6894
  const providerLifetimeMs = lifecycle.hardLifetimeMs!;
5849
- const rotationLeadMs = lifecycle.rotationLeadMs!;
5850
6895
  const idleTimeoutMs = lifecycle.providerIdleTimeoutMs!;
5851
6896
  if (!(idleTimeoutMs <= providerLifetimeMs)) {
5852
6897
  throw new Error(
@@ -5861,13 +6906,6 @@ function validateSettings(settings: Settings): void {
5861
6906
  `OPENGENI_MODAL_TIMEOUT_SECONDS*1000 (${providerLifetimeMs}).`,
5862
6907
  );
5863
6908
  }
5864
- const captureTimeoutMs = sandboxArchiveCaptureTimeoutMs(settings);
5865
- if (!(rotationLeadMs > captureTimeoutMs + reaperPeriod)) {
5866
- throw new Error(
5867
- `OPENGENI_SANDBOX_ROTATION_LEAD_MS (${rotationLeadMs}) must exceed the durable capture ` +
5868
- `timeout plus one reaper period (${captureTimeoutMs + reaperPeriod}).`,
5869
- );
5870
- }
5871
6909
  if (!(viewerTtl < idleTimeoutMs)) {
5872
6910
  throw new Error(
5873
6911
  `OPENGENI_SANDBOX_VIEWER_HOLDER_TTL_MS (${viewerTtl}) must be strictly less than the effective box ` +
@@ -5925,15 +6963,23 @@ function validateSettings(settings: Settings): void {
5925
6963
  "OPENGENI_STREAM_TOKEN_SECRET to enable the live desktop stream.",
5926
6964
  );
5927
6965
  }
5928
- // Model provider registry: parse it here so JSON/zod errors surface at boot,
5929
- // reject a registry id colliding with the built-in provider id (it would
5930
- // shadow the built-in in configuredProviders), reject duplicate registry
5931
- // ids, and preserve the existing key requirement for every registry provider
5932
- // except connected Codex and the explicit anonymous opt-in. Anonymous
5933
- // providers are externally metered; a missing key on every other ordinary
5934
- // provider remains a boot error.
5935
- // Registry models flow through configuredAllowedModels, so the managed-billing
5936
- // pricing check above already covers the OpenGeni-credit providers.
6966
+ if (settings.modelCatalogSource === "code") {
6967
+ validateModelCatalogSettings(settings, source);
6968
+ } else {
6969
+ // Database mode resolves membership asynchronously. Only the independent
6970
+ // deployment funding JSON is parsed here; env catalog and note inputs are
6971
+ // intentionally ignored until resolveCatalogSettings applies the singleton.
6972
+ parseModelCostPolicyJson(settings.modelCostPolicyJson);
6973
+ }
6974
+ }
6975
+
6976
+ /** Validate one fully resolved, secret-bearing executable catalog. */
6977
+ export function validateModelCatalogSettings(
6978
+ settings: Settings,
6979
+ source: NodeJS.ProcessEnv = process.env,
6980
+ ): ConfiguredModel[] {
6981
+ const costPolicy = parseModelCostPolicyJson(settings.modelCostPolicyJson);
6982
+ const notes = parseModelNotesJson(settings.modelNotesJson);
5937
6983
  const registryProviders = parseModelProvidersJson(settings.modelProvidersJson);
5938
6984
  const builtinId = builtinProviderId(settings);
5939
6985
  const providerIds = new Set<string>();
@@ -5941,12 +6987,20 @@ function validateSettings(settings: Settings): void {
5941
6987
  if (
5942
6988
  provider.kind === "vercel-gateway-managed" ||
5943
6989
  provider.kind === "vercel-gateway-workspace" ||
6990
+ provider.kind === "vercel-gateway-organization" ||
6991
+ provider.kind === "openrouter-workspace" ||
6992
+ provider.kind === "openrouter-organization" ||
5944
6993
  provider.kind === "xai-subscription"
5945
6994
  ) {
5946
6995
  throw new Error(
5947
6996
  `OPENGENI_MODEL_PROVIDERS_JSON provider kind ${provider.kind} is reserved for a reviewed OpenGeni credential broker`,
5948
6997
  );
5949
6998
  }
6999
+ if (RESERVED_MODEL_PROVIDER_IDS.has(provider.id)) {
7000
+ throw new Error(
7001
+ `OPENGENI_MODEL_PROVIDERS_JSON provider id ${provider.id} is reserved for a reviewed OpenGeni provider`,
7002
+ );
7003
+ }
5950
7004
  if (provider.id === builtinId) {
5951
7005
  throw new Error(
5952
7006
  `OPENGENI_MODEL_PROVIDERS_JSON provider id ${provider.id} collides with the built-in provider id`,
@@ -5961,7 +7015,7 @@ function validateSettings(settings: Settings): void {
5961
7015
  if (
5962
7016
  provider.kind !== "codex-subscription" &&
5963
7017
  provider.kind !== "anonymous" &&
5964
- !resolveProviderApiKey(provider)
7018
+ !resolveProviderApiKey(provider, source)
5965
7019
  ) {
5966
7020
  throw new Error(
5967
7021
  `OPENGENI_MODEL_PROVIDERS_JSON provider ${provider.id} requires a resolvable API key (set apiKey or apiKeyEnv)`,
@@ -5971,7 +7025,65 @@ function validateSettings(settings: Settings): void {
5971
7025
  // Materialize the normalized catalog at boot so canonical product ids,
5972
7026
  // aliases, definition digests, and capability/pricing normalization are
5973
7027
  // validated even when managed billing is disabled.
5974
- configuredModels(settings);
7028
+ const models = configuredModels(settings, source);
7029
+ const defaultCatalogSettings = settingsForTurnExecutionPolicy(settings, settings.openaiModel);
7030
+ const defaultCatalogModels =
7031
+ defaultCatalogSettings === settings ? models : configuredModels(defaultCatalogSettings, source);
7032
+ if (models.length === 0 && defaultCatalogModels.length === 0) {
7033
+ throw new Error("The resolved model catalog contains no executable models");
7034
+ }
7035
+ const defaultModelId = canonicalizeConfiguredModelId(
7036
+ defaultCatalogSettings,
7037
+ settings.openaiModel,
7038
+ );
7039
+ if (!defaultCatalogModels.some((model) => model.id === defaultModelId)) {
7040
+ throw new Error(
7041
+ `The default model ${settings.openaiModel} is not executable in the resolved model catalog`,
7042
+ );
7043
+ }
7044
+
7045
+ const deploymentProductIds = new Set(
7046
+ models.filter((model) => model.credentialSource.kind === "deployment").map((model) => model.id),
7047
+ );
7048
+ const noteProductIds = new Set(models.map((model) => model.id));
7049
+ for (const model of configuredGatewayCatalogModels(settings)) {
7050
+ deploymentProductIds.add(model.productId);
7051
+ noteProductIds.add(model.productId);
7052
+ noteProductIds.add(model.workspaceProductId);
7053
+ }
7054
+ for (const model of configuredOpenRouterCatalogModels(settings)) {
7055
+ const productId = `${OPENROUTER_MODEL_ID_PREFIX}${model.upstreamModelId}`;
7056
+ deploymentProductIds.add(productId);
7057
+ noteProductIds.add(productId);
7058
+ noteProductIds.add(`${WORKSPACE_OPENROUTER_MODEL_ID_PREFIX}${model.upstreamModelId}`);
7059
+ }
7060
+ if (settings.modelCatalogSource === "code") {
7061
+ for (const productId of Object.keys(costPolicy)) {
7062
+ if (!deploymentProductIds.has(productId)) {
7063
+ throw new Error(
7064
+ `OPENGENI_MODEL_COST_POLICY_JSON references unknown deployment model ${productId}`,
7065
+ );
7066
+ }
7067
+ }
7068
+ }
7069
+ for (const productId of Object.keys(notes)) {
7070
+ if (!noteProductIds.has(productId)) {
7071
+ throw new Error(`OPENGENI_MODEL_NOTES_JSON references unknown catalog model ${productId}`);
7072
+ }
7073
+ }
7074
+
7075
+ if (settings.billingMode === "stripe" || settings.usageLimitsMode === "managed") {
7076
+ const pricing = configuredModelPricing(settings);
7077
+ const missing = models
7078
+ .filter((model) => model.cost === "credits" && !pricing[model.id])
7079
+ .map((model) => model.id);
7080
+ if (missing.length > 0) {
7081
+ throw new Error(
7082
+ `Missing model pricing for managed billing model(s): ${missing.join(", ")}. Set OPENGENI_MODEL_PRICING_JSON.`,
7083
+ );
7084
+ }
7085
+ }
7086
+ return models;
5975
7087
  }
5976
7088
 
5977
7089
  /**