@opengeni/config 0.22.5 → 1.0.0-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({
@@ -260,6 +273,10 @@ export const McpServerConnectionRefSchema = z
260
273
  });
261
274
  export type McpServerConnectionRef = z.infer<typeof McpServerConnectionRefSchema>;
262
275
 
276
+ /** Public, digest-pinned desktop image used by Modal unless the operator overrides it. */
277
+ export const DEFAULT_MODAL_IMAGE_REF =
278
+ "opengenipublicneuacr.azurecr.io/opengeni-desktop@sha256:c3bd17b8841de1bff9bb2777aad422cf8c75de78e2c30f9ac81d0cd6810a1b78";
279
+
263
280
  const SettingsSchema = z.object({
264
281
  serviceName: z.string().default("opengeni"),
265
282
  environment: z.string().default("local"),
@@ -324,6 +341,12 @@ const SettingsSchema = z.object({
324
341
  .regex(/^G-[A-Z0-9]+$/u)
325
342
  .optional(),
326
343
  publicBaseUrl: z.string().url().optional(),
344
+ // Standards-based OAuth authorization server for external workspace MCP
345
+ // clients. Opt-in because it creates a new public authentication surface.
346
+ mcpOauthEnabled: EnvBoolean.default(false),
347
+ // Forwarded client addresses are ignored by default. Operators may trust an
348
+ // exact number of proxy hops only when direct access to the API is blocked.
349
+ mcpOauthTrustedProxyHops: z.coerce.number().int().min(0).max(16).default(0),
327
350
  // Browser origin when the web app and API use separate origins in local
328
351
  // development. Production normally leaves this unset and uses publicBaseUrl.
329
352
  webBaseUrl: z.string().url().optional(),
@@ -502,6 +525,13 @@ const SettingsSchema = z.object({
502
525
  // into @opengeni/db once at boot.
503
526
  // Env: OPENGENI_CHILD_LIFECYCLE_NOTICES_ENABLED.
504
527
  childLifecycleNoticesEnabled: EnvBoolean.default(false),
528
+ // Explicit host-owned MCP connection authority is a rolling protocol
529
+ // activation. Keep it off while any API, worker, or browser bundle predates
530
+ // the authority discriminator; enable it only after the whole fleet runs an
531
+ // image that understands host refs. Legacy markerless non-UUID refs remain a
532
+ // separate compatibility lane for already-persisted embedding integrations.
533
+ // Env: OPENGENI_HOST_MCP_AUTHORITY_SOURCE_ADMISSION_ENABLED.
534
+ hostMcpAuthoritySourceAdmissionEnabled: EnvBoolean.default(false),
505
535
  // Per-channel and per-DM Slack workspace routing. Default ON. A channel does
506
536
  // not count a personal workspace as a candidate, so an organization with one
507
537
  // shared workspace resolves it as the sole candidate and never asks; the
@@ -684,6 +714,24 @@ const SettingsSchema = z.object({
684
714
  // keep subscription model routing while disabling Codex voice input.
685
715
  voiceInputCodexExperimentalEnabled: EnvBoolean.default(false),
686
716
  modelPricingJson: z.string().default("{}"),
717
+ // Supported-model membership source. Database mode is resolved by the async
718
+ // core overlay; getSettings remains synchronous and env-only.
719
+ modelCatalogSource: z.enum(["code", "database"]).default("code"),
720
+ // Deployment-owned workspace-facing price policy. This is deliberately
721
+ // separate from catalog membership and upstream credential ownership.
722
+ // Shape: { "product/model-id": "free" | "credits" }.
723
+ modelCostPolicyJson: z.string().default("{}"),
724
+ // Optional per-product agent guidance. Database mode replaces this with the
725
+ // singleton document's validated modelNotes map.
726
+ modelNotesJson: z.string().default("{}"),
727
+ // Managed OpenRouter credential. The curated model table is injected in
728
+ // code/catalog-document resolution and never read from host provider JSON.
729
+ openrouterApiKey: z.string().optional(),
730
+ // Internal, secret-free catalog overlays populated only by
731
+ // applyModelCatalogDocument. They intentionally have no OPENGENI_* env
732
+ // binding so database mode cannot be bypassed with a second source.
733
+ resolvedGatewayModelsJson: z.string().optional(),
734
+ resolvedOpenRouterModelsJson: z.string().optional(),
687
735
  // Extra (non-built-in) model providers, declared by the host as a JSON
688
736
  // provider registry. Each entry carries its own base URL, API key, wire API
689
737
  // ("responses" | "chat") and the models it exposes. The models a client may
@@ -726,11 +774,6 @@ const SettingsSchema = z.object({
726
774
  // the Codex rollout so an emergency Codex opt-out cannot disable every model.
727
775
  // OPENGENI_LAZY_TOOL_SEARCH_ENABLED
728
776
  lazyToolSearchEnabled: EnvBoolean.default(true),
729
- // credential allocator atomic, workspace-local credential allocation. Default OFF is a
730
- // deliberate rolling-deploy fence: migrate + roll every worker first, then
731
- // enable. Turning it off restores the legacy sticky selector without a schema
732
- // rollback; the additive lease table/cursor columns become inert.
733
- codexCredentialLeasingEnabled: EnvBoolean.default(false),
734
777
  // Decision-observability fence. When enabled, the worker emits one
735
778
  // bounded, metadata-only adaptive-policy replay record alongside the unchanged
736
779
  // sticky-sharded decision. It never changes placement/admission/failover.
@@ -1151,6 +1194,18 @@ const SettingsSchema = z.object({
1151
1194
  .positive()
1152
1195
  .max(SANDBOX_SNAPSHOT_MAX_TIMEOUT_MS)
1153
1196
  .default(60_000),
1197
+ // A zero-holder drain may need substantially longer than a best-effort
1198
+ // mid-turn/turn-end snapshot for a very large workspace. Keep that provider
1199
+ // budget independent so increasing drain recovery headroom cannot pin an
1200
+ // ordinary turn finalizer for the same duration. Unset preserves the legacy
1201
+ // single-budget behavior. Knob:
1202
+ // OPENGENI_SANDBOX_DRAIN_SNAPSHOT_TIMEOUT_MS.
1203
+ sandboxDrainSnapshotTimeoutMs: z.coerce
1204
+ .number()
1205
+ .int()
1206
+ .positive()
1207
+ .max(SANDBOX_SNAPSHOT_MAX_TIMEOUT_MS)
1208
+ .optional(),
1154
1209
  // Begin a controlled snapshot/quiesce/drain/rematerialize transition this far
1155
1210
  // ahead of a finite provider deadline. Modal's 24h creation clock cannot be
1156
1211
  // extended; the logical sandbox outlives it by moving to one successor box.
@@ -1274,6 +1329,15 @@ const SettingsSchema = z.object({
1274
1329
  // Rolling browser login-slot compatibility. Repository/deployment default is
1275
1330
  // deliberately legacy; changing to broker is an operator-authorized rollout.
1276
1331
  managedAuthSessionSetMode: z.enum(["legacy", "dual", "broker"]).default("legacy"),
1332
+ // Query transport is an explicit second-stage rollout. A pre-compatibility
1333
+ // web image understands only fragment bearers, so API replicas must keep
1334
+ // generating fragment links until the compatible web fleet has converged.
1335
+ organizationUserSetupEmailTokenTransport: z.enum(["fragment", "query"]).default("fragment"),
1336
+ // Query-bearing setup links may appear in controller/edge error logs even
1337
+ // when access logs and request tracing are disabled. This explicit operator
1338
+ // confirmation keeps query transport fail closed until that separate sink is
1339
+ // proven sanitized.
1340
+ organizationUserSetupQueryEdgeSanitizationConfirmed: EnvBoolean.default(false),
1277
1341
  resendApiKey: z.string().optional(),
1278
1342
  emailFrom: z.string().default("OpenGeni <auth@mail.opengeni.ai>"),
1279
1343
  stripeSecretKey: z.string().optional(),
@@ -1575,6 +1639,7 @@ export type TemporalConnectionOptions = {
1575
1639
  export type ModelPricing = {
1576
1640
  inputMicrosPerMillionTokens: number;
1577
1641
  cachedInputMicrosPerMillionTokens?: number | undefined;
1642
+ cacheWriteMicrosPerMillionTokens?: number | undefined;
1578
1643
  outputMicrosPerMillionTokens: number;
1579
1644
  marginBps?: number | undefined;
1580
1645
  };
@@ -1608,6 +1673,7 @@ export type EntitlementsConfig = Entitlements;
1608
1673
  const ModelPricingSchema = z.object({
1609
1674
  inputMicrosPerMillionTokens: z.number().int().nonnegative(),
1610
1675
  cachedInputMicrosPerMillionTokens: z.number().int().nonnegative().optional(),
1676
+ cacheWriteMicrosPerMillionTokens: z.number().int().nonnegative().optional(),
1611
1677
  outputMicrosPerMillionTokens: z.number().int().nonnegative(),
1612
1678
  marginBps: z.number().int().min(0).max(100_000).optional(),
1613
1679
  });
@@ -1772,10 +1838,11 @@ export type ModelExecutionLimitsV1 = {
1772
1838
  export type CredentialSourceV1 =
1773
1839
  | { kind: "deployment"; mechanism: "api_key" | "azure_ad_bearer" | "none" }
1774
1840
  | { kind: "connected_subscription"; provider: "codex" | "xai" }
1775
- | { kind: "workspace_connection"; mechanism: "api_key" };
1841
+ | { kind: "workspace_connection"; mechanism: "api_key" }
1842
+ | { kind: "organization_connection"; mechanism: "api_key" };
1776
1843
 
1777
1844
  export type BillingAttributionV1 = {
1778
- upstreamPayer: "deployment" | "workspace" | "connected_subscription";
1845
+ upstreamPayer: "deployment" | "workspace" | "organization" | "connected_subscription";
1779
1846
  metering: "opengeni_credits" | "external";
1780
1847
  };
1781
1848
 
@@ -1810,6 +1877,9 @@ export const RegistryProviderKind = z.enum([
1810
1877
  "xai-subscription",
1811
1878
  "vercel-gateway-managed",
1812
1879
  "vercel-gateway-workspace",
1880
+ "vercel-gateway-organization",
1881
+ "openrouter-workspace",
1882
+ "openrouter-organization",
1813
1883
  ]);
1814
1884
  export type RegistryProviderKind = z.infer<typeof RegistryProviderKind>;
1815
1885
 
@@ -1932,6 +2002,350 @@ const RegistryProviderSchema = z
1932
2002
  });
1933
2003
  export type RegistryProvider = z.infer<typeof RegistryProviderSchema>;
1934
2004
 
2005
+ export const OPENGENI_GATEWAY_PROVIDER_ID = "opengeni-gateway" as const;
2006
+ export const WORKSPACE_GATEWAY_PROVIDER_ID = "workspace-gateway" as const;
2007
+ export const WORKSPACE_GATEWAY_MODEL_ID_PREFIX = "workspace-gateway/" as const;
2008
+ export const ORGANIZATION_GATEWAY_PROVIDER_ID = "organization-gateway" as const;
2009
+ export const ORGANIZATION_GATEWAY_MODEL_ID_PREFIX = "organization-gateway/" as const;
2010
+ export const OPENROUTER_PROVIDER_ID = "openrouter" as const;
2011
+ export const OPENROUTER_MODEL_ID_PREFIX = "openrouter/" as const;
2012
+ export const WORKSPACE_OPENROUTER_PROVIDER_ID = "workspace-openrouter" as const;
2013
+ export const WORKSPACE_OPENROUTER_MODEL_ID_PREFIX = "workspace-openrouter/" as const;
2014
+ export const ORGANIZATION_OPENROUTER_PROVIDER_ID = "organization-openrouter" as const;
2015
+ export const ORGANIZATION_OPENROUTER_MODEL_ID_PREFIX = "organization-openrouter/" as const;
2016
+ export const OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1" as const;
2017
+
2018
+ const RESERVED_MODEL_PROVIDER_IDS = new Set<string>([
2019
+ "openai",
2020
+ "azure",
2021
+ CODEX_PROVIDER_ID,
2022
+ XAI_SUBSCRIPTION_PROVIDER_ID,
2023
+ OPENGENI_GATEWAY_PROVIDER_ID,
2024
+ WORKSPACE_GATEWAY_PROVIDER_ID,
2025
+ ORGANIZATION_GATEWAY_PROVIDER_ID,
2026
+ OPENROUTER_PROVIDER_ID,
2027
+ WORKSPACE_OPENROUTER_PROVIDER_ID,
2028
+ ORGANIZATION_OPENROUTER_PROVIDER_ID,
2029
+ ]);
2030
+
2031
+ export const ModelCostClass = z.enum(["free", "credits"]);
2032
+ export type ModelCostClass = z.infer<typeof ModelCostClass>;
2033
+
2034
+ export const ConfiguredModelCostClass = z.enum([
2035
+ "free",
2036
+ "credits",
2037
+ "subscription",
2038
+ "workspace",
2039
+ "organization",
2040
+ ]);
2041
+ export type ConfiguredModelCostClass = z.infer<typeof ConfiguredModelCostClass>;
2042
+
2043
+ const ModelNote = z
2044
+ .string()
2045
+ .max(500)
2046
+ .refine((value) => !/[\r\n|]/u.test(value), {
2047
+ message: "model notes must not contain newlines or the | field separator",
2048
+ });
2049
+
2050
+ export function parseModelCostPolicyJson(raw: string): Record<string, ModelCostClass> {
2051
+ let parsed: unknown;
2052
+ try {
2053
+ parsed = JSON.parse(raw);
2054
+ } catch (error) {
2055
+ throw new Error(
2056
+ `OPENGENI_MODEL_COST_POLICY_JSON must be valid JSON: ${error instanceof Error ? error.message : String(error)}`,
2057
+ { cause: error },
2058
+ );
2059
+ }
2060
+ return z.record(z.string().min(1), ModelCostClass).parse(parsed);
2061
+ }
2062
+
2063
+ export function parseModelNotesJson(raw: string): Record<string, string> {
2064
+ let parsed: unknown;
2065
+ try {
2066
+ parsed = JSON.parse(raw);
2067
+ } catch (error) {
2068
+ throw new Error(
2069
+ `OPENGENI_MODEL_NOTES_JSON must be valid JSON: ${error instanceof Error ? error.message : String(error)}`,
2070
+ { cause: error },
2071
+ );
2072
+ }
2073
+ return z.record(z.string().min(1), ModelNote).parse(parsed);
2074
+ }
2075
+
2076
+ export function configuredModelNotes(
2077
+ settings: Pick<Settings, "modelNotesJson">,
2078
+ ): Record<string, string> {
2079
+ return parseModelNotesJson(settings.modelNotesJson);
2080
+ }
2081
+
2082
+ export const GatewayCatalogModel = z
2083
+ .object({
2084
+ productId: z.string().min(1),
2085
+ workspaceProductId: z.string().min(1).startsWith(WORKSPACE_GATEWAY_MODEL_ID_PREFIX),
2086
+ upstreamModelId: z.string().min(1),
2087
+ label: z.string().min(1),
2088
+ shortLabel: z.string().min(1).max(64).optional(),
2089
+ providers: z.array(z.string().min(1)).min(1),
2090
+ implicitCaching: z.boolean().default(false),
2091
+ vision: z.boolean().default(false),
2092
+ inputFileMediaTypes: z.array(z.string().min(1)).default([]),
2093
+ contextWindowTokens: z.number().int().positive().default(1_000_000),
2094
+ effectiveContextWindowTokens: z.number().int().positive().default(900_000),
2095
+ autoCompactTokenLimit: z.number().int().positive().default(850_000),
2096
+ pricing: z.union([ModelPricingSchema, ModelPricingScheduleSchema]).optional(),
2097
+ credentialSource: z.never().optional(),
2098
+ billing: z.never().optional(),
2099
+ apiKey: z.never().optional(),
2100
+ })
2101
+ .strict();
2102
+ export type GatewayCatalogModel = z.infer<typeof GatewayCatalogModel>;
2103
+
2104
+ export const OpenRouterCatalogModel = z
2105
+ .object({
2106
+ upstreamModelId: z.string().min(1).endsWith(":free"),
2107
+ label: z.string().min(1),
2108
+ shortLabel: z.string().min(1).max(64).optional(),
2109
+ aliases: z.array(z.string().min(1)).default([]),
2110
+ capabilities: ModelCapabilitiesV1Schema,
2111
+ contextWindowTokens: z.number().int().positive().optional(),
2112
+ effectiveContextWindowTokens: z.number().int().positive().optional(),
2113
+ autoCompactTokenLimit: z.number().int().positive().optional(),
2114
+ toolOutputTruncationTokens: z.number().int().positive().optional(),
2115
+ credentialSource: z.never().optional(),
2116
+ billing: z.never().optional(),
2117
+ pricing: z.never().optional(),
2118
+ apiKey: z.never().optional(),
2119
+ })
2120
+ .strict();
2121
+ export type OpenRouterCatalogModel = z.infer<typeof OpenRouterCatalogModel>;
2122
+
2123
+ const DeploymentRegistryBaseUrl = z
2124
+ .string()
2125
+ .url()
2126
+ .superRefine((value, context) => {
2127
+ const url = new URL(value);
2128
+ if (url.username || url.password) {
2129
+ context.addIssue({
2130
+ code: "custom",
2131
+ message: "database catalog provider baseUrl must not contain userinfo",
2132
+ });
2133
+ }
2134
+ if (url.search) {
2135
+ context.addIssue({
2136
+ code: "custom",
2137
+ message: "database catalog provider baseUrl must not contain a query",
2138
+ });
2139
+ }
2140
+ if (url.hash) {
2141
+ context.addIssue({
2142
+ code: "custom",
2143
+ message: "database catalog provider baseUrl must not contain a fragment",
2144
+ });
2145
+ }
2146
+ });
2147
+
2148
+ const DeploymentRegistryProviderKind = z.enum(["api-key", "anonymous"]);
2149
+
2150
+ const DeploymentRegistryModelSchema = RegistryModelSchema.safeExtend({
2151
+ pricing: z.never().optional(),
2152
+ }).strict();
2153
+
2154
+ const DeploymentRegistryProviderSchema = RegistryProviderSchema.safeExtend({
2155
+ kind: DeploymentRegistryProviderKind.default("api-key"),
2156
+ baseUrl: DeploymentRegistryBaseUrl,
2157
+ models: z.array(DeploymentRegistryModelSchema).min(1),
2158
+ apiKey: z.never().optional(),
2159
+ apiKeyEnv: z.never().optional(),
2160
+ defaultHeaders: z.never().optional(),
2161
+ defaultQuery: z.never().optional(),
2162
+ publicDefaultHeaderNames: z.never().optional(),
2163
+ publicDefaultQueryNames: z.never().optional(),
2164
+ }).strict();
2165
+
2166
+ const DeploymentGatewayCatalogModelSchema = GatewayCatalogModel.safeExtend({
2167
+ pricing: z.never().optional(),
2168
+ }).strict();
2169
+
2170
+ export const ModelCatalogDocument = z
2171
+ .object({
2172
+ schemaVersion: z.literal(1),
2173
+ /** Canonical deployment default. Omission preserves the V1 first-built-in
2174
+ * fallback for existing documents; operators should set this explicitly
2175
+ * when cutting over a registry or connected-subscription default. */
2176
+ defaultModel: z.string().min(1).optional(),
2177
+ builtInModels: z.array(z.string().min(1)).min(1),
2178
+ registryProviders: z.array(DeploymentRegistryProviderSchema).default([]),
2179
+ gatewayModels: z.array(DeploymentGatewayCatalogModelSchema).default([]),
2180
+ openrouterModels: z.array(OpenRouterCatalogModel).default([]),
2181
+ modelNotes: z.record(z.string().min(1), ModelNote).default({}),
2182
+ billing: z.never().optional(),
2183
+ enabled: z.never().optional(),
2184
+ apiKey: z.never().optional(),
2185
+ bands: z.never().optional(),
2186
+ })
2187
+ .strict()
2188
+ .superRefine((document, context) => {
2189
+ const productIds = new Set<string>();
2190
+ const providerIds = new Set<string>();
2191
+ const gatewayUpstreamIds = new Set<string>();
2192
+ const add = (id: string, path: Array<string | number>): void => {
2193
+ if (/[\u000A\u000D|]/u.test(id)) {
2194
+ context.addIssue({
2195
+ code: "custom",
2196
+ path,
2197
+ message: "catalog product ids must not contain newlines or the | field separator",
2198
+ });
2199
+ }
2200
+ if (productIds.has(id)) {
2201
+ context.addIssue({
2202
+ code: "custom",
2203
+ path,
2204
+ message: `duplicate product id ${id}`,
2205
+ });
2206
+ }
2207
+ productIds.add(id);
2208
+ };
2209
+ document.builtInModels.forEach((id, index) => add(id, ["builtInModels", index]));
2210
+ document.registryProviders.forEach((provider, providerIndex) => {
2211
+ if (RESERVED_MODEL_PROVIDER_IDS.has(provider.id)) {
2212
+ context.addIssue({
2213
+ code: "custom",
2214
+ path: ["registryProviders", providerIndex, "id"],
2215
+ message: `provider id ${provider.id} is reserved for a reviewed OpenGeni provider`,
2216
+ });
2217
+ }
2218
+ if (providerIds.has(provider.id)) {
2219
+ context.addIssue({
2220
+ code: "custom",
2221
+ path: ["registryProviders", providerIndex, "id"],
2222
+ message: `duplicate provider id ${provider.id}`,
2223
+ });
2224
+ }
2225
+ providerIds.add(provider.id);
2226
+ provider.models.forEach((model, modelIndex) =>
2227
+ add(model.id, ["registryProviders", providerIndex, "models", modelIndex, "id"]),
2228
+ );
2229
+ });
2230
+ document.gatewayModels.forEach((model, index) => {
2231
+ if (gatewayUpstreamIds.has(model.upstreamModelId)) {
2232
+ context.addIssue({
2233
+ code: "custom",
2234
+ path: ["gatewayModels", index, "upstreamModelId"],
2235
+ message: `duplicate Gateway upstream model id ${model.upstreamModelId}`,
2236
+ });
2237
+ }
2238
+ gatewayUpstreamIds.add(model.upstreamModelId);
2239
+ add(model.productId, ["gatewayModels", index, "productId"]);
2240
+ add(model.workspaceProductId, ["gatewayModels", index, "workspaceProductId"]);
2241
+ });
2242
+ document.openrouterModels.forEach((model, index) =>
2243
+ add(`${OPENROUTER_MODEL_ID_PREFIX}${model.upstreamModelId}`, [
2244
+ "openrouterModels",
2245
+ index,
2246
+ "upstreamModelId",
2247
+ ]),
2248
+ );
2249
+ if (document.defaultModel && /[\u000A\u000D|]/u.test(document.defaultModel)) {
2250
+ context.addIssue({
2251
+ code: "custom",
2252
+ path: ["defaultModel"],
2253
+ message: "catalog default model must not contain newlines or the | field separator",
2254
+ });
2255
+ }
2256
+ if (
2257
+ document.defaultModel &&
2258
+ !productIds.has(document.defaultModel) &&
2259
+ !document.defaultModel.startsWith(CODEX_MODEL_ID_PREFIX) &&
2260
+ !document.defaultModel.startsWith(XAI_SUBSCRIPTION_MODEL_ID_PREFIX)
2261
+ ) {
2262
+ context.addIssue({
2263
+ code: "custom",
2264
+ path: ["defaultModel"],
2265
+ message:
2266
+ "catalog default model must reference deployment catalog membership or a connected-subscription product",
2267
+ });
2268
+ }
2269
+ for (const productId of Object.keys(document.modelNotes)) {
2270
+ if (!productIds.has(productId)) {
2271
+ context.addIssue({
2272
+ code: "custom",
2273
+ path: ["modelNotes", productId],
2274
+ message: "model note references a product id outside the deployment catalog",
2275
+ });
2276
+ }
2277
+ }
2278
+ });
2279
+ export type ModelCatalogDocument = z.infer<typeof ModelCatalogDocument>;
2280
+
2281
+ export function parseModelCatalogDocument(value: unknown): ModelCatalogDocument {
2282
+ return ModelCatalogDocument.parse(value);
2283
+ }
2284
+
2285
+ function deploymentRegistryProvidersWithHostCredentials(
2286
+ settings: Settings,
2287
+ providers: readonly z.infer<typeof DeploymentRegistryProviderSchema>[],
2288
+ ): RegistryProvider[] {
2289
+ const hostProviders = new Map(
2290
+ parseModelProvidersJson(settings.modelProvidersJson).map((provider) => [provider.id, provider]),
2291
+ );
2292
+ return providers.map((provider) => {
2293
+ if (provider.kind !== "api-key") return provider;
2294
+ const host = hostProviders.get(provider.id);
2295
+ if (!host || host.kind !== "api-key") {
2296
+ throw new Error(
2297
+ `database model catalog provider ${provider.id} has no matching host-authorized api-key transport`,
2298
+ );
2299
+ }
2300
+ const transportIdentity = (candidate: typeof provider | RegistryProvider) => ({
2301
+ kind: candidate.kind,
2302
+ baseUrl: candidate.baseUrl,
2303
+ api: candidate.api,
2304
+ wireProfile: candidate.wireProfile,
2305
+ });
2306
+ if (canonicalJson(transportIdentity(provider)) !== canonicalJson(transportIdentity(host))) {
2307
+ throw new Error(
2308
+ `database model catalog provider ${provider.id} does not match its host-authorized transport`,
2309
+ );
2310
+ }
2311
+ return {
2312
+ ...provider,
2313
+ ...(host.defaultHeaders === undefined ? {} : { defaultHeaders: host.defaultHeaders }),
2314
+ ...(host.defaultQuery === undefined ? {} : { defaultQuery: host.defaultQuery }),
2315
+ ...(host.publicDefaultHeaderNames === undefined
2316
+ ? {}
2317
+ : { publicDefaultHeaderNames: host.publicDefaultHeaderNames }),
2318
+ ...(host.publicDefaultQueryNames === undefined
2319
+ ? {}
2320
+ : { publicDefaultQueryNames: host.publicDefaultQueryNames }),
2321
+ ...(host.apiKey === undefined ? {} : { apiKey: host.apiKey }),
2322
+ ...(host.apiKeyEnv === undefined ? {} : { apiKeyEnv: host.apiKeyEnv }),
2323
+ };
2324
+ });
2325
+ }
2326
+
2327
+ /** Pure secret-free database catalog overlay. getSettings remains env-only. */
2328
+ export function applyModelCatalogDocument(settings: Settings, rawDocument: unknown): Settings {
2329
+ const document = parseModelCatalogDocument(rawDocument);
2330
+ const defaultModel = document.defaultModel ?? document.builtInModels[0]!;
2331
+ const resolved = {
2332
+ ...settings,
2333
+ openaiModel: defaultModel,
2334
+ // Keep the complete built-in membership, including the default. The worker
2335
+ // replaces openaiModel with the exact turn model; the run-scoped router
2336
+ // needs one stable built-in id in this allow-list so a bare provider model
2337
+ // is not temporarily claimed by OpenAI/Azure during name re-resolution.
2338
+ openaiAllowedModels: document.builtInModels.join(","),
2339
+ modelProvidersJson: JSON.stringify(
2340
+ deploymentRegistryProvidersWithHostCredentials(settings, document.registryProviders),
2341
+ ),
2342
+ resolvedGatewayModelsJson: JSON.stringify(document.gatewayModels),
2343
+ resolvedOpenRouterModelsJson: JSON.stringify(document.openrouterModels),
2344
+ modelNotesJson: JSON.stringify(document.modelNotes),
2345
+ };
2346
+ return resolved;
2347
+ }
2348
+
1935
2349
  export const IntegrationOAuthClientConfigSchema = z.object({
1936
2350
  clientId: z.string().min(1),
1937
2351
  clientSecret: z.string().min(1).optional(),
@@ -1951,7 +2365,7 @@ export type IntegrationOAuthClientConfig = z.infer<typeof IntegrationOAuthClient
1951
2365
  export interface ResolvedModelProvider {
1952
2366
  id: string; // "openai" | "azure" | registry id
1953
2367
  label: string;
1954
- kind: RegistryProviderKind; // "api-key" (built-ins + most registry) | "anonymous" | subscription
2368
+ kind: RegistryProviderKind | "openrouter-managed";
1955
2369
  api: ModelProviderApi;
1956
2370
  wireProfile: ModelProviderWireProfile;
1957
2371
  builtin: boolean;
@@ -1965,6 +2379,10 @@ export interface ResolvedModelProvider {
1965
2379
  billing: BillingAttributionV1;
1966
2380
  }
1967
2381
 
2382
+ type InternalRegistryProvider = Omit<RegistryProvider, "kind"> & {
2383
+ kind: RegistryProviderKind | "openrouter-managed";
2384
+ };
2385
+
1968
2386
  /** A single exposed model + the provider that serves it. */
1969
2387
  export interface ConfiguredModel {
1970
2388
  schemaVersion: 1;
@@ -1981,6 +2399,8 @@ export interface ConfiguredModel {
1981
2399
  executionLimits: ModelExecutionLimitsV1;
1982
2400
  credentialSource: CredentialSourceV1;
1983
2401
  billing: BillingAttributionV1;
2402
+ /** Workspace-facing funding policy, independent of upstream settlement. */
2403
+ cost: ConfiguredModelCostClass;
1984
2404
  capabilities: ModelCapabilitiesV1;
1985
2405
  requestPolicy?: {
1986
2406
  gateway: {
@@ -2000,11 +2420,10 @@ export interface ConfiguredModel {
2000
2420
 
2001
2421
  export const VERCEL_AI_GATEWAY_BASE_URL = "https://ai-gateway.vercel.sh/v1" as const;
2002
2422
  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
2423
  export const VERCEL_AI_GATEWAY_CONNECTION_DOMAIN = "ai-gateway.vercel.sh" as const;
2007
2424
  export const VERCEL_AI_GATEWAY_CONNECTION_ROLE = "vercel_ai_gateway" as const;
2425
+ export const WORKSPACE_OPENROUTER_CONNECTION_DOMAIN = "openrouter.ai" as const;
2426
+ export const WORKSPACE_OPENROUTER_CONNECTION_ROLE = "openrouter" as const;
2008
2427
 
2009
2428
  export const CODEX_REALTIME_MODEL_ID = "gpt-live-1-boulder-alpha" as const;
2010
2429
  export const SUPERGROK_REALTIME_MODEL_ID = "supergrok/grok-voice-think-fast-2.0" as const;
@@ -2074,16 +2493,133 @@ export const OPENGENI_GATEWAY_MODELS = {
2074
2493
  },
2075
2494
  } as const;
2076
2495
 
2496
+ export const OPENGENI_OPENROUTER_MODELS: readonly OpenRouterCatalogModel[] = [
2497
+ OpenRouterCatalogModel.parse({
2498
+ upstreamModelId: "nvidia/nemotron-3-super-120b-a12b:free",
2499
+ label: "Nemotron 3 Super 120B",
2500
+ shortLabel: "Nemotron 3 Super",
2501
+ aliases: [],
2502
+ capabilities: {
2503
+ reasoning: {
2504
+ upstream: "supported",
2505
+ // OpenRouter advertises the reasoning controls, but the catalogue does
2506
+ // not publish this model's accepted effort vocabulary. Preserve that
2507
+ // upstream fact without exposing an unverified runnable selector.
2508
+ runnable: false,
2509
+ efforts: [],
2510
+ defaultEffort: null,
2511
+ required: false,
2512
+ },
2513
+ functionCalling: { upstream: "supported", runnable: true },
2514
+ structuredOutput: { upstream: "supported", runnable: true },
2515
+ hostedTools: {
2516
+ webSearch: { upstream: "unknown", runnable: false },
2517
+ xSearch: { upstream: "unknown", runnable: false },
2518
+ codeExecution: { upstream: "unknown", runnable: false },
2519
+ imageGeneration: { upstream: "unknown", runnable: false },
2520
+ },
2521
+ inputModalities: ["text"],
2522
+ inputFileMediaTypes: [],
2523
+ outputModalities: ["text"],
2524
+ transports: {
2525
+ sse: { upstream: "supported", runnable: true },
2526
+ responsesWebSocket: { upstream: "unknown", runnable: false },
2527
+ realtimeAudio: { upstream: "unsupported", runnable: false },
2528
+ },
2529
+ latencyModes: [{ id: "standard", upstream: "unknown", runnable: true }],
2530
+ },
2531
+ contextWindowTokens: 262_144,
2532
+ effectiveContextWindowTokens: 235_929,
2533
+ autoCompactTokenLimit: 220_000,
2534
+ }),
2535
+ ];
2536
+
2537
+ function defaultGatewayCatalogModels(): GatewayCatalogModel[] {
2538
+ return [
2539
+ {
2540
+ ...OPENGENI_GATEWAY_MODELS.deepseek,
2541
+ vision: false,
2542
+ inputFileMediaTypes: [],
2543
+ contextWindowTokens: 1_000_000,
2544
+ effectiveContextWindowTokens: 900_000,
2545
+ autoCompactTokenLimit: 850_000,
2546
+ },
2547
+ {
2548
+ ...OPENGENI_GATEWAY_MODELS.kimi,
2549
+ vision: true,
2550
+ inputFileMediaTypes: ["application/pdf"],
2551
+ contextWindowTokens: 1_000_000,
2552
+ effectiveContextWindowTokens: 900_000,
2553
+ autoCompactTokenLimit: 850_000,
2554
+ },
2555
+ ].map((model) => GatewayCatalogModel.parse(model));
2556
+ }
2557
+
2558
+ function configuredGatewayCatalogModels(settings: Settings): GatewayCatalogModel[] {
2559
+ if (settings.resolvedGatewayModelsJson === undefined) {
2560
+ return defaultGatewayCatalogModels();
2561
+ }
2562
+ return z.array(GatewayCatalogModel).parse(JSON.parse(settings.resolvedGatewayModelsJson));
2563
+ }
2564
+
2565
+ export function configuredGatewayUpstreamModelIds(settings: Settings): string[] {
2566
+ return configuredGatewayCatalogModels(settings).map((model) => model.upstreamModelId);
2567
+ }
2568
+
2569
+ export function configuredGatewayWorkspaceProductModelIds(settings: Settings): string[] {
2570
+ return configuredGatewayCatalogModels(settings).map((model) => model.workspaceProductId);
2571
+ }
2572
+
2573
+ export function configuredGatewayOrganizationProductModelIds(settings: Settings): string[] {
2574
+ void settings;
2575
+ return [];
2576
+ }
2577
+
2578
+ export function configuredModelInputIdentities(settings: Settings): string[] {
2579
+ return configuredModels(settings).flatMap((model) => [model.id, ...model.aliases]);
2580
+ }
2581
+
2582
+ function configuredOpenRouterCatalogModels(settings: Settings): OpenRouterCatalogModel[] {
2583
+ if (settings.resolvedOpenRouterModelsJson === undefined) {
2584
+ return [...OPENGENI_OPENROUTER_MODELS];
2585
+ }
2586
+ return z.array(OpenRouterCatalogModel).parse(JSON.parse(settings.resolvedOpenRouterModelsJson));
2587
+ }
2588
+
2589
+ export function configuredOpenRouterUpstreamModelIds(settings: Settings): string[] {
2590
+ return configuredOpenRouterCatalogModels(settings).map((model) => model.upstreamModelId);
2591
+ }
2592
+
2593
+ function workspaceOpenRouterProductId(modelId: string): string {
2594
+ return `${WORKSPACE_OPENROUTER_MODEL_ID_PREFIX}${
2595
+ modelId.startsWith(OPENROUTER_MODEL_ID_PREFIX)
2596
+ ? modelId.slice(OPENROUTER_MODEL_ID_PREFIX.length)
2597
+ : modelId
2598
+ }`;
2599
+ }
2600
+
2601
+ export function configuredOpenRouterWorkspaceProductModelIds(settings: Settings): string[] {
2602
+ return configuredOpenRouterCatalogModels(settings).flatMap((model) =>
2603
+ [model.upstreamModelId, ...model.aliases].map(workspaceOpenRouterProductId),
2604
+ );
2605
+ }
2606
+
2607
+ export function configuredOpenRouterOrganizationProductModelIds(settings: Settings): string[] {
2608
+ void settings;
2609
+ return [];
2610
+ }
2611
+
2077
2612
  /**
2078
2613
  * Built-in OpenGeni credit pricing schedules.
2079
2614
  *
2080
2615
  * 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
2616
+ * `marginBps` (500 = +5%) on top. Long-context tiers follow OpenAI's
2082
2617
  * ">272K input tokens" rule (threshold exclusive of 272_000).
2083
2618
  *
2084
2619
  * 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.
2620
+ * offered. Codex / connected-subscription turns use `metering: external`, so
2621
+ * this map never debits them, but it does provide their equivalent OpenGeni
2622
+ * credit price when a matching product model is configured.
2087
2623
  *
2088
2624
  * When adding or changing a billed model, run `bun run check:model-pricing`
2089
2625
  * (see docs/model-providers.md § Price audit). That compares this map to
@@ -2092,20 +2628,23 @@ export const OPENGENI_GATEWAY_MODELS = {
2092
2628
  export const defaultModelPricing: Record<string, ModelPricingScheduleV1> = {
2093
2629
  "gpt-5.6-sol": {
2094
2630
  default: {
2095
- inputMicrosPerMillionTokens: 5_000_000,
2096
- cachedInputMicrosPerMillionTokens: 500_000,
2097
- outputMicrosPerMillionTokens: 30_000_000,
2098
- marginBps: 2_500,
2631
+ // Promotional OpenAI pricing, guaranteed through at least 2026-11-21.
2632
+ inputMicrosPerMillionTokens: 4_000_000,
2633
+ cachedInputMicrosPerMillionTokens: 400_000,
2634
+ cacheWriteMicrosPerMillionTokens: 5_000_000,
2635
+ outputMicrosPerMillionTokens: 20_000_000,
2636
+ marginBps: 500,
2099
2637
  },
2100
2638
  inputTokenTiers: [
2101
2639
  {
2102
2640
  // OpenAI: prompts with >272K input tokens use the long-context rate.
2103
2641
  minimumInputTokens: 272_001,
2104
2642
  pricing: {
2105
- inputMicrosPerMillionTokens: 10_000_000,
2106
- cachedInputMicrosPerMillionTokens: 1_000_000,
2107
- outputMicrosPerMillionTokens: 45_000_000,
2108
- marginBps: 2_500,
2643
+ inputMicrosPerMillionTokens: 8_000_000,
2644
+ cachedInputMicrosPerMillionTokens: 800_000,
2645
+ cacheWriteMicrosPerMillionTokens: 10_000_000,
2646
+ outputMicrosPerMillionTokens: 30_000_000,
2647
+ marginBps: 500,
2109
2648
  },
2110
2649
  },
2111
2650
  ],
@@ -2114,8 +2653,9 @@ export const defaultModelPricing: Record<string, ModelPricingScheduleV1> = {
2114
2653
  default: {
2115
2654
  inputMicrosPerMillionTokens: 2_000_000,
2116
2655
  cachedInputMicrosPerMillionTokens: 200_000,
2656
+ cacheWriteMicrosPerMillionTokens: 2_500_000,
2117
2657
  outputMicrosPerMillionTokens: 12_000_000,
2118
- marginBps: 2_500,
2658
+ marginBps: 500,
2119
2659
  },
2120
2660
  inputTokenTiers: [
2121
2661
  {
@@ -2123,8 +2663,9 @@ export const defaultModelPricing: Record<string, ModelPricingScheduleV1> = {
2123
2663
  pricing: {
2124
2664
  inputMicrosPerMillionTokens: 4_000_000,
2125
2665
  cachedInputMicrosPerMillionTokens: 400_000,
2666
+ cacheWriteMicrosPerMillionTokens: 5_000_000,
2126
2667
  outputMicrosPerMillionTokens: 18_000_000,
2127
- marginBps: 2_500,
2668
+ marginBps: 500,
2128
2669
  },
2129
2670
  },
2130
2671
  ],
@@ -2133,8 +2674,9 @@ export const defaultModelPricing: Record<string, ModelPricingScheduleV1> = {
2133
2674
  default: {
2134
2675
  inputMicrosPerMillionTokens: 200_000,
2135
2676
  cachedInputMicrosPerMillionTokens: 20_000,
2677
+ cacheWriteMicrosPerMillionTokens: 250_000,
2136
2678
  outputMicrosPerMillionTokens: 1_200_000,
2137
- marginBps: 2_500,
2679
+ marginBps: 500,
2138
2680
  },
2139
2681
  inputTokenTiers: [
2140
2682
  {
@@ -2142,8 +2684,9 @@ export const defaultModelPricing: Record<string, ModelPricingScheduleV1> = {
2142
2684
  pricing: {
2143
2685
  inputMicrosPerMillionTokens: 400_000,
2144
2686
  cachedInputMicrosPerMillionTokens: 40_000,
2687
+ cacheWriteMicrosPerMillionTokens: 500_000,
2145
2688
  outputMicrosPerMillionTokens: 1_800_000,
2146
- marginBps: 2_500,
2689
+ marginBps: 500,
2147
2690
  },
2148
2691
  },
2149
2692
  ],
@@ -2158,7 +2701,7 @@ export const defaultModelPricing: Record<string, ModelPricingScheduleV1> = {
2158
2701
  inputMicrosPerMillionTokens: 140_000,
2159
2702
  cachedInputMicrosPerMillionTokens: 28_000,
2160
2703
  outputMicrosPerMillionTokens: 280_000,
2161
- marginBps: 2_500,
2704
+ marginBps: 500,
2162
2705
  },
2163
2706
  },
2164
2707
  [OPENGENI_GATEWAY_MODELS.kimi.productId]: {
@@ -2166,7 +2709,7 @@ export const defaultModelPricing: Record<string, ModelPricingScheduleV1> = {
2166
2709
  inputMicrosPerMillionTokens: 3_000_000,
2167
2710
  cachedInputMicrosPerMillionTokens: 300_000,
2168
2711
  outputMicrosPerMillionTokens: 15_000_000,
2169
- marginBps: 2_500,
2712
+ marginBps: 500,
2170
2713
  },
2171
2714
  },
2172
2715
  // Fireworks AI / GLM 5.2 — the first shipped non-OpenAI registry model. A
@@ -2178,7 +2721,7 @@ export const defaultModelPricing: Record<string, ModelPricingScheduleV1> = {
2178
2721
  inputMicrosPerMillionTokens: 1_400_000,
2179
2722
  cachedInputMicrosPerMillionTokens: 140_000,
2180
2723
  outputMicrosPerMillionTokens: 4_400_000,
2181
- marginBps: 2_500,
2724
+ marginBps: 500,
2182
2725
  },
2183
2726
  },
2184
2727
  };
@@ -2269,12 +2812,17 @@ function objectStorageConfiguredForWorkspaceArchives(settings: Settings): boolea
2269
2812
  }
2270
2813
  }
2271
2814
 
2272
- function optional(name: string): string | undefined {
2273
- const value = process.env[name];
2815
+ function optionalEnvironmentValue(name: string, source: NodeJS.ProcessEnv): string | undefined {
2816
+ const value = source[name];
2274
2817
  return value && value.trim().length > 0 ? value : undefined;
2275
2818
  }
2276
2819
 
2277
- export function getSettings(): Settings {
2820
+ export function getSettings(source: NodeJS.ProcessEnv = process.env): Settings {
2821
+ const optional = (name: string): string | undefined => optionalEnvironmentValue(name, source);
2822
+ const modelCatalogSource = optional("OPENGENI_MODEL_CATALOG_SOURCE");
2823
+ const modelCostPolicyJson =
2824
+ optional("OPENGENI_MODEL_COST_POLICY_JSON") ??
2825
+ (modelCatalogSource === "database" ? "{}" : DEFAULT_MODEL_COST_POLICY_JSON);
2278
2826
  const raw = {
2279
2827
  serviceName: optional("OPENGENI_SERVICE_NAME"),
2280
2828
  environment: optional("OPENGENI_ENVIRONMENT"),
@@ -2324,6 +2872,8 @@ export function getSettings(): Settings {
2324
2872
  analyticsPosthogHost: optional("OPENGENI_ANALYTICS_POSTHOG_HOST"),
2325
2873
  analyticsGa4MeasurementId: optional("OPENGENI_ANALYTICS_GA4_MEASUREMENT_ID"),
2326
2874
  publicBaseUrl: optional("OPENGENI_PUBLIC_BASE_URL"),
2875
+ mcpOauthEnabled: optional("OPENGENI_MCP_OAUTH_ENABLED"),
2876
+ mcpOauthTrustedProxyHops: optional("OPENGENI_MCP_OAUTH_TRUSTED_PROXY_HOPS"),
2327
2877
  webBaseUrl: optional("OPENGENI_WEB_BASE_URL"),
2328
2878
  agentReleasesBaseUrl: optional("OPENGENI_AGENT_RELEASES_BASE_URL"),
2329
2879
  agentStableVersion: optional("OPENGENI_AGENT_STABLE_VERSION"),
@@ -2395,6 +2945,9 @@ export function getSettings(): Settings {
2395
2945
  goalIdleBackoffMs: optional("OPENGENI_GOAL_IDLE_BACKOFF_MS"),
2396
2946
  goalIdleBackoffMaxMs: optional("OPENGENI_GOAL_IDLE_BACKOFF_MAX_MS"),
2397
2947
  childLifecycleNoticesEnabled: optional("OPENGENI_CHILD_LIFECYCLE_NOTICES_ENABLED"),
2948
+ hostMcpAuthoritySourceAdmissionEnabled: optional(
2949
+ "OPENGENI_HOST_MCP_AUTHORITY_SOURCE_ADMISSION_ENABLED",
2950
+ ),
2398
2951
  slackWorkspaceRoutingEnabled: optional("OPENGENI_SLACK_WORKSPACE_ROUTING_ENABLED"),
2399
2952
  agentMaxModelCallsPerTurn: optional("OPENGENI_AGENT_MAX_MODEL_CALLS_PER_TURN"),
2400
2953
  contextWindowTokens: optional("OPENGENI_CONTEXT_WINDOW_TOKENS"),
@@ -2464,6 +3017,10 @@ export function getSettings(): Settings {
2464
3017
  voiceInputAzureAdToken: optional("OPENGENI_VOICE_INPUT_AZURE_AD_TOKEN"),
2465
3018
  voiceInputCodexExperimentalEnabled: optional("OPENGENI_VOICE_INPUT_CODEX_EXPERIMENTAL"),
2466
3019
  modelPricingJson: optional("OPENGENI_MODEL_PRICING_JSON"),
3020
+ modelCatalogSource,
3021
+ modelCostPolicyJson,
3022
+ modelNotesJson: optional("OPENGENI_MODEL_NOTES_JSON"),
3023
+ openrouterApiKey: optional("OPENGENI_OPENROUTER_API_KEY"),
2467
3024
  modelProvidersJson: optional("OPENGENI_MODEL_PROVIDERS_JSON"),
2468
3025
  codexSubscriptionEnabled: optional("OPENGENI_CODEX_SUBSCRIPTION_ENABLED"),
2469
3026
  supergrokSubscriptionEnabled: optional("OPENGENI_SUPERGROK_SUBSCRIPTION_ENABLED"),
@@ -2473,7 +3030,6 @@ export function getSettings(): Settings {
2473
3030
  codexConnectedAppsEnabled: optional("OPENGENI_CODEX_CONNECTED_APPS_ENABLED"),
2474
3031
  codexToolSearchEnabled: optional("OPENGENI_CODEX_TOOL_SEARCH_ENABLED"),
2475
3032
  lazyToolSearchEnabled: optional("OPENGENI_LAZY_TOOL_SEARCH_ENABLED"),
2476
- codexCredentialLeasingEnabled: optional("OPENGENI_CODEX_CREDENTIAL_LEASING_ENABLED"),
2477
3033
  codexFleetPolicyShadowEnabled: optional("OPENGENI_CODEX_FLEET_POLICY_SHADOW_ENABLED"),
2478
3034
  codexProductSku: optional("OPENGENI_CODEX_PRODUCT_SKU"),
2479
3035
  openaiReasoningEffort: optional("OPENGENI_OPENAI_REASONING_EFFORT"),
@@ -2498,7 +3054,7 @@ export function getSettings(): Settings {
2498
3054
  dockerNetwork: optional("OPENGENI_DOCKER_NETWORK"),
2499
3055
  dockerWorkspaceBaseDir: optional("OPENGENI_DOCKER_WORKSPACE_BASE_DIR"),
2500
3056
  modalAppName: optional("OPENGENI_MODAL_APP_NAME"),
2501
- modalImageRef: optional("OPENGENI_MODAL_IMAGE_REF"),
3057
+ modalImageRef: optional("OPENGENI_MODAL_IMAGE_REF") ?? DEFAULT_MODAL_IMAGE_REF,
2502
3058
  modalImageId: optional("OPENGENI_MODAL_IMAGE_ID"),
2503
3059
  modalImageRegistrySecret: optional("OPENGENI_MODAL_IMAGE_REGISTRY_SECRET"),
2504
3060
  modalTimeoutSeconds: optional("OPENGENI_MODAL_TIMEOUT_SECONDS"),
@@ -2600,6 +3156,7 @@ export function getSettings(): Settings {
2600
3156
  sandboxIdleGraceMs: optional("OPENGENI_SANDBOX_IDLE_GRACE_MS"),
2601
3157
  sandboxSnapshotIntervalMs: optional("OPENGENI_SANDBOX_SNAPSHOT_INTERVAL_MS"),
2602
3158
  sandboxSnapshotTimeoutMs: optional("OPENGENI_SANDBOX_SNAPSHOT_TIMEOUT_MS"),
3159
+ sandboxDrainSnapshotTimeoutMs: optional("OPENGENI_SANDBOX_DRAIN_SNAPSHOT_TIMEOUT_MS"),
2603
3160
  sandboxRotationLeadMs: optional("OPENGENI_SANDBOX_ROTATION_LEAD_MS"),
2604
3161
  sandboxRotationBatchSize: optional("OPENGENI_SANDBOX_ROTATION_BATCH_SIZE"),
2605
3162
  sandboxLeaseTtlMs: optional("OPENGENI_SANDBOX_LEASE_TTL_MS"),
@@ -2674,6 +3231,12 @@ export function getSettings(): Settings {
2674
3231
  managedAuthGithubClientId: optional("OPENGENI_MANAGED_AUTH_GITHUB_CLIENT_ID"),
2675
3232
  managedAuthGithubClientSecret: optional("OPENGENI_MANAGED_AUTH_GITHUB_CLIENT_SECRET"),
2676
3233
  managedAuthSessionSetMode: optional("OPENGENI_MANAGED_AUTH_SESSION_SET_MODE"),
3234
+ organizationUserSetupEmailTokenTransport: optional(
3235
+ "OPENGENI_ORGANIZATION_USER_SETUP_EMAIL_TOKEN_TRANSPORT",
3236
+ ),
3237
+ organizationUserSetupQueryEdgeSanitizationConfirmed: optional(
3238
+ "OPENGENI_ORGANIZATION_USER_SETUP_QUERY_EDGE_SANITIZATION_CONFIRMED",
3239
+ ),
2677
3240
  resendApiKey: optional("OPENGENI_RESEND_API_KEY"),
2678
3241
  emailFrom: optional("OPENGENI_EMAIL_FROM"),
2679
3242
  stripeSecretKey: optional("OPENGENI_STRIPE_SECRET_KEY"),
@@ -2695,7 +3258,7 @@ export function getSettings(): Settings {
2695
3258
  : parsed.sandboxRotationLeadMs,
2696
3259
  mcpServers: ensureBuiltInMcpServers(parsed),
2697
3260
  };
2698
- validateSettings(settings);
3261
+ validateSettings(settings, source);
2699
3262
  return settings;
2700
3263
  }
2701
3264
 
@@ -2819,10 +3382,23 @@ export function sandboxArchiveCaptureTimeoutMs(
2819
3382
  );
2820
3383
  }
2821
3384
 
3385
+ /** Provider operation budget used only by zero-holder drain/rotation capture.
3386
+ * Unset preserves the historical shared snapshot budget exactly. */
3387
+ export function effectiveSandboxDrainSnapshotTimeoutMs(
3388
+ settings: Pick<Settings, "sandboxSnapshotTimeoutMs" | "sandboxDrainSnapshotTimeoutMs">,
3389
+ ): number {
3390
+ return settings.sandboxDrainSnapshotTimeoutMs ?? settings.sandboxSnapshotTimeoutMs;
3391
+ }
3392
+
2822
3393
  export function sandboxLifecycleTransitionWaitMs(
2823
- settings: Pick<Settings, "sandboxSnapshotTimeoutMs" | "sandboxLeaseReaperPeriodMs">,
3394
+ settings: Pick<
3395
+ Settings,
3396
+ "sandboxSnapshotTimeoutMs" | "sandboxDrainSnapshotTimeoutMs" | "sandboxLeaseReaperPeriodMs"
3397
+ >,
2824
3398
  ): number {
2825
- const captureTimeoutMs = sandboxArchiveCaptureTimeoutMs(settings);
3399
+ const captureTimeoutMs = sandboxArchiveCaptureTimeoutMs({
3400
+ sandboxSnapshotTimeoutMs: effectiveSandboxDrainSnapshotTimeoutMs(settings),
3401
+ });
2826
3402
  return Math.min(
2827
3403
  SANDBOX_LIFECYCLE_TRANSITION_MAX_WAIT_MS,
2828
3404
  settings.sandboxLeaseReaperPeriodMs +
@@ -3140,10 +3716,9 @@ function legacyModelCapabilities(
3140
3716
 
3141
3717
  export function gatewayRequestPolicyForUpstreamModel(
3142
3718
  upstreamModelId: string,
3719
+ models: readonly GatewayCatalogModel[] = defaultGatewayCatalogModels(),
3143
3720
  ): ConfiguredModel["requestPolicy"] {
3144
- const model = Object.values(OPENGENI_GATEWAY_MODELS).find(
3145
- (candidate) => candidate.upstreamModelId === upstreamModelId,
3146
- );
3721
+ const model = models.find((candidate) => candidate.upstreamModelId === upstreamModelId);
3147
3722
  if (!model) {
3148
3723
  return undefined;
3149
3724
  }
@@ -3157,7 +3732,11 @@ export function gatewayRequestPolicyForUpstreamModel(
3157
3732
 
3158
3733
  function gatewayModelCapabilities(
3159
3734
  settings: Settings,
3160
- input: { implicitCaching: boolean; vision: boolean; inputFileMediaTypes?: string[] },
3735
+ input: {
3736
+ implicitCaching: boolean;
3737
+ vision: boolean;
3738
+ inputFileMediaTypes?: string[];
3739
+ },
3161
3740
  ): ModelCapabilitiesV1 {
3162
3741
  const legacy = legacyModelCapabilities(settings, {
3163
3742
  reasoningEffort: true,
@@ -3181,35 +3760,112 @@ function gatewayModelCapabilities(
3181
3760
  });
3182
3761
  }
3183
3762
 
3763
+ function openRouterCustomModelCapabilities(settings: Settings): ModelCapabilitiesV1 {
3764
+ const legacy = legacyModelCapabilities(settings, {
3765
+ reasoningEffort: false,
3766
+ hostedWebSearch: false,
3767
+ });
3768
+ return normalizeCapabilities({
3769
+ ...legacy,
3770
+ functionCalling: { upstream: "supported", runnable: true },
3771
+ inputModalities: ["text"],
3772
+ inputFileMediaTypes: [],
3773
+ transports: {
3774
+ ...legacy.transports,
3775
+ sse: { upstream: "supported", runnable: true },
3776
+ },
3777
+ promptCaching: { upstream: "unsupported", runnable: false, mode: "none" },
3778
+ latencyModes: [{ id: "standard", upstream: "supported", runnable: true }],
3779
+ });
3780
+ }
3781
+
3184
3782
  function gatewayRegistryProvider(
3185
3783
  settings: Settings,
3186
3784
  input:
3187
3785
  | { kind: "vercel-gateway-managed"; apiKey: string }
3188
- | { kind: "vercel-gateway-workspace"; apiKey?: string },
3189
- ): RegistryProvider {
3786
+ | {
3787
+ kind: "vercel-gateway-workspace" | "vercel-gateway-organization";
3788
+ apiKey?: string;
3789
+ customModels?: readonly {
3790
+ upstreamModelId: string;
3791
+ label?: string | null;
3792
+ }[];
3793
+ },
3794
+ ): InternalRegistryProvider {
3190
3795
  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;
3796
+ const organization = input.kind === "vercel-gateway-organization";
3797
+ const scoped = workspace || organization;
3798
+ const curated = organization ? [] : configuredGatewayCatalogModels(settings);
3799
+ const upstreamIds = new Set(curated.map((model) => model.upstreamModelId));
3800
+ const productIds = new Set(
3801
+ parseModelProvidersJson(settings.modelProvidersJson)
3802
+ .filter(
3803
+ (provider) =>
3804
+ provider.id !== WORKSPACE_GATEWAY_PROVIDER_ID &&
3805
+ provider.id !== ORGANIZATION_GATEWAY_PROVIDER_ID,
3806
+ )
3807
+ .flatMap((provider) =>
3808
+ provider.models.flatMap((model) => [model.id, ...(model.aliases ?? [])]),
3809
+ ),
3810
+ );
3811
+ const models = curated.map((model) => {
3812
+ const id = workspace
3813
+ ? model.workspaceProductId
3814
+ : organization
3815
+ ? `${ORGANIZATION_GATEWAY_MODEL_ID_PREFIX}${model.upstreamModelId}`
3816
+ : model.productId;
3817
+ productIds.add(id);
3193
3818
  return {
3194
- id: workspace ? model.workspaceProductId : model.productId,
3819
+ id,
3195
3820
  upstreamModelId: model.upstreamModelId,
3196
3821
  label: model.label,
3197
- shortLabel: model.shortLabel,
3822
+ ...(model.shortLabel ? { shortLabel: model.shortLabel } : {}),
3198
3823
  capabilities: gatewayModelCapabilities(settings, {
3199
3824
  implicitCaching: model.implicitCaching,
3200
- vision: kimi,
3201
- inputFileMediaTypes: kimi ? ["application/pdf"] : [],
3825
+ vision: model.vision,
3826
+ inputFileMediaTypes: model.inputFileMediaTypes,
3202
3827
  }),
3203
- contextWindowTokens: 1_000_000,
3204
- effectiveContextWindowTokens: 900_000,
3205
- autoCompactTokenLimit: 850_000,
3828
+ contextWindowTokens: model.contextWindowTokens,
3829
+ effectiveContextWindowTokens: model.effectiveContextWindowTokens,
3830
+ autoCompactTokenLimit: model.autoCompactTokenLimit,
3206
3831
  toolOutputTruncationTokens: settings.modelToolOutputTruncationTokens,
3832
+ ...(model.pricing === undefined ? {} : { pricing: model.pricing }),
3207
3833
  };
3208
3834
  });
3835
+ if (scoped) {
3836
+ for (const custom of input.customModels ?? []) {
3837
+ const productId = `${workspace ? WORKSPACE_GATEWAY_MODEL_ID_PREFIX : ORGANIZATION_GATEWAY_MODEL_ID_PREFIX}${custom.upstreamModelId}`;
3838
+ // Deployment membership wins over an older or concurrently-created
3839
+ // workspace row with the same upstream identity or generated product id.
3840
+ // This keeps runtime routing deterministic and prevents a legacy/admin
3841
+ // row from making the entire workspace catalog fail uniqueness checks.
3842
+ if (upstreamIds.has(custom.upstreamModelId) || productIds.has(productId)) continue;
3843
+ upstreamIds.add(custom.upstreamModelId);
3844
+ productIds.add(productId);
3845
+ models.push({
3846
+ id: productId,
3847
+ upstreamModelId: custom.upstreamModelId,
3848
+ label: custom.label?.trim() || custom.upstreamModelId,
3849
+ capabilities: gatewayModelCapabilities(settings, {
3850
+ implicitCaching: false,
3851
+ vision: false,
3852
+ inputFileMediaTypes: [],
3853
+ }),
3854
+ contextWindowTokens: 1_000_000,
3855
+ effectiveContextWindowTokens: 900_000,
3856
+ autoCompactTokenLimit: 850_000,
3857
+ toolOutputTruncationTokens: settings.modelToolOutputTruncationTokens,
3858
+ });
3859
+ }
3860
+ }
3209
3861
  return {
3210
3862
  kind: input.kind,
3211
- id: workspace ? WORKSPACE_GATEWAY_PROVIDER_ID : OPENGENI_GATEWAY_PROVIDER_ID,
3212
- label: workspace ? "Your Gateway" : "OpenGeni",
3863
+ id: workspace
3864
+ ? WORKSPACE_GATEWAY_PROVIDER_ID
3865
+ : organization
3866
+ ? ORGANIZATION_GATEWAY_PROVIDER_ID
3867
+ : OPENGENI_GATEWAY_PROVIDER_ID,
3868
+ label: workspace ? "Your Gateway" : organization ? "Organization Gateway" : "OpenGeni",
3213
3869
  // Responses preserves vision, reasoning items, and provider-native usage.
3214
3870
  // Model-specific compatibility stays at the reviewed request fence rather
3215
3871
  // than downgrading the whole provider wire.
@@ -3221,52 +3877,274 @@ function gatewayRegistryProvider(
3221
3877
  };
3222
3878
  }
3223
3879
 
3224
- function configuredRegistryProviders(settings: Settings): RegistryProvider[] {
3225
- const providers = parseModelProvidersJson(settings.modelProvidersJson);
3226
- if (!settings.vercelAiGatewayApiKey) {
3227
- return providers;
3880
+ function openRouterRegistryProvider(
3881
+ settings: Settings,
3882
+ input:
3883
+ | { kind: "openrouter-managed"; apiKey: string }
3884
+ | {
3885
+ kind: "openrouter-workspace" | "openrouter-organization";
3886
+ apiKey?: string;
3887
+ customModels?: readonly {
3888
+ upstreamModelId: string;
3889
+ label?: string | null;
3890
+ }[];
3891
+ },
3892
+ ): InternalRegistryProvider | null {
3893
+ const workspace = input.kind === "openrouter-workspace";
3894
+ const organization = input.kind === "openrouter-organization";
3895
+ const scoped = workspace || organization;
3896
+ const curated = organization ? [] : configuredOpenRouterCatalogModels(settings);
3897
+ const upstreamIds = new Set(curated.map((model) => model.upstreamModelId));
3898
+ const productIds = new Set(
3899
+ parseModelProvidersJson(settings.modelProvidersJson)
3900
+ .filter(
3901
+ (provider) =>
3902
+ provider.id !== WORKSPACE_OPENROUTER_PROVIDER_ID &&
3903
+ provider.id !== ORGANIZATION_OPENROUTER_PROVIDER_ID,
3904
+ )
3905
+ .flatMap((provider) =>
3906
+ provider.models.flatMap((model) => [model.id, ...(model.aliases ?? [])]),
3907
+ ),
3908
+ );
3909
+ const models: RegistryProvider["models"] = curated.map((model) => {
3910
+ const id = workspace
3911
+ ? workspaceOpenRouterProductId(model.upstreamModelId)
3912
+ : organization
3913
+ ? `${ORGANIZATION_OPENROUTER_MODEL_ID_PREFIX}${model.upstreamModelId}`
3914
+ : `${OPENROUTER_MODEL_ID_PREFIX}${model.upstreamModelId}`;
3915
+ const aliases = workspace
3916
+ ? model.aliases.map(workspaceOpenRouterProductId)
3917
+ : organization
3918
+ ? model.aliases.map((alias) => `${ORGANIZATION_OPENROUTER_MODEL_ID_PREFIX}${alias}`)
3919
+ : model.aliases;
3920
+ productIds.add(id);
3921
+ for (const alias of aliases) productIds.add(alias);
3922
+ return {
3923
+ id,
3924
+ upstreamModelId: model.upstreamModelId,
3925
+ aliases,
3926
+ label: model.label,
3927
+ ...(model.shortLabel ? { shortLabel: model.shortLabel } : {}),
3928
+ capabilities: model.capabilities,
3929
+ ...(model.contextWindowTokens === undefined
3930
+ ? {}
3931
+ : { contextWindowTokens: model.contextWindowTokens }),
3932
+ ...(model.effectiveContextWindowTokens === undefined
3933
+ ? {}
3934
+ : { effectiveContextWindowTokens: model.effectiveContextWindowTokens }),
3935
+ ...(model.autoCompactTokenLimit === undefined
3936
+ ? {}
3937
+ : { autoCompactTokenLimit: model.autoCompactTokenLimit }),
3938
+ toolOutputTruncationTokens:
3939
+ model.toolOutputTruncationTokens ?? settings.modelToolOutputTruncationTokens,
3940
+ };
3941
+ });
3942
+ if (scoped) {
3943
+ for (const custom of input.customModels ?? []) {
3944
+ const productId = `${workspace ? WORKSPACE_OPENROUTER_MODEL_ID_PREFIX : ORGANIZATION_OPENROUTER_MODEL_ID_PREFIX}${custom.upstreamModelId}`;
3945
+ if (upstreamIds.has(custom.upstreamModelId) || productIds.has(productId)) continue;
3946
+ upstreamIds.add(custom.upstreamModelId);
3947
+ productIds.add(productId);
3948
+ models.push({
3949
+ id: productId,
3950
+ upstreamModelId: custom.upstreamModelId,
3951
+ aliases: [],
3952
+ label: custom.label?.trim() || custom.upstreamModelId,
3953
+ capabilities: openRouterCustomModelCapabilities(settings),
3954
+ toolOutputTruncationTokens: settings.modelToolOutputTruncationTokens,
3955
+ });
3956
+ }
3228
3957
  }
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`,
3958
+ if (models.length === 0) return null;
3959
+ const defaultHeaders: Record<string, string> = {
3960
+ "x-title": "OpenGeni",
3961
+ ...(settings.publicBaseUrl ? { "http-referer": settings.publicBaseUrl } : {}),
3962
+ };
3963
+ return {
3964
+ kind: input.kind,
3965
+ id: workspace
3966
+ ? WORKSPACE_OPENROUTER_PROVIDER_ID
3967
+ : organization
3968
+ ? ORGANIZATION_OPENROUTER_PROVIDER_ID
3969
+ : OPENROUTER_PROVIDER_ID,
3970
+ label: workspace ? "Your OpenRouter" : organization ? "Organization OpenRouter" : "OpenRouter",
3971
+ api: "chat",
3972
+ wireProfile: "openai",
3973
+ baseUrl: OPENROUTER_BASE_URL,
3974
+ ...(input.apiKey ? { apiKey: input.apiKey } : {}),
3975
+ defaultHeaders,
3976
+ publicDefaultHeaderNames: Object.keys(defaultHeaders),
3977
+ models,
3978
+ };
3979
+ }
3980
+
3981
+ function configuredRegistryProviders(settings: Settings): InternalRegistryProvider[] {
3982
+ const providers = parseModelProvidersJson(settings.modelProvidersJson);
3983
+ const injected: InternalRegistryProvider[] = [...providers];
3984
+ if (settings.vercelAiGatewayApiKey && configuredGatewayCatalogModels(settings).length > 0) {
3985
+ injected.push(
3986
+ gatewayRegistryProvider(settings, {
3987
+ kind: "vercel-gateway-managed",
3988
+ apiKey: settings.vercelAiGatewayApiKey,
3989
+ }),
3232
3990
  );
3233
3991
  }
3234
- return [
3235
- ...providers,
3236
- gatewayRegistryProvider(settings, {
3237
- kind: "vercel-gateway-managed",
3238
- apiKey: settings.vercelAiGatewayApiKey,
3239
- }),
3240
- ];
3992
+ const openrouter = settings.openrouterApiKey
3993
+ ? openRouterRegistryProvider(settings, {
3994
+ kind: "openrouter-managed",
3995
+ apiKey: settings.openrouterApiKey,
3996
+ })
3997
+ : null;
3998
+ if (openrouter) injected.push(openrouter);
3999
+ return injected;
3241
4000
  }
3242
4001
 
3243
4002
  /** Static catalog overlay; it contains no concrete workspace credential. */
3244
- export function withWorkspaceGatewayCatalogProvider(settings: Settings): Settings {
4003
+ export function withWorkspaceGatewayCatalogProvider(
4004
+ settings: Settings,
4005
+ customModels: readonly {
4006
+ upstreamModelId: string;
4007
+ label?: string | null;
4008
+ }[] = [],
4009
+ ): Settings {
3245
4010
  const providers = parseModelProvidersJson(settings.modelProvidersJson);
3246
- if (providers.some((provider) => provider.id === WORKSPACE_GATEWAY_PROVIDER_ID)) {
3247
- return settings;
3248
- }
4011
+ const withoutWorkspace = providers.filter(
4012
+ (provider) => provider.id !== WORKSPACE_GATEWAY_PROVIDER_ID,
4013
+ );
4014
+ const curatedCount = configuredGatewayCatalogModels(settings).length;
4015
+ if (curatedCount === 0 && customModels.length === 0) return settings;
3249
4016
  return {
3250
4017
  ...settings,
3251
4018
  modelProvidersJson: JSON.stringify([
3252
- ...providers,
3253
- gatewayRegistryProvider(settings, { kind: "vercel-gateway-workspace" }),
4019
+ ...withoutWorkspace,
4020
+ gatewayRegistryProvider(settings, {
4021
+ kind: "vercel-gateway-workspace",
4022
+ customModels,
4023
+ }),
3254
4024
  ]),
3255
4025
  };
3256
4026
  }
3257
4027
 
3258
4028
  /** Runtime overlay after the worker resolves the workspace's encrypted key. */
3259
- export function withWorkspaceGatewayCredential(settings: Settings, apiKey: string): Settings {
4029
+ export function withWorkspaceGatewayCredential(
4030
+ settings: Settings,
4031
+ apiKey: string,
4032
+ customModels: readonly {
4033
+ upstreamModelId: string;
4034
+ label?: string | null;
4035
+ }[] = [],
4036
+ ): Settings {
3260
4037
  if (!apiKey.trim()) {
3261
4038
  throw new Error("workspace AI Gateway credential is empty");
3262
4039
  }
3263
- const catalogSettings = withWorkspaceGatewayCatalogProvider(settings);
4040
+ const catalogSettings = withWorkspaceGatewayCatalogProvider(settings, customModels);
3264
4041
  const providers = parseModelProvidersJson(catalogSettings.modelProvidersJson).map((provider) =>
3265
4042
  provider.id === WORKSPACE_GATEWAY_PROVIDER_ID ? { ...provider, apiKey } : provider,
3266
4043
  );
3267
4044
  return { ...catalogSettings, modelProvidersJson: JSON.stringify(providers) };
3268
4045
  }
3269
4046
 
4047
+ /** Static OpenRouter catalog overlay; it contains no concrete workspace credential. */
4048
+ export function withWorkspaceOpenRouterCatalogProvider(
4049
+ settings: Settings,
4050
+ customModels: readonly {
4051
+ upstreamModelId: string;
4052
+ label?: string | null;
4053
+ }[] = [],
4054
+ ): Settings {
4055
+ const providers = parseModelProvidersJson(settings.modelProvidersJson);
4056
+ const withoutWorkspace = providers.filter(
4057
+ (provider) => provider.id !== WORKSPACE_OPENROUTER_PROVIDER_ID,
4058
+ );
4059
+ const provider = openRouterRegistryProvider(settings, {
4060
+ kind: "openrouter-workspace",
4061
+ customModels,
4062
+ });
4063
+ if (!provider) return settings;
4064
+ return {
4065
+ ...settings,
4066
+ modelProvidersJson: JSON.stringify([...withoutWorkspace, provider]),
4067
+ };
4068
+ }
4069
+
4070
+ /** Runtime overlay after the worker resolves the workspace's encrypted OpenRouter key. */
4071
+ export function withWorkspaceOpenRouterCredential(
4072
+ settings: Settings,
4073
+ apiKey: string,
4074
+ customModels: readonly {
4075
+ upstreamModelId: string;
4076
+ label?: string | null;
4077
+ }[] = [],
4078
+ ): Settings {
4079
+ if (!apiKey.trim()) {
4080
+ throw new Error("workspace OpenRouter credential is empty");
4081
+ }
4082
+ const catalogSettings = withWorkspaceOpenRouterCatalogProvider(settings, customModels);
4083
+ const providers = parseModelProvidersJson(catalogSettings.modelProvidersJson).map((provider) =>
4084
+ provider.id === WORKSPACE_OPENROUTER_PROVIDER_ID ? { ...provider, apiKey } : provider,
4085
+ );
4086
+ return { ...catalogSettings, modelProvidersJson: JSON.stringify(providers) };
4087
+ }
4088
+
4089
+ /** Secret-free organization Vercel AI Gateway catalog overlay. */
4090
+ export function withOrganizationGatewayCatalogProvider(
4091
+ settings: Settings,
4092
+ customModels: readonly { upstreamModelId: string; label?: string | null }[] = [],
4093
+ ): Settings {
4094
+ if (customModels.length === 0) return settings;
4095
+ const providers = parseModelProvidersJson(settings.modelProvidersJson).filter(
4096
+ (provider) => provider.id !== ORGANIZATION_GATEWAY_PROVIDER_ID,
4097
+ );
4098
+ const provider = gatewayRegistryProvider(settings, {
4099
+ kind: "vercel-gateway-organization",
4100
+ customModels,
4101
+ });
4102
+ return { ...settings, modelProvidersJson: JSON.stringify([...providers, provider]) };
4103
+ }
4104
+
4105
+ export function withOrganizationGatewayCredential(
4106
+ settings: Settings,
4107
+ apiKey: string,
4108
+ customModels: readonly { upstreamModelId: string; label?: string | null }[] = [],
4109
+ ): Settings {
4110
+ if (!apiKey.trim()) throw new Error("organization AI Gateway credential is empty");
4111
+ const catalog = withOrganizationGatewayCatalogProvider(settings, customModels);
4112
+ const providers = parseModelProvidersJson(catalog.modelProvidersJson).map((provider) =>
4113
+ provider.id === ORGANIZATION_GATEWAY_PROVIDER_ID ? { ...provider, apiKey } : provider,
4114
+ );
4115
+ return { ...catalog, modelProvidersJson: JSON.stringify(providers) };
4116
+ }
4117
+
4118
+ /** Secret-free organization OpenRouter catalog overlay. */
4119
+ export function withOrganizationOpenRouterCatalogProvider(
4120
+ settings: Settings,
4121
+ customModels: readonly { upstreamModelId: string; label?: string | null }[] = [],
4122
+ ): Settings {
4123
+ const providers = parseModelProvidersJson(settings.modelProvidersJson).filter(
4124
+ (provider) => provider.id !== ORGANIZATION_OPENROUTER_PROVIDER_ID,
4125
+ );
4126
+ const provider = openRouterRegistryProvider(settings, {
4127
+ kind: "openrouter-organization",
4128
+ customModels,
4129
+ });
4130
+ return provider
4131
+ ? { ...settings, modelProvidersJson: JSON.stringify([...providers, provider]) }
4132
+ : settings;
4133
+ }
4134
+
4135
+ export function withOrganizationOpenRouterCredential(
4136
+ settings: Settings,
4137
+ apiKey: string,
4138
+ customModels: readonly { upstreamModelId: string; label?: string | null }[] = [],
4139
+ ): Settings {
4140
+ if (!apiKey.trim()) throw new Error("organization OpenRouter credential is empty");
4141
+ const catalog = withOrganizationOpenRouterCatalogProvider(settings, customModels);
4142
+ const providers = parseModelProvidersJson(catalog.modelProvidersJson).map((provider) =>
4143
+ provider.id === ORGANIZATION_OPENROUTER_PROVIDER_ID ? { ...provider, apiKey } : provider,
4144
+ );
4145
+ return { ...catalog, modelProvidersJson: JSON.stringify(providers) };
4146
+ }
4147
+
3270
4148
  /** OpenAI GPT-5.6 Fast mode is 2× Standard list rates (service_tier fast/priority). */
3271
4149
  const GPT56_FAST_BILLING_MULTIPLIER_BPS = 20_000;
3272
4150
 
@@ -3318,6 +4196,8 @@ export function productShortLabelForModelId(modelId: string): string | null {
3318
4196
  return "5.6 Terra";
3319
4197
  case "gpt-5.6-luna":
3320
4198
  return "5.6 Luna";
4199
+ case "gpt-6-astra":
4200
+ return "6 Astra";
3321
4201
  default:
3322
4202
  return null;
3323
4203
  }
@@ -3353,7 +4233,11 @@ function builtinLatencyModesForModel(modelId: string): Array<{
3353
4233
  runnable: boolean;
3354
4234
  billingMultiplierBps?: number;
3355
4235
  }> {
3356
- if (isBuiltinGpt56ModelId(modelId) || modelId.startsWith("codex/gpt-5.6-")) {
4236
+ if (
4237
+ isBuiltinGpt56ModelId(modelId) ||
4238
+ modelId.startsWith("codex/gpt-5.6-") ||
4239
+ modelId === "codex/gpt-6-astra"
4240
+ ) {
3357
4241
  return [
3358
4242
  { id: "standard", upstream: "supported", runnable: true },
3359
4243
  {
@@ -3466,33 +4350,71 @@ function assertLatencyModeRunnable(
3466
4350
  }
3467
4351
  }
3468
4352
 
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" };
4353
+ function registryCredentialSource(provider: InternalRegistryProvider): CredentialSourceV1 {
4354
+ switch (provider.kind) {
4355
+ case "anonymous":
4356
+ return { kind: "deployment", mechanism: "none" };
4357
+ case "codex-subscription":
4358
+ return { kind: "connected_subscription", provider: "codex" };
4359
+ case "xai-subscription":
4360
+ return { kind: "connected_subscription", provider: "xai" };
4361
+ case "vercel-gateway-workspace":
4362
+ case "openrouter-workspace":
4363
+ return { kind: "workspace_connection", mechanism: "api_key" };
4364
+ case "vercel-gateway-organization":
4365
+ case "openrouter-organization":
4366
+ return { kind: "organization_connection", mechanism: "api_key" };
4367
+ case "api-key":
4368
+ case "vercel-gateway-managed":
4369
+ case "openrouter-managed":
4370
+ return { kind: "deployment", mechanism: "api_key" };
4371
+ default: {
4372
+ const _exhaustive: never = provider.kind;
4373
+ return _exhaustive;
4374
+ }
3481
4375
  }
3482
- return { kind: "deployment", mechanism: "api_key" };
3483
4376
  }
3484
4377
 
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" };
4378
+ function registryBilling(provider: InternalRegistryProvider): BillingAttributionV1 {
4379
+ switch (provider.kind) {
4380
+ case "anonymous":
4381
+ case "openrouter-managed":
4382
+ return { upstreamPayer: "deployment", metering: "external" };
4383
+ case "codex-subscription":
4384
+ case "xai-subscription":
4385
+ return { upstreamPayer: "connected_subscription", metering: "external" };
4386
+ case "vercel-gateway-workspace":
4387
+ case "openrouter-workspace":
4388
+ return { upstreamPayer: "workspace", metering: "external" };
4389
+ case "vercel-gateway-organization":
4390
+ case "openrouter-organization":
4391
+ return { upstreamPayer: "organization", metering: "external" };
4392
+ case "api-key":
4393
+ case "vercel-gateway-managed":
4394
+ return { upstreamPayer: "deployment", metering: "opengeni_credits" };
4395
+ default: {
4396
+ const _exhaustive: never = provider.kind;
4397
+ return _exhaustive;
4398
+ }
3494
4399
  }
3495
- return { upstreamPayer: "deployment", metering: "opengeni_credits" };
4400
+ }
4401
+
4402
+ function configuredCostForModel(
4403
+ settings: Settings,
4404
+ productModelId: string,
4405
+ credentialSource: CredentialSourceV1,
4406
+ ): ConfiguredModelCostClass {
4407
+ if (credentialSource.kind === "workspace_connection") return "workspace";
4408
+ if (credentialSource.kind === "organization_connection") return "organization";
4409
+ if (credentialSource.kind === "connected_subscription") return "subscription";
4410
+ return parseModelCostPolicyJson(settings.modelCostPolicyJson)[productModelId] ?? "credits";
4411
+ }
4412
+
4413
+ export function modelCostClassForConfiguredModel(
4414
+ _settings: Settings,
4415
+ model: Pick<ConfiguredModel, "cost">,
4416
+ ): ConfiguredModelCostClass {
4417
+ return model.cost;
3496
4418
  }
3497
4419
 
3498
4420
  function builtinCredentialSource(settings: Settings): CredentialSourceV1 {
@@ -3578,6 +4500,10 @@ function definitionVersionFor(
3578
4500
  billing: model.billing,
3579
4501
  executionLimits: model.executionLimits,
3580
4502
  capabilities: model.capabilities,
4503
+ // Workspace-facing free/credits classification is a separate live
4504
+ // deployment policy. Operators must drain/fence accepted turns before
4505
+ // changing it; it is intentionally not a second executable-definition
4506
+ // freeze inside TurnExecutionPolicyV1.
3581
4507
  ...(model.requestPolicy ? { requestPolicy: model.requestPolicy } : {}),
3582
4508
  pricing: model.pricing ?? null,
3583
4509
  });
@@ -3593,7 +4519,9 @@ function legacyImplicitOpenAiDefinitionVersionFor(
3593
4519
  ): string | null {
3594
4520
  if (provider.wireProfile !== "openai") return null;
3595
4521
  const { definitionVersion: _definitionVersion, ...modelWithoutVersion } = model;
3596
- return definitionVersionFor(modelWithoutVersion, provider, { includeWireProfile: false });
4522
+ return definitionVersionFor(modelWithoutVersion, provider, {
4523
+ includeWireProfile: false,
4524
+ });
3597
4525
  }
3598
4526
 
3599
4527
  /**
@@ -3619,7 +4547,10 @@ function builtinProviderLabel(settings: Pick<Settings, "openaiProvider">): strin
3619
4547
  * registry entry for the rest. Registry ids may not collide with the built-in
3620
4548
  * id — validateSettings rejects that at boot.
3621
4549
  */
3622
- export function configuredProviders(settings: Settings): ResolvedModelProvider[] {
4550
+ export function configuredProviders(
4551
+ settings: Settings,
4552
+ source: NodeJS.ProcessEnv = process.env,
4553
+ ): ResolvedModelProvider[] {
3623
4554
  const credentialSource = builtinCredentialSource(settings);
3624
4555
  const builtin: ResolvedModelProvider = {
3625
4556
  id: builtinProviderId(settings),
@@ -3650,7 +4581,7 @@ export function configuredProviders(settings: Settings): ResolvedModelProvider[]
3650
4581
  wireProfile: provider.wireProfile,
3651
4582
  builtin: false,
3652
4583
  baseUrl: provider.baseUrl,
3653
- apiKey: resolveProviderApiKey(provider),
4584
+ apiKey: resolveProviderApiKey(provider, source),
3654
4585
  defaultQuery: provider.defaultQuery,
3655
4586
  defaultHeaders: provider.defaultHeaders,
3656
4587
  publicDefaultQueryNames: provider.publicDefaultQueryNames,
@@ -3685,7 +4616,7 @@ export function withCodexCatalogProvider(settings: Settings): Settings {
3685
4616
  ...legacyModelCapabilities(settings, {
3686
4617
  reasoningEffort: true,
3687
4618
  hostedWebSearch: true,
3688
- vision: slug.startsWith("gpt-5.6-"),
4619
+ vision: slug.startsWith("gpt-5.6-") || slug === "gpt-6-astra",
3689
4620
  }),
3690
4621
  ...(builtinPromptCachingForModel(`${CODEX_MODEL_ID_PREFIX}${slug}`)
3691
4622
  ? {
@@ -3750,8 +4681,14 @@ export function withXaiSubscriptionCatalogProvider(settings: Settings): Settings
3750
4681
  { id: "standard", upstream: "supported", runnable: true },
3751
4682
  { id: "fast", upstream: "supported", runnable: true },
3752
4683
  ];
3753
- capabilities.hostedTools.xSearch = { upstream: "supported", runnable: true };
3754
- capabilities.hostedTools.imageGeneration = { upstream: "supported", runnable: true };
4684
+ capabilities.hostedTools.xSearch = {
4685
+ upstream: "supported",
4686
+ runnable: true,
4687
+ };
4688
+ capabilities.hostedTools.imageGeneration = {
4689
+ upstream: "supported",
4690
+ runnable: true,
4691
+ };
3755
4692
  return {
3756
4693
  id: `${XAI_SUBSCRIPTION_MODEL_ID_PREFIX}${slug}`,
3757
4694
  upstreamModelId: slug,
@@ -3769,7 +4706,10 @@ export function withXaiSubscriptionCatalogProvider(settings: Settings): Settings
3769
4706
  };
3770
4707
  }),
3771
4708
  };
3772
- return { ...settings, modelProvidersJson: JSON.stringify([...providers, provider]) };
4709
+ return {
4710
+ ...settings,
4711
+ modelProvidersJson: JSON.stringify([...providers, provider]),
4712
+ };
3773
4713
  }
3774
4714
 
3775
4715
  /**
@@ -3796,6 +4736,9 @@ export function policyProviderIdForModel(settings: Settings, modelId: string): s
3796
4736
  if (canonicalModelId.startsWith(WORKSPACE_GATEWAY_MODEL_ID_PREFIX)) {
3797
4737
  return WORKSPACE_GATEWAY_PROVIDER_ID;
3798
4738
  }
4739
+ if (canonicalModelId.startsWith(WORKSPACE_OPENROUTER_MODEL_ID_PREFIX)) {
4740
+ return WORKSPACE_OPENROUTER_PROVIDER_ID;
4741
+ }
3799
4742
  const configured = configuredModels(settings).find((model) => model.id === canonicalModelId);
3800
4743
  return configured?.providerId ?? builtinProviderId(settings);
3801
4744
  }
@@ -3823,15 +4766,21 @@ function resolvedExecutionLimits(
3823
4766
  function finalizeConfiguredModel(
3824
4767
  settings: Settings,
3825
4768
  provider: ResolvedModelProvider,
3826
- input: Omit<ConfiguredModel, "schemaVersion" | "definitionVersion" | "executionLimits">,
4769
+ input: Omit<ConfiguredModel, "schemaVersion" | "definitionVersion" | "executionLimits" | "cost">,
3827
4770
  ): ConfiguredModel {
3828
4771
  const requestPolicy =
3829
- provider.kind === "vercel-gateway-managed" || provider.kind === "vercel-gateway-workspace"
3830
- ? gatewayRequestPolicyForUpstreamModel(input.upstreamModelId)
4772
+ provider.kind === "vercel-gateway-managed" ||
4773
+ provider.kind === "vercel-gateway-workspace" ||
4774
+ provider.kind === "vercel-gateway-organization"
4775
+ ? gatewayRequestPolicyForUpstreamModel(
4776
+ input.upstreamModelId,
4777
+ configuredGatewayCatalogModels(settings),
4778
+ )
3831
4779
  : undefined;
3832
4780
  const modelWithoutVersion: Omit<ConfiguredModel, "definitionVersion"> = {
3833
4781
  schemaVersion: 1,
3834
4782
  ...input,
4783
+ cost: configuredCostForModel(settings, input.id, input.credentialSource),
3835
4784
  ...(requestPolicy ? { requestPolicy } : {}),
3836
4785
  executionLimits: resolvedExecutionLimits(settings, input),
3837
4786
  };
@@ -3882,10 +4831,13 @@ function assertUniqueModelIdentities(models: ConfiguredModel[]): void {
3882
4831
  * default false). De-duplicated by id (first wins) so the default model stays
3883
4832
  * first and the built-in allow-list takes precedence over registry entries.
3884
4833
  */
3885
- export function configuredModels(settings: Settings): ConfiguredModel[] {
4834
+ export function configuredModels(
4835
+ settings: Settings,
4836
+ source: NodeJS.ProcessEnv = process.env,
4837
+ ): ConfiguredModel[] {
3886
4838
  const builtinId = builtinProviderId(settings);
3887
4839
  const builtinLabel = builtinProviderLabel(settings);
3888
- const providers = configuredProviders(settings);
4840
+ const providers = configuredProviders(settings, source);
3889
4841
  const providerById = new Map(providers.map((provider) => [provider.id, provider]));
3890
4842
  const pricingSchedules = configuredModelPricingSchedules(settings);
3891
4843
  // The built-in (OpenAI/Azure) provider must NEVER claim a registry-namespaced
@@ -4016,7 +4968,12 @@ export function configuredModels(settings: Settings): ConfiguredModel[] {
4016
4968
  }
4017
4969
  }
4018
4970
  assertUniqueModelIdentities(out);
4019
- return out;
4971
+ const defaultIndex = out.findIndex(
4972
+ (model) => model.id === settings.openaiModel || model.aliases.includes(settings.openaiModel),
4973
+ );
4974
+ return defaultIndex > 0
4975
+ ? [out[defaultIndex]!, ...out.slice(0, defaultIndex), ...out.slice(defaultIndex + 1)]
4976
+ : out;
4020
4977
  }
4021
4978
 
4022
4979
  /** Resolve a known canonical id or alias. Unknown strings are returned unchanged. */
@@ -4087,11 +5044,50 @@ function settingsForTurnExecutionPolicy(settings: Settings, modelId: string): Se
4087
5044
  return withXaiSubscriptionCatalogProvider(settings);
4088
5045
  }
4089
5046
  if (modelId.startsWith(WORKSPACE_GATEWAY_MODEL_ID_PREFIX)) {
5047
+ // API/worker workspace boundaries may already have overlaid durable custom
5048
+ // rows and, at execution time, the decrypted workspace key. Re-applying an
5049
+ // empty static overlay would silently discard both. Only synthesize the
5050
+ // curated fallback when this exact model is not already executable.
5051
+ if (resolveModelProvider(settings, modelId)) {
5052
+ return settings;
5053
+ }
4090
5054
  return withWorkspaceGatewayCatalogProvider(settings);
4091
5055
  }
5056
+ if (modelId.startsWith(WORKSPACE_OPENROUTER_MODEL_ID_PREFIX)) {
5057
+ if (resolveModelProvider(settings, modelId)) {
5058
+ return settings;
5059
+ }
5060
+ return withWorkspaceOpenRouterCatalogProvider(settings);
5061
+ }
5062
+ if (modelId.startsWith(ORGANIZATION_GATEWAY_MODEL_ID_PREFIX)) {
5063
+ return resolveModelProvider(settings, modelId)
5064
+ ? settings
5065
+ : withOrganizationGatewayCatalogProvider(settings);
5066
+ }
5067
+ if (modelId.startsWith(ORGANIZATION_OPENROUTER_MODEL_ID_PREFIX)) {
5068
+ return resolveModelProvider(settings, modelId)
5069
+ ? settings
5070
+ : withOrganizationOpenRouterCatalogProvider(settings);
5071
+ }
4092
5072
  return settings;
4093
5073
  }
4094
5074
 
5075
+ /**
5076
+ * Resolve the static catalog identity used by an accepted turn. Subscription
5077
+ * overlays contain no account or bearer and do not prove connection readiness;
5078
+ * callers must keep their live credential/readiness gate authoritative.
5079
+ */
5080
+ export function resolveModelProviderForTurn(
5081
+ settings: Settings,
5082
+ modelId: string,
5083
+ ): ReturnType<typeof resolveModelProvider> {
5084
+ const catalogSettings = settingsForTurnExecutionPolicy(settings, modelId);
5085
+ return resolveModelProvider(
5086
+ catalogSettings,
5087
+ canonicalizeConfiguredModelId(catalogSettings, modelId),
5088
+ );
5089
+ }
5090
+
4095
5091
  /**
4096
5092
  * Build a trusted, secret-safe execution policy from the normalized catalog.
4097
5093
  * The Codex overlay here contains static product/provider identity only; it
@@ -4203,10 +5199,9 @@ export function assertTurnExecutionPolicyMatchesConfigV1(
4203
5199
 
4204
5200
  /**
4205
5201
  * 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.
5202
+ * flat defaults → registry model flat/scheduled pricing → explicit
5203
+ * OPENGENI_MODEL_PRICING_JSON. Explicit entries may be legacy flat prices or a
5204
+ * complete schedule, and always replace the lower-precedence schedule.
4210
5205
  */
4211
5206
  export function configuredModelPricingSchedules(
4212
5207
  settings: Settings,
@@ -4228,7 +5223,7 @@ export function configuredModelPricingSchedules(
4228
5223
  const configured = Object.fromEntries(
4229
5224
  Object.entries(parseModelPricingJson(settings.modelPricingJson)).map(([model, pricing]) => [
4230
5225
  model,
4231
- { default: pricing },
5226
+ normalizeModelPricingSchedule(pricing),
4232
5227
  ]),
4233
5228
  );
4234
5229
  return {
@@ -4409,6 +5404,37 @@ export function calculateGatewayReportedCostMicros(
4409
5404
  .creditCostMicros;
4410
5405
  }
4411
5406
 
5407
+ type GatewayReportedCostDecimal = {
5408
+ providerNumerator: bigint;
5409
+ decimalScale: bigint;
5410
+ providerCostMicros: number;
5411
+ };
5412
+
5413
+ function parseGatewayReportedCostDecimal(inferenceCostUsd: string): GatewayReportedCostDecimal {
5414
+ const match = /^(0|[1-9]\d*)(?:\.(\d{1,18}))?$/.exec(inferenceCostUsd);
5415
+ if (!match) {
5416
+ throw new Error("Invalid AI Gateway inference cost");
5417
+ }
5418
+ const fraction = match[2] ?? "";
5419
+ const decimalDigits = BigInt(`${match[1]}${fraction}`);
5420
+ const decimalScale = 10n ** BigInt(fraction.length);
5421
+ const providerNumerator = decimalDigits * 1_000_000n;
5422
+ const providerCostMicros = (providerNumerator + decimalScale - 1n) / decimalScale;
5423
+ if (providerCostMicros > BigInt(Number.MAX_SAFE_INTEGER)) {
5424
+ throw new Error("AI Gateway inference cost exceeds the supported billing range");
5425
+ }
5426
+ return {
5427
+ providerNumerator,
5428
+ decimalScale,
5429
+ providerCostMicros: Number(providerCostMicros),
5430
+ };
5431
+ }
5432
+
5433
+ /** Exact provider-reported Gateway cost without requiring an OpenGeni price schedule. */
5434
+ export function calculateGatewayReportedProviderCostMicros(inferenceCostUsd: string): number {
5435
+ return parseGatewayReportedCostDecimal(inferenceCostUsd).providerCostMicros;
5436
+ }
5437
+
4412
5438
  export function calculateGatewayReportedCostBreakdown(
4413
5439
  settings: Settings,
4414
5440
  model: string,
@@ -4420,27 +5446,17 @@ export function calculateGatewayReportedCostBreakdown(
4420
5446
  throw new Error(`Missing model pricing for ${model}`);
4421
5447
  }
4422
5448
  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;
5449
+ const { providerNumerator, decimalScale, providerCostMicros } =
5450
+ parseGatewayReportedCostDecimal(inferenceCostUsd);
4432
5451
  const marginBps = BigInt(10_000 + (pricing.marginBps ?? 0));
4433
5452
  const numerator = providerNumerator * marginBps;
4434
5453
  const denominator = decimalScale * 10_000n;
4435
5454
  const creditMicros = (numerator + denominator - 1n) / denominator;
4436
- if (
4437
- providerMicros > BigInt(Number.MAX_SAFE_INTEGER) ||
4438
- creditMicros > BigInt(Number.MAX_SAFE_INTEGER)
4439
- ) {
5455
+ if (creditMicros > BigInt(Number.MAX_SAFE_INTEGER)) {
4440
5456
  throw new Error("AI Gateway inference cost exceeds the supported billing range");
4441
5457
  }
4442
5458
  return {
4443
- providerCostMicros: Number(providerMicros),
5459
+ providerCostMicros,
4444
5460
  creditCostMicros: Number(creditMicros),
4445
5461
  };
4446
5462
  }
@@ -4890,7 +5906,9 @@ export function parseMcpServers(raw: string | undefined): unknown[] | undefined
4890
5906
  }
4891
5907
  }
4892
5908
 
4893
- export function parseModelPricingJson(raw: string): Record<string, ModelPricing> {
5909
+ export function parseModelPricingJson(
5910
+ raw: string,
5911
+ ): Record<string, ModelPricing | ModelPricingScheduleV1> {
4894
5912
  if (!raw.trim() || raw.trim() === "{}") {
4895
5913
  return {};
4896
5914
  }
@@ -4904,12 +5922,12 @@ export function parseModelPricingJson(raw: string): Record<string, ModelPricing>
4904
5922
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
4905
5923
  throw new Error("OPENGENI_MODEL_PRICING_JSON must be a JSON object keyed by model name");
4906
5924
  }
4907
- const out: Record<string, ModelPricing> = {};
5925
+ const out: Record<string, ModelPricing | ModelPricingScheduleV1> = {};
4908
5926
  for (const [model, value] of Object.entries(parsed)) {
4909
5927
  if (!model.trim()) {
4910
5928
  throw new Error("OPENGENI_MODEL_PRICING_JSON contains an empty model name");
4911
5929
  }
4912
- out[model] = ModelPricingSchema.parse(value);
5930
+ out[model] = z.union([ModelPricingSchema, ModelPricingScheduleSchema]).parse(value);
4913
5931
  }
4914
5932
  return out;
4915
5933
  }
@@ -5120,12 +6138,19 @@ function calculateEntryCostMicros(pricing: ModelPricing, entry: ModelUsageInput)
5120
6138
  const inputTokens = positiveInt(entry.inputTokens);
5121
6139
  const outputTokens = positiveInt(entry.outputTokens);
5122
6140
  const cachedTokens = Math.min(inputTokens, cachedInputTokens(entry));
5123
- const uncachedInputTokens = Math.max(0, inputTokens - cachedTokens);
6141
+ const cacheWriteTokens = Math.min(
6142
+ Math.max(0, inputTokens - cachedTokens),
6143
+ cacheWriteInputTokens(entry),
6144
+ );
6145
+ const uncachedInputTokens = Math.max(0, inputTokens - cachedTokens - cacheWriteTokens);
5124
6146
  const cachedInputRate =
5125
6147
  pricing.cachedInputMicrosPerMillionTokens ?? pricing.inputMicrosPerMillionTokens;
6148
+ const cacheWriteRate =
6149
+ pricing.cacheWriteMicrosPerMillionTokens ?? pricing.inputMicrosPerMillionTokens;
5126
6150
  return (
5127
6151
  Math.ceil((uncachedInputTokens * pricing.inputMicrosPerMillionTokens) / 1_000_000) +
5128
6152
  Math.ceil((cachedTokens * cachedInputRate) / 1_000_000) +
6153
+ Math.ceil((cacheWriteTokens * cacheWriteRate) / 1_000_000) +
5129
6154
  Math.ceil((outputTokens * pricing.outputMicrosPerMillionTokens) / 1_000_000)
5130
6155
  );
5131
6156
  }
@@ -5146,6 +6171,19 @@ function cachedInputTokens(entry: ModelUsageInput): number {
5146
6171
  return total;
5147
6172
  }
5148
6173
 
6174
+ function cacheWriteInputTokens(entry: ModelUsageInput): number {
6175
+ const details = Array.isArray(entry.inputTokensDetails)
6176
+ ? entry.inputTokensDetails
6177
+ : entry.inputTokensDetails
6178
+ ? [entry.inputTokensDetails]
6179
+ : [];
6180
+ let total = 0;
6181
+ for (const detail of details) {
6182
+ total += positiveInt(detail.cache_write_tokens ?? detail.cacheWriteTokens);
6183
+ }
6184
+ return total;
6185
+ }
6186
+
5149
6187
  function positiveInt(value: unknown): number {
5150
6188
  return typeof value === "number" && Number.isFinite(value) && value > 0 ? Math.floor(value) : 0;
5151
6189
  }
@@ -5314,8 +6352,16 @@ function isDigestPinnedModalDesktopImage(settings: Settings): boolean {
5314
6352
  );
5315
6353
  }
5316
6354
 
5317
- function validateSettings(settings: Settings): void {
6355
+ function validateSettings(settings: Settings, source: NodeJS.ProcessEnv = process.env): void {
5318
6356
  temporalConnectionOptions(settings);
6357
+ if (
6358
+ settings.organizationUserSetupEmailTokenTransport === "query" &&
6359
+ !settings.organizationUserSetupQueryEdgeSanitizationConfirmed
6360
+ ) {
6361
+ throw new Error(
6362
+ "OPENGENI_ORGANIZATION_USER_SETUP_QUERY_EDGE_SANITIZATION_CONFIRMED=true is required when OPENGENI_ORGANIZATION_USER_SETUP_EMAIL_TOKEN_TRANSPORT=query",
6363
+ );
6364
+ }
5319
6365
  if (settings.goalIdleBackoffMs.some((delayMs) => delayMs > settings.goalIdleBackoffMaxMs)) {
5320
6366
  throw new Error(
5321
6367
  `OPENGENI_GOAL_IDLE_BACKOFF_MS entries must not exceed OPENGENI_GOAL_IDLE_BACKOFF_MAX_MS (${settings.goalIdleBackoffMaxMs})`,
@@ -5401,6 +6447,24 @@ function validateSettings(settings: Settings): void {
5401
6447
  );
5402
6448
  }
5403
6449
  }
6450
+ if (settings.mcpOauthEnabled) {
6451
+ if (settings.productAccessMode === "configured") {
6452
+ throw new Error(
6453
+ "OPENGENI_MCP_OAUTH_ENABLED=true requires managed or local product access mode",
6454
+ );
6455
+ }
6456
+ const publicOrigin = canonicalPublicOrigin(settings.publicBaseUrl);
6457
+ if (!publicOrigin) {
6458
+ throw new Error(
6459
+ "OPENGENI_PUBLIC_BASE_URL must be a credential-free HTTP(S) origin when OPENGENI_MCP_OAUTH_ENABLED=true",
6460
+ );
6461
+ }
6462
+ if (!publicOrigin.startsWith("https://") && !["local", "test"].includes(settings.environment)) {
6463
+ throw new Error(
6464
+ "OPENGENI_PUBLIC_BASE_URL must use https when OPENGENI_MCP_OAUTH_ENABLED=true outside local/test",
6465
+ );
6466
+ }
6467
+ }
5404
6468
  environmentsEncryptionKeyBytes(settings);
5405
6469
  if (settings.integrationsEnabled) {
5406
6470
  if (settings.productAccessMode === "managed" && !settings.publicBaseUrl) {
@@ -5620,15 +6684,6 @@ function validateSettings(settings: Settings): void {
5620
6684
  if (settings.productAccessMode !== "managed" && settings.billingMode === "stripe") {
5621
6685
  throw new Error("OPENGENI_BILLING_MODE=stripe requires OPENGENI_PRODUCT_ACCESS_MODE=managed");
5622
6686
  }
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
6687
  if (settings.usageLimitsMode === "static") {
5633
6688
  const limits = configuredStaticUsageLimits(settings);
5634
6689
  if (Object.keys(limits).length === 0) {
@@ -5819,6 +6874,14 @@ function validateSettings(settings: Settings): void {
5819
6874
  throw new Error(`OPENGENI_MCP_SERVERS contains duplicate id ${server.id}`);
5820
6875
  }
5821
6876
  serverIds.add(server.id);
6877
+ if (
6878
+ server.connectionRef?.authoritySource === "host" &&
6879
+ !settings.hostMcpAuthoritySourceAdmissionEnabled
6880
+ ) {
6881
+ throw new Error(
6882
+ "OPENGENI_MCP_SERVERS host-owned connection refs require OPENGENI_HOST_MCP_AUTHORITY_SOURCE_ADMISSION_ENABLED=true after the whole API/worker fleet is upgraded",
6883
+ );
6884
+ }
5822
6885
  }
5823
6886
  // --- sandbox lease cadence invariant (fail fast at boot) ---
5824
6887
  // Holder TTLs are provider-neutral. Modal's finite hard/idle clocks and
@@ -5842,11 +6905,46 @@ function validateSettings(settings: Settings): void {
5842
6905
  `more often than the controller-heartbeat horizon.`,
5843
6906
  );
5844
6907
  }
6908
+ if (settings.sandboxDrainSnapshotTimeoutMs !== undefined) {
6909
+ const drainCaptureTimeoutMs = sandboxArchiveCaptureTimeoutMs({
6910
+ sandboxSnapshotTimeoutMs: effectiveSandboxDrainSnapshotTimeoutMs(settings),
6911
+ });
6912
+ const requiredTransitionWaitMs =
6913
+ reaperPeriod + drainCaptureTimeoutMs + SANDBOX_LIFECYCLE_RETRY_HANDOFF_GRACE_MS;
6914
+ if (requiredTransitionWaitMs > SANDBOX_LIFECYCLE_TRANSITION_MAX_WAIT_MS) {
6915
+ throw new Error(
6916
+ `OPENGENI_SANDBOX_DRAIN_SNAPSHOT_TIMEOUT_MS (${settings.sandboxDrainSnapshotTimeoutMs}) ` +
6917
+ `requires a sandbox lifecycle transition wait of ${requiredTransitionWaitMs}ms after ` +
6918
+ `one reaper period and provider settlement, exceeding the ` +
6919
+ `${SANDBOX_LIFECYCLE_TRANSITION_MAX_WAIT_MS}ms limit. Lower the drain snapshot timeout ` +
6920
+ `or OPENGENI_SANDBOX_LEASE_REAPER_PERIOD_MS.`,
6921
+ );
6922
+ }
6923
+ }
6924
+ // A backend rollout does not rewrite or synchronously drain existing
6925
+ // leases. Preserve enough deadline-rotation headroom for historical Modal
6926
+ // leases even when the deployment default has moved to another backend.
6927
+ const rotationLeadMs = settings.sandboxRotationLeadMs;
6928
+ const ordinaryCaptureTimeoutMs = sandboxArchiveCaptureTimeoutMs(settings);
6929
+ const drainCaptureTimeoutMs = sandboxArchiveCaptureTimeoutMs({
6930
+ sandboxSnapshotTimeoutMs: effectiveSandboxDrainSnapshotTimeoutMs(settings),
6931
+ });
6932
+ const providerDeadlineCaptureTimeoutMs = Math.max(
6933
+ ordinaryCaptureTimeoutMs,
6934
+ drainCaptureTimeoutMs,
6935
+ );
6936
+ if (!(rotationLeadMs > providerDeadlineCaptureTimeoutMs + reaperPeriod)) {
6937
+ throw new Error(
6938
+ `OPENGENI_SANDBOX_ROTATION_LEAD_MS (${rotationLeadMs}) must exceed the ` +
6939
+ `largest durable snapshot or drain capture timeout plus one reaper period ` +
6940
+ `(${providerDeadlineCaptureTimeoutMs + reaperPeriod}), including for persisted Modal ` +
6941
+ `leases after a default-backend rollout.`,
6942
+ );
6943
+ }
5845
6944
  if (settings.sandboxBackend === "modal") {
5846
6945
  const idleGraceMs = settings.sandboxIdleGraceMs;
5847
6946
  const lifecycle = effectiveSandboxLifecycle(settings, "modal");
5848
6947
  const providerLifetimeMs = lifecycle.hardLifetimeMs!;
5849
- const rotationLeadMs = lifecycle.rotationLeadMs!;
5850
6948
  const idleTimeoutMs = lifecycle.providerIdleTimeoutMs!;
5851
6949
  if (!(idleTimeoutMs <= providerLifetimeMs)) {
5852
6950
  throw new Error(
@@ -5861,13 +6959,6 @@ function validateSettings(settings: Settings): void {
5861
6959
  `OPENGENI_MODAL_TIMEOUT_SECONDS*1000 (${providerLifetimeMs}).`,
5862
6960
  );
5863
6961
  }
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
6962
  if (!(viewerTtl < idleTimeoutMs)) {
5872
6963
  throw new Error(
5873
6964
  `OPENGENI_SANDBOX_VIEWER_HOLDER_TTL_MS (${viewerTtl}) must be strictly less than the effective box ` +
@@ -5925,15 +7016,23 @@ function validateSettings(settings: Settings): void {
5925
7016
  "OPENGENI_STREAM_TOKEN_SECRET to enable the live desktop stream.",
5926
7017
  );
5927
7018
  }
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.
7019
+ if (settings.modelCatalogSource === "code") {
7020
+ validateModelCatalogSettings(settings, source);
7021
+ } else {
7022
+ // Database mode resolves membership asynchronously. Only the independent
7023
+ // deployment funding JSON is parsed here; env catalog and note inputs are
7024
+ // intentionally ignored until resolveCatalogSettings applies the singleton.
7025
+ parseModelCostPolicyJson(settings.modelCostPolicyJson);
7026
+ }
7027
+ }
7028
+
7029
+ /** Validate one fully resolved, secret-bearing executable catalog. */
7030
+ export function validateModelCatalogSettings(
7031
+ settings: Settings,
7032
+ source: NodeJS.ProcessEnv = process.env,
7033
+ ): ConfiguredModel[] {
7034
+ const costPolicy = parseModelCostPolicyJson(settings.modelCostPolicyJson);
7035
+ const notes = parseModelNotesJson(settings.modelNotesJson);
5937
7036
  const registryProviders = parseModelProvidersJson(settings.modelProvidersJson);
5938
7037
  const builtinId = builtinProviderId(settings);
5939
7038
  const providerIds = new Set<string>();
@@ -5941,12 +7040,20 @@ function validateSettings(settings: Settings): void {
5941
7040
  if (
5942
7041
  provider.kind === "vercel-gateway-managed" ||
5943
7042
  provider.kind === "vercel-gateway-workspace" ||
7043
+ provider.kind === "vercel-gateway-organization" ||
7044
+ provider.kind === "openrouter-workspace" ||
7045
+ provider.kind === "openrouter-organization" ||
5944
7046
  provider.kind === "xai-subscription"
5945
7047
  ) {
5946
7048
  throw new Error(
5947
7049
  `OPENGENI_MODEL_PROVIDERS_JSON provider kind ${provider.kind} is reserved for a reviewed OpenGeni credential broker`,
5948
7050
  );
5949
7051
  }
7052
+ if (RESERVED_MODEL_PROVIDER_IDS.has(provider.id)) {
7053
+ throw new Error(
7054
+ `OPENGENI_MODEL_PROVIDERS_JSON provider id ${provider.id} is reserved for a reviewed OpenGeni provider`,
7055
+ );
7056
+ }
5950
7057
  if (provider.id === builtinId) {
5951
7058
  throw new Error(
5952
7059
  `OPENGENI_MODEL_PROVIDERS_JSON provider id ${provider.id} collides with the built-in provider id`,
@@ -5961,7 +7068,7 @@ function validateSettings(settings: Settings): void {
5961
7068
  if (
5962
7069
  provider.kind !== "codex-subscription" &&
5963
7070
  provider.kind !== "anonymous" &&
5964
- !resolveProviderApiKey(provider)
7071
+ !resolveProviderApiKey(provider, source)
5965
7072
  ) {
5966
7073
  throw new Error(
5967
7074
  `OPENGENI_MODEL_PROVIDERS_JSON provider ${provider.id} requires a resolvable API key (set apiKey or apiKeyEnv)`,
@@ -5971,7 +7078,65 @@ function validateSettings(settings: Settings): void {
5971
7078
  // Materialize the normalized catalog at boot so canonical product ids,
5972
7079
  // aliases, definition digests, and capability/pricing normalization are
5973
7080
  // validated even when managed billing is disabled.
5974
- configuredModels(settings);
7081
+ const models = configuredModels(settings, source);
7082
+ const defaultCatalogSettings = settingsForTurnExecutionPolicy(settings, settings.openaiModel);
7083
+ const defaultCatalogModels =
7084
+ defaultCatalogSettings === settings ? models : configuredModels(defaultCatalogSettings, source);
7085
+ if (models.length === 0 && defaultCatalogModels.length === 0) {
7086
+ throw new Error("The resolved model catalog contains no executable models");
7087
+ }
7088
+ const defaultModelId = canonicalizeConfiguredModelId(
7089
+ defaultCatalogSettings,
7090
+ settings.openaiModel,
7091
+ );
7092
+ if (!defaultCatalogModels.some((model) => model.id === defaultModelId)) {
7093
+ throw new Error(
7094
+ `The default model ${settings.openaiModel} is not executable in the resolved model catalog`,
7095
+ );
7096
+ }
7097
+
7098
+ const deploymentProductIds = new Set(
7099
+ models.filter((model) => model.credentialSource.kind === "deployment").map((model) => model.id),
7100
+ );
7101
+ const noteProductIds = new Set(models.map((model) => model.id));
7102
+ for (const model of configuredGatewayCatalogModels(settings)) {
7103
+ deploymentProductIds.add(model.productId);
7104
+ noteProductIds.add(model.productId);
7105
+ noteProductIds.add(model.workspaceProductId);
7106
+ }
7107
+ for (const model of configuredOpenRouterCatalogModels(settings)) {
7108
+ const productId = `${OPENROUTER_MODEL_ID_PREFIX}${model.upstreamModelId}`;
7109
+ deploymentProductIds.add(productId);
7110
+ noteProductIds.add(productId);
7111
+ noteProductIds.add(`${WORKSPACE_OPENROUTER_MODEL_ID_PREFIX}${model.upstreamModelId}`);
7112
+ }
7113
+ if (settings.modelCatalogSource === "code") {
7114
+ for (const productId of Object.keys(costPolicy)) {
7115
+ if (!deploymentProductIds.has(productId)) {
7116
+ throw new Error(
7117
+ `OPENGENI_MODEL_COST_POLICY_JSON references unknown deployment model ${productId}`,
7118
+ );
7119
+ }
7120
+ }
7121
+ }
7122
+ for (const productId of Object.keys(notes)) {
7123
+ if (!noteProductIds.has(productId)) {
7124
+ throw new Error(`OPENGENI_MODEL_NOTES_JSON references unknown catalog model ${productId}`);
7125
+ }
7126
+ }
7127
+
7128
+ if (settings.billingMode === "stripe" || settings.usageLimitsMode === "managed") {
7129
+ const pricing = configuredModelPricing(settings);
7130
+ const missing = models
7131
+ .filter((model) => model.cost === "credits" && !pricing[model.id])
7132
+ .map((model) => model.id);
7133
+ if (missing.length > 0) {
7134
+ throw new Error(
7135
+ `Missing model pricing for managed billing model(s): ${missing.join(", ")}. Set OPENGENI_MODEL_PRICING_JSON.`,
7136
+ );
7137
+ }
7138
+ }
7139
+ return models;
5975
7140
  }
5976
7141
 
5977
7142
  /**