@opengeni/config 0.22.0 → 0.22.5-canary.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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;
@@ -684,6 +689,24 @@ const SettingsSchema = z.object({
684
689
  // keep subscription model routing while disabling Codex voice input.
685
690
  voiceInputCodexExperimentalEnabled: EnvBoolean.default(false),
686
691
  modelPricingJson: z.string().default("{}"),
692
+ // Supported-model membership source. Database mode is resolved by the async
693
+ // core overlay; getSettings remains synchronous and env-only.
694
+ modelCatalogSource: z.enum(["code", "database"]).default("code"),
695
+ // Deployment-owned workspace-facing price policy. This is deliberately
696
+ // separate from catalog membership and upstream credential ownership.
697
+ // Shape: { "product/model-id": "free" | "credits" }.
698
+ modelCostPolicyJson: z.string().default("{}"),
699
+ // Optional per-product agent guidance. Database mode replaces this with the
700
+ // singleton document's validated modelNotes map.
701
+ modelNotesJson: z.string().default("{}"),
702
+ // Managed OpenRouter credential. The curated model table is injected in
703
+ // code/catalog-document resolution and never read from host provider JSON.
704
+ openrouterApiKey: z.string().optional(),
705
+ // Internal, secret-free catalog overlays populated only by
706
+ // applyModelCatalogDocument. They intentionally have no OPENGENI_* env
707
+ // binding so database mode cannot be bypassed with a second source.
708
+ resolvedGatewayModelsJson: z.string().optional(),
709
+ resolvedOpenRouterModelsJson: z.string().optional(),
687
710
  // Extra (non-built-in) model providers, declared by the host as a JSON
688
711
  // provider registry. Each entry carries its own base URL, API key, wire API
689
712
  // ("responses" | "chat") and the models it exposes. The models a client may
@@ -834,6 +857,13 @@ const SettingsSchema = z.object({
834
857
  // larger timeout—is what lets a session outlive one finite provider box.
835
858
  // Knob: OPENGENI_MODAL_TIMEOUT_SECONDS.
836
859
  modalTimeoutSeconds: z.coerce.number().int().positive().max(86_400).default(86_400),
860
+ // Optional provider reservations for every new Modal sandbox. CPU is measured
861
+ // in physical cores and may be fractional; memory is measured in MiB. Leave
862
+ // both unset to preserve Modal's provider defaults. The Agents adapter stores
863
+ // the resolved values in its session state so snapshot replacement and exact
864
+ // resume cannot silently change the reservation.
865
+ modalSandboxCpu: z.coerce.number().positive().optional(),
866
+ modalSandboxMemoryMiB: z.coerce.number().int().positive().optional(),
837
867
  modalTokenId: z.string().optional(),
838
868
  modalTokenSecret: z.string().optional(),
839
869
  modalEnvironment: z.string().optional(),
@@ -873,8 +903,8 @@ const SettingsSchema = z.object({
873
903
  // cell advertises mode "interactive" — the noVNC viewer can drive mouse+keyboard
874
904
  // into :0 (x11vnc runs without -viewonly). Turn it OFF for a genuinely read-only
875
905
  // deployment: the cell reports mode "read-only" and the client disables the
876
- // "Take control" affordance. Independent of computerUseReadOnly (the AGENT
877
- // driver); this gates the HUMAN viewer plane.
906
+ // "Take control" affordance. This gates the HUMAN viewer plane; agent
907
+ // interaction is authorized through managed ComputerSession tools.
878
908
  sandboxDesktopInteractive: EnvBoolean.default(true),
879
909
  // REAL PTY terminal toggle (P5.t): gates the ttyd pty-ws plane (7681) the API
880
910
  // mints over the SAME tunnel as the desktop. Defaults ON — the interactive
@@ -888,13 +918,7 @@ const SettingsSchema = z.object({
888
918
  // down→up restart. Defaults match the proven spike geometry (1280x800).
889
919
  streamResolutionWidth: z.coerce.number().int().positive().default(1280),
890
920
  streamResolutionHeight: z.coerce.number().int().positive().default(800),
891
- // P4.3 computer-use: the agent drives the SAME :0 humans watch (xdotool/XTEST +
892
- // scrot). Gated by sandboxDesktopEnabled + a desktop-capable backend in
893
- // buildAgentCapabilities; computerUseReadOnly:false is the agent-driver default
894
- // (it must click/type — the human viewer plane is the read-only one).
895
- computerUseEnabled: EnvBoolean.default(true),
896
- computerUseReadOnly: EnvBoolean.default(false),
897
- // P4.3 recording loop: ffmpeg x11grab of :0 → mp4/webm → @opengeni/storage.
921
+ // Recording loop: ffmpeg x11grab of :0 → mp4/webm → @opengeni/storage.
898
922
  // recordingMaxBytes caps the in-memory finalize buffer (≤ storage single-PUT);
899
923
  // recordingMaxSeconds is the ffmpeg -t hard ceiling (bounds a multi-day turn).
900
924
  recordingEnabled: EnvBoolean.default(true),
@@ -1266,6 +1290,10 @@ const SettingsSchema = z.object({
1266
1290
  betterAuthAllowedHosts: z.string().default(""),
1267
1291
  betterAuthCookieDomain: z.string().optional(),
1268
1292
  betterAuthTrustedOrigins: z.string().default(""),
1293
+ managedAuthGoogleClientId: z.string().optional(),
1294
+ managedAuthGoogleClientSecret: z.string().optional(),
1295
+ managedAuthGithubClientId: z.string().optional(),
1296
+ managedAuthGithubClientSecret: z.string().optional(),
1269
1297
  // Rolling browser login-slot compatibility. Repository/deployment default is
1270
1298
  // deliberately legacy; changing to broker is an operator-authorized rollout.
1271
1299
  managedAuthSessionSetMode: z.enum(["legacy", "dual", "broker"]).default("legacy"),
@@ -1805,6 +1833,7 @@ export const RegistryProviderKind = z.enum([
1805
1833
  "xai-subscription",
1806
1834
  "vercel-gateway-managed",
1807
1835
  "vercel-gateway-workspace",
1836
+ "openrouter-workspace",
1808
1837
  ]);
1809
1838
  export type RegistryProviderKind = z.infer<typeof RegistryProviderKind>;
1810
1839
 
@@ -1927,6 +1956,338 @@ const RegistryProviderSchema = z
1927
1956
  });
1928
1957
  export type RegistryProvider = z.infer<typeof RegistryProviderSchema>;
1929
1958
 
1959
+ export const OPENGENI_GATEWAY_PROVIDER_ID = "opengeni-gateway" as const;
1960
+ export const WORKSPACE_GATEWAY_PROVIDER_ID = "workspace-gateway" as const;
1961
+ export const WORKSPACE_GATEWAY_MODEL_ID_PREFIX = "workspace-gateway/" as const;
1962
+ export const OPENROUTER_PROVIDER_ID = "openrouter" as const;
1963
+ export const OPENROUTER_MODEL_ID_PREFIX = "openrouter/" as const;
1964
+ export const WORKSPACE_OPENROUTER_PROVIDER_ID = "workspace-openrouter" as const;
1965
+ export const WORKSPACE_OPENROUTER_MODEL_ID_PREFIX = "workspace-openrouter/" as const;
1966
+ export const OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1" as const;
1967
+
1968
+ const RESERVED_MODEL_PROVIDER_IDS = new Set<string>([
1969
+ "openai",
1970
+ "azure",
1971
+ CODEX_PROVIDER_ID,
1972
+ XAI_SUBSCRIPTION_PROVIDER_ID,
1973
+ OPENGENI_GATEWAY_PROVIDER_ID,
1974
+ WORKSPACE_GATEWAY_PROVIDER_ID,
1975
+ OPENROUTER_PROVIDER_ID,
1976
+ WORKSPACE_OPENROUTER_PROVIDER_ID,
1977
+ ]);
1978
+
1979
+ export const ModelCostClass = z.enum(["free", "credits"]);
1980
+ export type ModelCostClass = z.infer<typeof ModelCostClass>;
1981
+
1982
+ export const ConfiguredModelCostClass = z.enum(["free", "credits", "subscription", "workspace"]);
1983
+ export type ConfiguredModelCostClass = z.infer<typeof ConfiguredModelCostClass>;
1984
+
1985
+ const ModelNote = z
1986
+ .string()
1987
+ .max(500)
1988
+ .refine((value) => !/[\r\n|]/u.test(value), {
1989
+ message: "model notes must not contain newlines or the | field separator",
1990
+ });
1991
+
1992
+ export function parseModelCostPolicyJson(raw: string): Record<string, ModelCostClass> {
1993
+ let parsed: unknown;
1994
+ try {
1995
+ parsed = JSON.parse(raw);
1996
+ } catch (error) {
1997
+ throw new Error(
1998
+ `OPENGENI_MODEL_COST_POLICY_JSON must be valid JSON: ${error instanceof Error ? error.message : String(error)}`,
1999
+ { cause: error },
2000
+ );
2001
+ }
2002
+ return z.record(z.string().min(1), ModelCostClass).parse(parsed);
2003
+ }
2004
+
2005
+ export function parseModelNotesJson(raw: string): Record<string, string> {
2006
+ let parsed: unknown;
2007
+ try {
2008
+ parsed = JSON.parse(raw);
2009
+ } catch (error) {
2010
+ throw new Error(
2011
+ `OPENGENI_MODEL_NOTES_JSON must be valid JSON: ${error instanceof Error ? error.message : String(error)}`,
2012
+ { cause: error },
2013
+ );
2014
+ }
2015
+ return z.record(z.string().min(1), ModelNote).parse(parsed);
2016
+ }
2017
+
2018
+ export function configuredModelNotes(
2019
+ settings: Pick<Settings, "modelNotesJson">,
2020
+ ): Record<string, string> {
2021
+ return parseModelNotesJson(settings.modelNotesJson);
2022
+ }
2023
+
2024
+ export const GatewayCatalogModel = z
2025
+ .object({
2026
+ productId: z.string().min(1),
2027
+ workspaceProductId: z.string().min(1).startsWith(WORKSPACE_GATEWAY_MODEL_ID_PREFIX),
2028
+ upstreamModelId: z.string().min(1),
2029
+ label: z.string().min(1),
2030
+ shortLabel: z.string().min(1).max(64).optional(),
2031
+ providers: z.array(z.string().min(1)).min(1),
2032
+ implicitCaching: z.boolean().default(false),
2033
+ vision: z.boolean().default(false),
2034
+ inputFileMediaTypes: z.array(z.string().min(1)).default([]),
2035
+ contextWindowTokens: z.number().int().positive().default(1_000_000),
2036
+ effectiveContextWindowTokens: z.number().int().positive().default(900_000),
2037
+ autoCompactTokenLimit: z.number().int().positive().default(850_000),
2038
+ pricing: z.union([ModelPricingSchema, ModelPricingScheduleSchema]).optional(),
2039
+ credentialSource: z.never().optional(),
2040
+ billing: z.never().optional(),
2041
+ apiKey: z.never().optional(),
2042
+ })
2043
+ .strict();
2044
+ export type GatewayCatalogModel = z.infer<typeof GatewayCatalogModel>;
2045
+
2046
+ export const OpenRouterCatalogModel = z
2047
+ .object({
2048
+ upstreamModelId: z.string().min(1).endsWith(":free"),
2049
+ label: z.string().min(1),
2050
+ shortLabel: z.string().min(1).max(64).optional(),
2051
+ aliases: z.array(z.string().min(1)).default([]),
2052
+ capabilities: ModelCapabilitiesV1Schema,
2053
+ contextWindowTokens: z.number().int().positive().optional(),
2054
+ effectiveContextWindowTokens: z.number().int().positive().optional(),
2055
+ autoCompactTokenLimit: z.number().int().positive().optional(),
2056
+ toolOutputTruncationTokens: z.number().int().positive().optional(),
2057
+ credentialSource: z.never().optional(),
2058
+ billing: z.never().optional(),
2059
+ pricing: z.never().optional(),
2060
+ apiKey: z.never().optional(),
2061
+ })
2062
+ .strict();
2063
+ export type OpenRouterCatalogModel = z.infer<typeof OpenRouterCatalogModel>;
2064
+
2065
+ const DeploymentRegistryBaseUrl = z
2066
+ .string()
2067
+ .url()
2068
+ .superRefine((value, context) => {
2069
+ const url = new URL(value);
2070
+ if (url.username || url.password) {
2071
+ context.addIssue({
2072
+ code: "custom",
2073
+ message: "database catalog provider baseUrl must not contain userinfo",
2074
+ });
2075
+ }
2076
+ if (url.search) {
2077
+ context.addIssue({
2078
+ code: "custom",
2079
+ message: "database catalog provider baseUrl must not contain a query",
2080
+ });
2081
+ }
2082
+ if (url.hash) {
2083
+ context.addIssue({
2084
+ code: "custom",
2085
+ message: "database catalog provider baseUrl must not contain a fragment",
2086
+ });
2087
+ }
2088
+ });
2089
+
2090
+ const DeploymentRegistryProviderKind = z.enum(["api-key", "anonymous"]);
2091
+
2092
+ const DeploymentRegistryModelSchema = RegistryModelSchema.safeExtend({
2093
+ pricing: z.never().optional(),
2094
+ }).strict();
2095
+
2096
+ const DeploymentRegistryProviderSchema = RegistryProviderSchema.safeExtend({
2097
+ kind: DeploymentRegistryProviderKind.default("api-key"),
2098
+ baseUrl: DeploymentRegistryBaseUrl,
2099
+ models: z.array(DeploymentRegistryModelSchema).min(1),
2100
+ apiKey: z.never().optional(),
2101
+ apiKeyEnv: z.never().optional(),
2102
+ defaultHeaders: z.never().optional(),
2103
+ defaultQuery: z.never().optional(),
2104
+ publicDefaultHeaderNames: z.never().optional(),
2105
+ publicDefaultQueryNames: z.never().optional(),
2106
+ }).strict();
2107
+
2108
+ const DeploymentGatewayCatalogModelSchema = GatewayCatalogModel.safeExtend({
2109
+ pricing: z.never().optional(),
2110
+ }).strict();
2111
+
2112
+ export const ModelCatalogDocument = z
2113
+ .object({
2114
+ schemaVersion: z.literal(1),
2115
+ /** Canonical deployment default. Omission preserves the V1 first-built-in
2116
+ * fallback for existing documents; operators should set this explicitly
2117
+ * when cutting over a registry or connected-subscription default. */
2118
+ defaultModel: z.string().min(1).optional(),
2119
+ builtInModels: z.array(z.string().min(1)).min(1),
2120
+ registryProviders: z.array(DeploymentRegistryProviderSchema).default([]),
2121
+ gatewayModels: z.array(DeploymentGatewayCatalogModelSchema).default([]),
2122
+ openrouterModels: z.array(OpenRouterCatalogModel).default([]),
2123
+ modelNotes: z.record(z.string().min(1), ModelNote).default({}),
2124
+ billing: z.never().optional(),
2125
+ enabled: z.never().optional(),
2126
+ apiKey: z.never().optional(),
2127
+ bands: z.never().optional(),
2128
+ })
2129
+ .strict()
2130
+ .superRefine((document, context) => {
2131
+ const productIds = new Set<string>();
2132
+ const providerIds = new Set<string>();
2133
+ const gatewayUpstreamIds = new Set<string>();
2134
+ const add = (id: string, path: Array<string | number>): void => {
2135
+ if (/[\u000A\u000D|]/u.test(id)) {
2136
+ context.addIssue({
2137
+ code: "custom",
2138
+ path,
2139
+ message: "catalog product ids must not contain newlines or the | field separator",
2140
+ });
2141
+ }
2142
+ if (productIds.has(id)) {
2143
+ context.addIssue({
2144
+ code: "custom",
2145
+ path,
2146
+ message: `duplicate product id ${id}`,
2147
+ });
2148
+ }
2149
+ productIds.add(id);
2150
+ };
2151
+ document.builtInModels.forEach((id, index) => add(id, ["builtInModels", index]));
2152
+ document.registryProviders.forEach((provider, providerIndex) => {
2153
+ if (RESERVED_MODEL_PROVIDER_IDS.has(provider.id)) {
2154
+ context.addIssue({
2155
+ code: "custom",
2156
+ path: ["registryProviders", providerIndex, "id"],
2157
+ message: `provider id ${provider.id} is reserved for a reviewed OpenGeni provider`,
2158
+ });
2159
+ }
2160
+ if (providerIds.has(provider.id)) {
2161
+ context.addIssue({
2162
+ code: "custom",
2163
+ path: ["registryProviders", providerIndex, "id"],
2164
+ message: `duplicate provider id ${provider.id}`,
2165
+ });
2166
+ }
2167
+ providerIds.add(provider.id);
2168
+ provider.models.forEach((model, modelIndex) =>
2169
+ add(model.id, ["registryProviders", providerIndex, "models", modelIndex, "id"]),
2170
+ );
2171
+ });
2172
+ document.gatewayModels.forEach((model, index) => {
2173
+ if (gatewayUpstreamIds.has(model.upstreamModelId)) {
2174
+ context.addIssue({
2175
+ code: "custom",
2176
+ path: ["gatewayModels", index, "upstreamModelId"],
2177
+ message: `duplicate Gateway upstream model id ${model.upstreamModelId}`,
2178
+ });
2179
+ }
2180
+ gatewayUpstreamIds.add(model.upstreamModelId);
2181
+ add(model.productId, ["gatewayModels", index, "productId"]);
2182
+ add(model.workspaceProductId, ["gatewayModels", index, "workspaceProductId"]);
2183
+ });
2184
+ document.openrouterModels.forEach((model, index) =>
2185
+ add(`${OPENROUTER_MODEL_ID_PREFIX}${model.upstreamModelId}`, [
2186
+ "openrouterModels",
2187
+ index,
2188
+ "upstreamModelId",
2189
+ ]),
2190
+ );
2191
+ if (document.defaultModel && /[\u000A\u000D|]/u.test(document.defaultModel)) {
2192
+ context.addIssue({
2193
+ code: "custom",
2194
+ path: ["defaultModel"],
2195
+ message: "catalog default model must not contain newlines or the | field separator",
2196
+ });
2197
+ }
2198
+ if (
2199
+ document.defaultModel &&
2200
+ !productIds.has(document.defaultModel) &&
2201
+ !document.defaultModel.startsWith(CODEX_MODEL_ID_PREFIX) &&
2202
+ !document.defaultModel.startsWith(XAI_SUBSCRIPTION_MODEL_ID_PREFIX)
2203
+ ) {
2204
+ context.addIssue({
2205
+ code: "custom",
2206
+ path: ["defaultModel"],
2207
+ message:
2208
+ "catalog default model must reference deployment catalog membership or a connected-subscription product",
2209
+ });
2210
+ }
2211
+ for (const productId of Object.keys(document.modelNotes)) {
2212
+ if (!productIds.has(productId)) {
2213
+ context.addIssue({
2214
+ code: "custom",
2215
+ path: ["modelNotes", productId],
2216
+ message: "model note references a product id outside the deployment catalog",
2217
+ });
2218
+ }
2219
+ }
2220
+ });
2221
+ export type ModelCatalogDocument = z.infer<typeof ModelCatalogDocument>;
2222
+
2223
+ export function parseModelCatalogDocument(value: unknown): ModelCatalogDocument {
2224
+ return ModelCatalogDocument.parse(value);
2225
+ }
2226
+
2227
+ function deploymentRegistryProvidersWithHostCredentials(
2228
+ settings: Settings,
2229
+ providers: readonly z.infer<typeof DeploymentRegistryProviderSchema>[],
2230
+ ): RegistryProvider[] {
2231
+ const hostProviders = new Map(
2232
+ parseModelProvidersJson(settings.modelProvidersJson).map((provider) => [provider.id, provider]),
2233
+ );
2234
+ return providers.map((provider) => {
2235
+ if (provider.kind !== "api-key") return provider;
2236
+ const host = hostProviders.get(provider.id);
2237
+ if (!host || host.kind !== "api-key") {
2238
+ throw new Error(
2239
+ `database model catalog provider ${provider.id} has no matching host-authorized api-key transport`,
2240
+ );
2241
+ }
2242
+ const transportIdentity = (candidate: typeof provider | RegistryProvider) => ({
2243
+ kind: candidate.kind,
2244
+ baseUrl: candidate.baseUrl,
2245
+ api: candidate.api,
2246
+ wireProfile: candidate.wireProfile,
2247
+ });
2248
+ if (canonicalJson(transportIdentity(provider)) !== canonicalJson(transportIdentity(host))) {
2249
+ throw new Error(
2250
+ `database model catalog provider ${provider.id} does not match its host-authorized transport`,
2251
+ );
2252
+ }
2253
+ return {
2254
+ ...provider,
2255
+ ...(host.defaultHeaders === undefined ? {} : { defaultHeaders: host.defaultHeaders }),
2256
+ ...(host.defaultQuery === undefined ? {} : { defaultQuery: host.defaultQuery }),
2257
+ ...(host.publicDefaultHeaderNames === undefined
2258
+ ? {}
2259
+ : { publicDefaultHeaderNames: host.publicDefaultHeaderNames }),
2260
+ ...(host.publicDefaultQueryNames === undefined
2261
+ ? {}
2262
+ : { publicDefaultQueryNames: host.publicDefaultQueryNames }),
2263
+ ...(host.apiKey === undefined ? {} : { apiKey: host.apiKey }),
2264
+ ...(host.apiKeyEnv === undefined ? {} : { apiKeyEnv: host.apiKeyEnv }),
2265
+ };
2266
+ });
2267
+ }
2268
+
2269
+ /** Pure secret-free database catalog overlay. getSettings remains env-only. */
2270
+ export function applyModelCatalogDocument(settings: Settings, rawDocument: unknown): Settings {
2271
+ const document = parseModelCatalogDocument(rawDocument);
2272
+ const defaultModel = document.defaultModel ?? document.builtInModels[0]!;
2273
+ const resolved = {
2274
+ ...settings,
2275
+ openaiModel: defaultModel,
2276
+ // Keep the complete built-in membership, including the default. The worker
2277
+ // replaces openaiModel with the exact turn model; the run-scoped router
2278
+ // needs one stable built-in id in this allow-list so a bare provider model
2279
+ // is not temporarily claimed by OpenAI/Azure during name re-resolution.
2280
+ openaiAllowedModels: document.builtInModels.join(","),
2281
+ modelProvidersJson: JSON.stringify(
2282
+ deploymentRegistryProvidersWithHostCredentials(settings, document.registryProviders),
2283
+ ),
2284
+ resolvedGatewayModelsJson: JSON.stringify(document.gatewayModels),
2285
+ resolvedOpenRouterModelsJson: JSON.stringify(document.openrouterModels),
2286
+ modelNotesJson: JSON.stringify(document.modelNotes),
2287
+ };
2288
+ return resolved;
2289
+ }
2290
+
1930
2291
  export const IntegrationOAuthClientConfigSchema = z.object({
1931
2292
  clientId: z.string().min(1),
1932
2293
  clientSecret: z.string().min(1).optional(),
@@ -1946,7 +2307,7 @@ export type IntegrationOAuthClientConfig = z.infer<typeof IntegrationOAuthClient
1946
2307
  export interface ResolvedModelProvider {
1947
2308
  id: string; // "openai" | "azure" | registry id
1948
2309
  label: string;
1949
- kind: RegistryProviderKind; // "api-key" (built-ins + most registry) | "anonymous" | subscription
2310
+ kind: RegistryProviderKind | "openrouter-managed";
1950
2311
  api: ModelProviderApi;
1951
2312
  wireProfile: ModelProviderWireProfile;
1952
2313
  builtin: boolean;
@@ -1960,6 +2321,10 @@ export interface ResolvedModelProvider {
1960
2321
  billing: BillingAttributionV1;
1961
2322
  }
1962
2323
 
2324
+ type InternalRegistryProvider = Omit<RegistryProvider, "kind"> & {
2325
+ kind: RegistryProviderKind | "openrouter-managed";
2326
+ };
2327
+
1963
2328
  /** A single exposed model + the provider that serves it. */
1964
2329
  export interface ConfiguredModel {
1965
2330
  schemaVersion: 1;
@@ -1976,6 +2341,8 @@ export interface ConfiguredModel {
1976
2341
  executionLimits: ModelExecutionLimitsV1;
1977
2342
  credentialSource: CredentialSourceV1;
1978
2343
  billing: BillingAttributionV1;
2344
+ /** Workspace-facing funding policy, independent of upstream settlement. */
2345
+ cost: ConfiguredModelCostClass;
1979
2346
  capabilities: ModelCapabilitiesV1;
1980
2347
  requestPolicy?: {
1981
2348
  gateway: {
@@ -1995,11 +2362,10 @@ export interface ConfiguredModel {
1995
2362
 
1996
2363
  export const VERCEL_AI_GATEWAY_BASE_URL = "https://ai-gateway.vercel.sh/v1" as const;
1997
2364
  export const VERCEL_AI_GATEWAY_AI_SDK_BASE_URL = "https://ai-gateway.vercel.sh/v4/ai" as const;
1998
- export const OPENGENI_GATEWAY_PROVIDER_ID = "opengeni-gateway" as const;
1999
- export const WORKSPACE_GATEWAY_PROVIDER_ID = "workspace-gateway" as const;
2000
- export const WORKSPACE_GATEWAY_MODEL_ID_PREFIX = "workspace-gateway/" as const;
2001
2365
  export const VERCEL_AI_GATEWAY_CONNECTION_DOMAIN = "ai-gateway.vercel.sh" as const;
2002
2366
  export const VERCEL_AI_GATEWAY_CONNECTION_ROLE = "vercel_ai_gateway" as const;
2367
+ export const WORKSPACE_OPENROUTER_CONNECTION_DOMAIN = "openrouter.ai" as const;
2368
+ export const WORKSPACE_OPENROUTER_CONNECTION_ROLE = "openrouter" as const;
2003
2369
 
2004
2370
  export const CODEX_REALTIME_MODEL_ID = "gpt-live-1-boulder-alpha" as const;
2005
2371
  export const SUPERGROK_REALTIME_MODEL_ID = "supergrok/grok-voice-think-fast-2.0" as const;
@@ -2069,6 +2435,112 @@ export const OPENGENI_GATEWAY_MODELS = {
2069
2435
  },
2070
2436
  } as const;
2071
2437
 
2438
+ export const OPENGENI_OPENROUTER_MODELS: readonly OpenRouterCatalogModel[] = [
2439
+ OpenRouterCatalogModel.parse({
2440
+ upstreamModelId: "nvidia/nemotron-3-super-120b-a12b:free",
2441
+ label: "Nemotron 3 Super 120B",
2442
+ shortLabel: "Nemotron 3 Super",
2443
+ aliases: [],
2444
+ capabilities: {
2445
+ reasoning: {
2446
+ upstream: "supported",
2447
+ // OpenRouter advertises the reasoning controls, but the catalogue does
2448
+ // not publish this model's accepted effort vocabulary. Preserve that
2449
+ // upstream fact without exposing an unverified runnable selector.
2450
+ runnable: false,
2451
+ efforts: [],
2452
+ defaultEffort: null,
2453
+ required: false,
2454
+ },
2455
+ functionCalling: { upstream: "supported", runnable: true },
2456
+ structuredOutput: { upstream: "supported", runnable: true },
2457
+ hostedTools: {
2458
+ webSearch: { upstream: "unknown", runnable: false },
2459
+ xSearch: { upstream: "unknown", runnable: false },
2460
+ codeExecution: { upstream: "unknown", runnable: false },
2461
+ imageGeneration: { upstream: "unknown", runnable: false },
2462
+ },
2463
+ inputModalities: ["text"],
2464
+ inputFileMediaTypes: [],
2465
+ outputModalities: ["text"],
2466
+ transports: {
2467
+ sse: { upstream: "supported", runnable: true },
2468
+ responsesWebSocket: { upstream: "unknown", runnable: false },
2469
+ realtimeAudio: { upstream: "unsupported", runnable: false },
2470
+ },
2471
+ latencyModes: [{ id: "standard", upstream: "unknown", runnable: true }],
2472
+ },
2473
+ contextWindowTokens: 262_144,
2474
+ effectiveContextWindowTokens: 235_929,
2475
+ autoCompactTokenLimit: 220_000,
2476
+ }),
2477
+ ];
2478
+
2479
+ function defaultGatewayCatalogModels(): GatewayCatalogModel[] {
2480
+ return [
2481
+ {
2482
+ ...OPENGENI_GATEWAY_MODELS.deepseek,
2483
+ vision: false,
2484
+ inputFileMediaTypes: [],
2485
+ contextWindowTokens: 1_000_000,
2486
+ effectiveContextWindowTokens: 900_000,
2487
+ autoCompactTokenLimit: 850_000,
2488
+ },
2489
+ {
2490
+ ...OPENGENI_GATEWAY_MODELS.kimi,
2491
+ vision: true,
2492
+ inputFileMediaTypes: ["application/pdf"],
2493
+ contextWindowTokens: 1_000_000,
2494
+ effectiveContextWindowTokens: 900_000,
2495
+ autoCompactTokenLimit: 850_000,
2496
+ },
2497
+ ].map((model) => GatewayCatalogModel.parse(model));
2498
+ }
2499
+
2500
+ function configuredGatewayCatalogModels(settings: Settings): GatewayCatalogModel[] {
2501
+ if (settings.resolvedGatewayModelsJson === undefined) {
2502
+ return defaultGatewayCatalogModels();
2503
+ }
2504
+ return z.array(GatewayCatalogModel).parse(JSON.parse(settings.resolvedGatewayModelsJson));
2505
+ }
2506
+
2507
+ export function configuredGatewayUpstreamModelIds(settings: Settings): string[] {
2508
+ return configuredGatewayCatalogModels(settings).map((model) => model.upstreamModelId);
2509
+ }
2510
+
2511
+ export function configuredGatewayWorkspaceProductModelIds(settings: Settings): string[] {
2512
+ return configuredGatewayCatalogModels(settings).map((model) => model.workspaceProductId);
2513
+ }
2514
+
2515
+ export function configuredModelInputIdentities(settings: Settings): string[] {
2516
+ return configuredModels(settings).flatMap((model) => [model.id, ...model.aliases]);
2517
+ }
2518
+
2519
+ function configuredOpenRouterCatalogModels(settings: Settings): OpenRouterCatalogModel[] {
2520
+ if (settings.resolvedOpenRouterModelsJson === undefined) {
2521
+ return [...OPENGENI_OPENROUTER_MODELS];
2522
+ }
2523
+ return z.array(OpenRouterCatalogModel).parse(JSON.parse(settings.resolvedOpenRouterModelsJson));
2524
+ }
2525
+
2526
+ export function configuredOpenRouterUpstreamModelIds(settings: Settings): string[] {
2527
+ return configuredOpenRouterCatalogModels(settings).map((model) => model.upstreamModelId);
2528
+ }
2529
+
2530
+ function workspaceOpenRouterProductId(modelId: string): string {
2531
+ return `${WORKSPACE_OPENROUTER_MODEL_ID_PREFIX}${
2532
+ modelId.startsWith(OPENROUTER_MODEL_ID_PREFIX)
2533
+ ? modelId.slice(OPENROUTER_MODEL_ID_PREFIX.length)
2534
+ : modelId
2535
+ }`;
2536
+ }
2537
+
2538
+ export function configuredOpenRouterWorkspaceProductModelIds(settings: Settings): string[] {
2539
+ return configuredOpenRouterCatalogModels(settings).flatMap((model) =>
2540
+ [model.upstreamModelId, ...model.aliases].map(workspaceOpenRouterProductId),
2541
+ );
2542
+ }
2543
+
2072
2544
  /**
2073
2545
  * Built-in OpenGeni credit pricing schedules.
2074
2546
  *
@@ -2264,12 +2736,17 @@ function objectStorageConfiguredForWorkspaceArchives(settings: Settings): boolea
2264
2736
  }
2265
2737
  }
2266
2738
 
2267
- function optional(name: string): string | undefined {
2268
- const value = process.env[name];
2739
+ function optionalEnvironmentValue(name: string, source: NodeJS.ProcessEnv): string | undefined {
2740
+ const value = source[name];
2269
2741
  return value && value.trim().length > 0 ? value : undefined;
2270
2742
  }
2271
2743
 
2272
- export function getSettings(): Settings {
2744
+ export function getSettings(source: NodeJS.ProcessEnv = process.env): Settings {
2745
+ const optional = (name: string): string | undefined => optionalEnvironmentValue(name, source);
2746
+ const modelCatalogSource = optional("OPENGENI_MODEL_CATALOG_SOURCE");
2747
+ const modelCostPolicyJson =
2748
+ optional("OPENGENI_MODEL_COST_POLICY_JSON") ??
2749
+ (modelCatalogSource === "database" ? "{}" : DEFAULT_MODEL_COST_POLICY_JSON);
2273
2750
  const raw = {
2274
2751
  serviceName: optional("OPENGENI_SERVICE_NAME"),
2275
2752
  environment: optional("OPENGENI_ENVIRONMENT"),
@@ -2459,6 +2936,10 @@ export function getSettings(): Settings {
2459
2936
  voiceInputAzureAdToken: optional("OPENGENI_VOICE_INPUT_AZURE_AD_TOKEN"),
2460
2937
  voiceInputCodexExperimentalEnabled: optional("OPENGENI_VOICE_INPUT_CODEX_EXPERIMENTAL"),
2461
2938
  modelPricingJson: optional("OPENGENI_MODEL_PRICING_JSON"),
2939
+ modelCatalogSource,
2940
+ modelCostPolicyJson,
2941
+ modelNotesJson: optional("OPENGENI_MODEL_NOTES_JSON"),
2942
+ openrouterApiKey: optional("OPENGENI_OPENROUTER_API_KEY"),
2462
2943
  modelProvidersJson: optional("OPENGENI_MODEL_PROVIDERS_JSON"),
2463
2944
  codexSubscriptionEnabled: optional("OPENGENI_CODEX_SUBSCRIPTION_ENABLED"),
2464
2945
  supergrokSubscriptionEnabled: optional("OPENGENI_SUPERGROK_SUBSCRIPTION_ENABLED"),
@@ -2497,6 +2978,8 @@ export function getSettings(): Settings {
2497
2978
  modalImageId: optional("OPENGENI_MODAL_IMAGE_ID"),
2498
2979
  modalImageRegistrySecret: optional("OPENGENI_MODAL_IMAGE_REGISTRY_SECRET"),
2499
2980
  modalTimeoutSeconds: optional("OPENGENI_MODAL_TIMEOUT_SECONDS"),
2981
+ modalSandboxCpu: optional("OPENGENI_MODAL_SANDBOX_CPU"),
2982
+ modalSandboxMemoryMiB: optional("OPENGENI_MODAL_SANDBOX_MEMORY_MIB"),
2500
2983
  modalTokenId: optional("OPENGENI_MODAL_TOKEN_ID"),
2501
2984
  modalTokenSecret: optional("OPENGENI_MODAL_TOKEN_SECRET"),
2502
2985
  modalEnvironment: optional("OPENGENI_MODAL_ENVIRONMENT"),
@@ -2507,8 +2990,6 @@ export function getSettings(): Settings {
2507
2990
  sandboxTerminalEnabled: optional("OPENGENI_SANDBOX_TERMINAL_ENABLED"),
2508
2991
  streamResolutionWidth: optional("OPENGENI_STREAM_RESOLUTION_WIDTH"),
2509
2992
  streamResolutionHeight: optional("OPENGENI_STREAM_RESOLUTION_HEIGHT"),
2510
- computerUseEnabled: optional("OPENGENI_COMPUTER_USE_ENABLED"),
2511
- computerUseReadOnly: optional("OPENGENI_COMPUTER_USE_READONLY"),
2512
2993
  recordingEnabled: optional("OPENGENI_RECORDING_ENABLED"),
2513
2994
  workspaceCaptureEnabled: optional("OPENGENI_WORKSPACE_CAPTURE"),
2514
2995
  recordingDefaultCodec: optional("OPENGENI_RECORDING_DEFAULT_CODEC"),
@@ -2664,6 +3145,10 @@ export function getSettings(): Settings {
2664
3145
  betterAuthAllowedHosts: optional("OPENGENI_BETTER_AUTH_ALLOWED_HOSTS"),
2665
3146
  betterAuthCookieDomain: optional("OPENGENI_BETTER_AUTH_COOKIE_DOMAIN"),
2666
3147
  betterAuthTrustedOrigins: optional("OPENGENI_BETTER_AUTH_TRUSTED_ORIGINS"),
3148
+ managedAuthGoogleClientId: optional("OPENGENI_MANAGED_AUTH_GOOGLE_CLIENT_ID"),
3149
+ managedAuthGoogleClientSecret: optional("OPENGENI_MANAGED_AUTH_GOOGLE_CLIENT_SECRET"),
3150
+ managedAuthGithubClientId: optional("OPENGENI_MANAGED_AUTH_GITHUB_CLIENT_ID"),
3151
+ managedAuthGithubClientSecret: optional("OPENGENI_MANAGED_AUTH_GITHUB_CLIENT_SECRET"),
2667
3152
  managedAuthSessionSetMode: optional("OPENGENI_MANAGED_AUTH_SESSION_SET_MODE"),
2668
3153
  resendApiKey: optional("OPENGENI_RESEND_API_KEY"),
2669
3154
  emailFrom: optional("OPENGENI_EMAIL_FROM"),
@@ -2686,7 +3171,7 @@ export function getSettings(): Settings {
2686
3171
  : parsed.sandboxRotationLeadMs,
2687
3172
  mcpServers: ensureBuiltInMcpServers(parsed),
2688
3173
  };
2689
- validateSettings(settings);
3174
+ validateSettings(settings, source);
2690
3175
  return settings;
2691
3176
  }
2692
3177
 
@@ -3131,10 +3616,9 @@ function legacyModelCapabilities(
3131
3616
 
3132
3617
  export function gatewayRequestPolicyForUpstreamModel(
3133
3618
  upstreamModelId: string,
3619
+ models: readonly GatewayCatalogModel[] = defaultGatewayCatalogModels(),
3134
3620
  ): ConfiguredModel["requestPolicy"] {
3135
- const model = Object.values(OPENGENI_GATEWAY_MODELS).find(
3136
- (candidate) => candidate.upstreamModelId === upstreamModelId,
3137
- );
3621
+ const model = models.find((candidate) => candidate.upstreamModelId === upstreamModelId);
3138
3622
  if (!model) {
3139
3623
  return undefined;
3140
3624
  }
@@ -3148,7 +3632,11 @@ export function gatewayRequestPolicyForUpstreamModel(
3148
3632
 
3149
3633
  function gatewayModelCapabilities(
3150
3634
  settings: Settings,
3151
- input: { implicitCaching: boolean; vision: boolean; inputFileMediaTypes?: string[] },
3635
+ input: {
3636
+ implicitCaching: boolean;
3637
+ vision: boolean;
3638
+ inputFileMediaTypes?: string[];
3639
+ },
3152
3640
  ): ModelCapabilitiesV1 {
3153
3641
  const legacy = legacyModelCapabilities(settings, {
3154
3642
  reasoningEffort: true,
@@ -3172,31 +3660,93 @@ function gatewayModelCapabilities(
3172
3660
  });
3173
3661
  }
3174
3662
 
3663
+ function openRouterCustomModelCapabilities(settings: Settings): ModelCapabilitiesV1 {
3664
+ const legacy = legacyModelCapabilities(settings, {
3665
+ reasoningEffort: false,
3666
+ hostedWebSearch: false,
3667
+ });
3668
+ return normalizeCapabilities({
3669
+ ...legacy,
3670
+ functionCalling: { upstream: "supported", runnable: true },
3671
+ inputModalities: ["text"],
3672
+ inputFileMediaTypes: [],
3673
+ transports: {
3674
+ ...legacy.transports,
3675
+ sse: { upstream: "supported", runnable: true },
3676
+ },
3677
+ promptCaching: { upstream: "unsupported", runnable: false, mode: "none" },
3678
+ latencyModes: [{ id: "standard", upstream: "supported", runnable: true }],
3679
+ });
3680
+ }
3681
+
3175
3682
  function gatewayRegistryProvider(
3176
3683
  settings: Settings,
3177
3684
  input:
3178
3685
  | { kind: "vercel-gateway-managed"; apiKey: string }
3179
- | { kind: "vercel-gateway-workspace"; apiKey?: string },
3180
- ): RegistryProvider {
3686
+ | {
3687
+ kind: "vercel-gateway-workspace";
3688
+ apiKey?: string;
3689
+ customModels?: readonly {
3690
+ upstreamModelId: string;
3691
+ label?: string | null;
3692
+ }[];
3693
+ },
3694
+ ): InternalRegistryProvider {
3181
3695
  const workspace = input.kind === "vercel-gateway-workspace";
3182
- const models = [OPENGENI_GATEWAY_MODELS.deepseek, OPENGENI_GATEWAY_MODELS.kimi].map((model) => {
3183
- const kimi = model === OPENGENI_GATEWAY_MODELS.kimi;
3696
+ const curated = configuredGatewayCatalogModels(settings);
3697
+ const upstreamIds = new Set(curated.map((model) => model.upstreamModelId));
3698
+ const productIds = new Set(
3699
+ parseModelProvidersJson(settings.modelProvidersJson)
3700
+ .filter((provider) => provider.id !== WORKSPACE_GATEWAY_PROVIDER_ID)
3701
+ .flatMap((provider) =>
3702
+ provider.models.flatMap((model) => [model.id, ...(model.aliases ?? [])]),
3703
+ ),
3704
+ );
3705
+ const models = curated.map((model) => {
3706
+ productIds.add(workspace ? model.workspaceProductId : model.productId);
3184
3707
  return {
3185
3708
  id: workspace ? model.workspaceProductId : model.productId,
3186
3709
  upstreamModelId: model.upstreamModelId,
3187
3710
  label: model.label,
3188
- shortLabel: model.shortLabel,
3711
+ ...(model.shortLabel ? { shortLabel: model.shortLabel } : {}),
3189
3712
  capabilities: gatewayModelCapabilities(settings, {
3190
3713
  implicitCaching: model.implicitCaching,
3191
- vision: kimi,
3192
- inputFileMediaTypes: kimi ? ["application/pdf"] : [],
3714
+ vision: model.vision,
3715
+ inputFileMediaTypes: model.inputFileMediaTypes,
3193
3716
  }),
3194
- contextWindowTokens: 1_000_000,
3195
- effectiveContextWindowTokens: 900_000,
3196
- autoCompactTokenLimit: 850_000,
3717
+ contextWindowTokens: model.contextWindowTokens,
3718
+ effectiveContextWindowTokens: model.effectiveContextWindowTokens,
3719
+ autoCompactTokenLimit: model.autoCompactTokenLimit,
3197
3720
  toolOutputTruncationTokens: settings.modelToolOutputTruncationTokens,
3721
+ ...(model.pricing === undefined ? {} : { pricing: model.pricing }),
3198
3722
  };
3199
3723
  });
3724
+ if (workspace) {
3725
+ for (const custom of input.customModels ?? []) {
3726
+ const productId = `${WORKSPACE_GATEWAY_MODEL_ID_PREFIX}${custom.upstreamModelId}`;
3727
+ // Deployment membership wins over an older or concurrently-created
3728
+ // workspace row with the same upstream identity or generated product id.
3729
+ // This keeps runtime routing deterministic and prevents a legacy/admin
3730
+ // row from making the entire workspace catalog fail uniqueness checks.
3731
+ if (upstreamIds.has(custom.upstreamModelId) || productIds.has(productId)) continue;
3732
+ upstreamIds.add(custom.upstreamModelId);
3733
+ productIds.add(productId);
3734
+ models.push({
3735
+ id: productId,
3736
+ upstreamModelId: custom.upstreamModelId,
3737
+ label: custom.label?.trim() || custom.upstreamModelId,
3738
+ capabilities: gatewayModelCapabilities(settings, {
3739
+ implicitCaching: false,
3740
+ vision: false,
3741
+ inputFileMediaTypes: [],
3742
+ }),
3743
+ contextWindowTokens: 1_000_000,
3744
+ effectiveContextWindowTokens: 900_000,
3745
+ autoCompactTokenLimit: 850_000,
3746
+ toolOutputTruncationTokens: settings.modelToolOutputTruncationTokens,
3747
+ });
3748
+ }
3749
+ }
3200
3750
  return {
3201
3751
  kind: input.kind,
3202
3752
  id: workspace ? WORKSPACE_GATEWAY_PROVIDER_ID : OPENGENI_GATEWAY_PROVIDER_ID,
@@ -3212,52 +3762,199 @@ function gatewayRegistryProvider(
3212
3762
  };
3213
3763
  }
3214
3764
 
3215
- function configuredRegistryProviders(settings: Settings): RegistryProvider[] {
3216
- const providers = parseModelProvidersJson(settings.modelProvidersJson);
3217
- if (!settings.vercelAiGatewayApiKey) {
3218
- return providers;
3765
+ function openRouterRegistryProvider(
3766
+ settings: Settings,
3767
+ input:
3768
+ | { kind: "openrouter-managed"; apiKey: string }
3769
+ | {
3770
+ kind: "openrouter-workspace";
3771
+ apiKey?: string;
3772
+ customModels?: readonly {
3773
+ upstreamModelId: string;
3774
+ label?: string | null;
3775
+ }[];
3776
+ },
3777
+ ): InternalRegistryProvider | null {
3778
+ const workspace = input.kind === "openrouter-workspace";
3779
+ const curated = configuredOpenRouterCatalogModels(settings);
3780
+ const upstreamIds = new Set(curated.map((model) => model.upstreamModelId));
3781
+ const productIds = new Set(
3782
+ parseModelProvidersJson(settings.modelProvidersJson)
3783
+ .filter((provider) => provider.id !== WORKSPACE_OPENROUTER_PROVIDER_ID)
3784
+ .flatMap((provider) =>
3785
+ provider.models.flatMap((model) => [model.id, ...(model.aliases ?? [])]),
3786
+ ),
3787
+ );
3788
+ const models: RegistryProvider["models"] = curated.map((model) => {
3789
+ const id = workspace
3790
+ ? workspaceOpenRouterProductId(model.upstreamModelId)
3791
+ : `${OPENROUTER_MODEL_ID_PREFIX}${model.upstreamModelId}`;
3792
+ const aliases = workspace ? model.aliases.map(workspaceOpenRouterProductId) : model.aliases;
3793
+ productIds.add(id);
3794
+ for (const alias of aliases) productIds.add(alias);
3795
+ return {
3796
+ id,
3797
+ upstreamModelId: model.upstreamModelId,
3798
+ aliases,
3799
+ label: model.label,
3800
+ ...(model.shortLabel ? { shortLabel: model.shortLabel } : {}),
3801
+ capabilities: model.capabilities,
3802
+ ...(model.contextWindowTokens === undefined
3803
+ ? {}
3804
+ : { contextWindowTokens: model.contextWindowTokens }),
3805
+ ...(model.effectiveContextWindowTokens === undefined
3806
+ ? {}
3807
+ : { effectiveContextWindowTokens: model.effectiveContextWindowTokens }),
3808
+ ...(model.autoCompactTokenLimit === undefined
3809
+ ? {}
3810
+ : { autoCompactTokenLimit: model.autoCompactTokenLimit }),
3811
+ toolOutputTruncationTokens:
3812
+ model.toolOutputTruncationTokens ?? settings.modelToolOutputTruncationTokens,
3813
+ };
3814
+ });
3815
+ if (workspace) {
3816
+ for (const custom of input.customModels ?? []) {
3817
+ const productId = `${WORKSPACE_OPENROUTER_MODEL_ID_PREFIX}${custom.upstreamModelId}`;
3818
+ if (upstreamIds.has(custom.upstreamModelId) || productIds.has(productId)) continue;
3819
+ upstreamIds.add(custom.upstreamModelId);
3820
+ productIds.add(productId);
3821
+ models.push({
3822
+ id: productId,
3823
+ upstreamModelId: custom.upstreamModelId,
3824
+ aliases: [],
3825
+ label: custom.label?.trim() || custom.upstreamModelId,
3826
+ capabilities: openRouterCustomModelCapabilities(settings),
3827
+ toolOutputTruncationTokens: settings.modelToolOutputTruncationTokens,
3828
+ });
3829
+ }
3219
3830
  }
3220
- if (providers.some((provider) => provider.id === OPENGENI_GATEWAY_PROVIDER_ID)) {
3221
- throw new Error(
3222
- `${OPENGENI_GATEWAY_PROVIDER_ID} is reserved for OPENGENI_VERCEL_AI_GATEWAY_API_KEY`,
3831
+ if (models.length === 0) return null;
3832
+ const defaultHeaders: Record<string, string> = {
3833
+ "x-title": "OpenGeni",
3834
+ ...(settings.publicBaseUrl ? { "http-referer": settings.publicBaseUrl } : {}),
3835
+ };
3836
+ return {
3837
+ kind: input.kind,
3838
+ id: workspace ? WORKSPACE_OPENROUTER_PROVIDER_ID : OPENROUTER_PROVIDER_ID,
3839
+ label: workspace ? "Your OpenRouter" : "OpenRouter",
3840
+ api: "chat",
3841
+ wireProfile: "openai",
3842
+ baseUrl: OPENROUTER_BASE_URL,
3843
+ ...(input.apiKey ? { apiKey: input.apiKey } : {}),
3844
+ defaultHeaders,
3845
+ publicDefaultHeaderNames: Object.keys(defaultHeaders),
3846
+ models,
3847
+ };
3848
+ }
3849
+
3850
+ function configuredRegistryProviders(settings: Settings): InternalRegistryProvider[] {
3851
+ const providers = parseModelProvidersJson(settings.modelProvidersJson);
3852
+ const injected: InternalRegistryProvider[] = [...providers];
3853
+ if (settings.vercelAiGatewayApiKey && configuredGatewayCatalogModels(settings).length > 0) {
3854
+ injected.push(
3855
+ gatewayRegistryProvider(settings, {
3856
+ kind: "vercel-gateway-managed",
3857
+ apiKey: settings.vercelAiGatewayApiKey,
3858
+ }),
3223
3859
  );
3224
3860
  }
3225
- return [
3226
- ...providers,
3227
- gatewayRegistryProvider(settings, {
3228
- kind: "vercel-gateway-managed",
3229
- apiKey: settings.vercelAiGatewayApiKey,
3230
- }),
3231
- ];
3861
+ const openrouter = settings.openrouterApiKey
3862
+ ? openRouterRegistryProvider(settings, {
3863
+ kind: "openrouter-managed",
3864
+ apiKey: settings.openrouterApiKey,
3865
+ })
3866
+ : null;
3867
+ if (openrouter) injected.push(openrouter);
3868
+ return injected;
3232
3869
  }
3233
3870
 
3234
3871
  /** Static catalog overlay; it contains no concrete workspace credential. */
3235
- export function withWorkspaceGatewayCatalogProvider(settings: Settings): Settings {
3872
+ export function withWorkspaceGatewayCatalogProvider(
3873
+ settings: Settings,
3874
+ customModels: readonly {
3875
+ upstreamModelId: string;
3876
+ label?: string | null;
3877
+ }[] = [],
3878
+ ): Settings {
3236
3879
  const providers = parseModelProvidersJson(settings.modelProvidersJson);
3237
- if (providers.some((provider) => provider.id === WORKSPACE_GATEWAY_PROVIDER_ID)) {
3238
- return settings;
3239
- }
3880
+ const withoutWorkspace = providers.filter(
3881
+ (provider) => provider.id !== WORKSPACE_GATEWAY_PROVIDER_ID,
3882
+ );
3883
+ const curatedCount = configuredGatewayCatalogModels(settings).length;
3884
+ if (curatedCount === 0 && customModels.length === 0) return settings;
3240
3885
  return {
3241
3886
  ...settings,
3242
3887
  modelProvidersJson: JSON.stringify([
3243
- ...providers,
3244
- gatewayRegistryProvider(settings, { kind: "vercel-gateway-workspace" }),
3888
+ ...withoutWorkspace,
3889
+ gatewayRegistryProvider(settings, {
3890
+ kind: "vercel-gateway-workspace",
3891
+ customModels,
3892
+ }),
3245
3893
  ]),
3246
3894
  };
3247
3895
  }
3248
3896
 
3249
3897
  /** Runtime overlay after the worker resolves the workspace's encrypted key. */
3250
- export function withWorkspaceGatewayCredential(settings: Settings, apiKey: string): Settings {
3898
+ export function withWorkspaceGatewayCredential(
3899
+ settings: Settings,
3900
+ apiKey: string,
3901
+ customModels: readonly {
3902
+ upstreamModelId: string;
3903
+ label?: string | null;
3904
+ }[] = [],
3905
+ ): Settings {
3251
3906
  if (!apiKey.trim()) {
3252
3907
  throw new Error("workspace AI Gateway credential is empty");
3253
3908
  }
3254
- const catalogSettings = withWorkspaceGatewayCatalogProvider(settings);
3909
+ const catalogSettings = withWorkspaceGatewayCatalogProvider(settings, customModels);
3255
3910
  const providers = parseModelProvidersJson(catalogSettings.modelProvidersJson).map((provider) =>
3256
3911
  provider.id === WORKSPACE_GATEWAY_PROVIDER_ID ? { ...provider, apiKey } : provider,
3257
3912
  );
3258
3913
  return { ...catalogSettings, modelProvidersJson: JSON.stringify(providers) };
3259
3914
  }
3260
3915
 
3916
+ /** Static OpenRouter catalog overlay; it contains no concrete workspace credential. */
3917
+ export function withWorkspaceOpenRouterCatalogProvider(
3918
+ settings: Settings,
3919
+ customModels: readonly {
3920
+ upstreamModelId: string;
3921
+ label?: string | null;
3922
+ }[] = [],
3923
+ ): Settings {
3924
+ const providers = parseModelProvidersJson(settings.modelProvidersJson);
3925
+ const withoutWorkspace = providers.filter(
3926
+ (provider) => provider.id !== WORKSPACE_OPENROUTER_PROVIDER_ID,
3927
+ );
3928
+ const provider = openRouterRegistryProvider(settings, {
3929
+ kind: "openrouter-workspace",
3930
+ customModels,
3931
+ });
3932
+ if (!provider) return settings;
3933
+ return {
3934
+ ...settings,
3935
+ modelProvidersJson: JSON.stringify([...withoutWorkspace, provider]),
3936
+ };
3937
+ }
3938
+
3939
+ /** Runtime overlay after the worker resolves the workspace's encrypted OpenRouter key. */
3940
+ export function withWorkspaceOpenRouterCredential(
3941
+ settings: Settings,
3942
+ apiKey: string,
3943
+ customModels: readonly {
3944
+ upstreamModelId: string;
3945
+ label?: string | null;
3946
+ }[] = [],
3947
+ ): Settings {
3948
+ if (!apiKey.trim()) {
3949
+ throw new Error("workspace OpenRouter credential is empty");
3950
+ }
3951
+ const catalogSettings = withWorkspaceOpenRouterCatalogProvider(settings, customModels);
3952
+ const providers = parseModelProvidersJson(catalogSettings.modelProvidersJson).map((provider) =>
3953
+ provider.id === WORKSPACE_OPENROUTER_PROVIDER_ID ? { ...provider, apiKey } : provider,
3954
+ );
3955
+ return { ...catalogSettings, modelProvidersJson: JSON.stringify(providers) };
3956
+ }
3957
+
3261
3958
  /** OpenAI GPT-5.6 Fast mode is 2× Standard list rates (service_tier fast/priority). */
3262
3959
  const GPT56_FAST_BILLING_MULTIPLIER_BPS = 20_000;
3263
3960
 
@@ -3457,33 +4154,64 @@ function assertLatencyModeRunnable(
3457
4154
  }
3458
4155
  }
3459
4156
 
3460
- function registryCredentialSource(provider: RegistryProvider): CredentialSourceV1 {
3461
- if (provider.kind === "anonymous") {
3462
- return { kind: "deployment", mechanism: "none" };
3463
- }
3464
- if (provider.kind === "codex-subscription") {
3465
- return { kind: "connected_subscription", provider: "codex" };
3466
- }
3467
- if (provider.kind === "xai-subscription") {
3468
- return { kind: "connected_subscription", provider: "xai" };
3469
- }
3470
- if (provider.kind === "vercel-gateway-workspace") {
3471
- return { kind: "workspace_connection", mechanism: "api_key" };
4157
+ function registryCredentialSource(provider: InternalRegistryProvider): CredentialSourceV1 {
4158
+ switch (provider.kind) {
4159
+ case "anonymous":
4160
+ return { kind: "deployment", mechanism: "none" };
4161
+ case "codex-subscription":
4162
+ return { kind: "connected_subscription", provider: "codex" };
4163
+ case "xai-subscription":
4164
+ return { kind: "connected_subscription", provider: "xai" };
4165
+ case "vercel-gateway-workspace":
4166
+ case "openrouter-workspace":
4167
+ return { kind: "workspace_connection", mechanism: "api_key" };
4168
+ case "api-key":
4169
+ case "vercel-gateway-managed":
4170
+ case "openrouter-managed":
4171
+ return { kind: "deployment", mechanism: "api_key" };
4172
+ default: {
4173
+ const _exhaustive: never = provider.kind;
4174
+ return _exhaustive;
4175
+ }
3472
4176
  }
3473
- return { kind: "deployment", mechanism: "api_key" };
3474
4177
  }
3475
4178
 
3476
- function registryBilling(provider: RegistryProvider): BillingAttributionV1 {
3477
- if (provider.kind === "anonymous") {
3478
- return { upstreamPayer: "deployment", metering: "external" };
3479
- }
3480
- if (provider.kind === "codex-subscription" || provider.kind === "xai-subscription") {
3481
- return { upstreamPayer: "connected_subscription", metering: "external" };
3482
- }
3483
- if (provider.kind === "vercel-gateway-workspace") {
3484
- return { upstreamPayer: "workspace", metering: "external" };
4179
+ function registryBilling(provider: InternalRegistryProvider): BillingAttributionV1 {
4180
+ switch (provider.kind) {
4181
+ case "anonymous":
4182
+ case "openrouter-managed":
4183
+ return { upstreamPayer: "deployment", metering: "external" };
4184
+ case "codex-subscription":
4185
+ case "xai-subscription":
4186
+ return { upstreamPayer: "connected_subscription", metering: "external" };
4187
+ case "vercel-gateway-workspace":
4188
+ case "openrouter-workspace":
4189
+ return { upstreamPayer: "workspace", metering: "external" };
4190
+ case "api-key":
4191
+ case "vercel-gateway-managed":
4192
+ return { upstreamPayer: "deployment", metering: "opengeni_credits" };
4193
+ default: {
4194
+ const _exhaustive: never = provider.kind;
4195
+ return _exhaustive;
4196
+ }
3485
4197
  }
3486
- return { upstreamPayer: "deployment", metering: "opengeni_credits" };
4198
+ }
4199
+
4200
+ function configuredCostForModel(
4201
+ settings: Settings,
4202
+ productModelId: string,
4203
+ credentialSource: CredentialSourceV1,
4204
+ ): ConfiguredModelCostClass {
4205
+ if (credentialSource.kind === "workspace_connection") return "workspace";
4206
+ if (credentialSource.kind === "connected_subscription") return "subscription";
4207
+ return parseModelCostPolicyJson(settings.modelCostPolicyJson)[productModelId] ?? "credits";
4208
+ }
4209
+
4210
+ export function modelCostClassForConfiguredModel(
4211
+ _settings: Settings,
4212
+ model: Pick<ConfiguredModel, "cost">,
4213
+ ): ConfiguredModelCostClass {
4214
+ return model.cost;
3487
4215
  }
3488
4216
 
3489
4217
  function builtinCredentialSource(settings: Settings): CredentialSourceV1 {
@@ -3548,8 +4276,10 @@ function canonicalJson(value: unknown): string {
3548
4276
  function definitionVersionFor(
3549
4277
  model: Omit<ConfiguredModel, "definitionVersion">,
3550
4278
  provider: ResolvedModelProvider,
4279
+ options: { includeWireProfile?: boolean } = {},
3551
4280
  ): string {
3552
4281
  const requestMetadata = staticRequestMetadataForDigest(provider);
4282
+ const includeWireProfile = options.includeWireProfile ?? true;
3553
4283
  const digestInput = canonicalJson({
3554
4284
  schemaVersion: model.schemaVersion,
3555
4285
  id: model.id,
@@ -3558,7 +4288,7 @@ function definitionVersionFor(
3558
4288
  provider: {
3559
4289
  adapterKind: provider.kind,
3560
4290
  wireApi: provider.api,
3561
- wireProfile: provider.wireProfile,
4291
+ ...(includeWireProfile ? { wireProfile: provider.wireProfile } : {}),
3562
4292
  baseUrl: provider.baseUrl ?? null,
3563
4293
  defaultHeaders: requestMetadata.headers,
3564
4294
  defaultQuery: requestMetadata.query,
@@ -3567,6 +4297,10 @@ function definitionVersionFor(
3567
4297
  billing: model.billing,
3568
4298
  executionLimits: model.executionLimits,
3569
4299
  capabilities: model.capabilities,
4300
+ // Workspace-facing free/credits classification is a separate live
4301
+ // deployment policy. Operators must drain/fence accepted turns before
4302
+ // changing it; it is intentionally not a second executable-definition
4303
+ // freeze inside TurnExecutionPolicyV1.
3570
4304
  ...(model.requestPolicy ? { requestPolicy: model.requestPolicy } : {}),
3571
4305
  pricing: model.pricing ?? null,
3572
4306
  });
@@ -3576,6 +4310,17 @@ function definitionVersionFor(
3576
4310
  .digest("hex")}`;
3577
4311
  }
3578
4312
 
4313
+ function legacyImplicitOpenAiDefinitionVersionFor(
4314
+ model: ConfiguredModel,
4315
+ provider: ResolvedModelProvider,
4316
+ ): string | null {
4317
+ if (provider.wireProfile !== "openai") return null;
4318
+ const { definitionVersion: _definitionVersion, ...modelWithoutVersion } = model;
4319
+ return definitionVersionFor(modelWithoutVersion, provider, {
4320
+ includeWireProfile: false,
4321
+ });
4322
+ }
4323
+
3579
4324
  /**
3580
4325
  * The built-in provider's stable id: "openai" on the OpenAI platform, "azure"
3581
4326
  * on Azure. Exported because the workspace model-policy gate must attribute
@@ -3599,7 +4344,10 @@ function builtinProviderLabel(settings: Pick<Settings, "openaiProvider">): strin
3599
4344
  * registry entry for the rest. Registry ids may not collide with the built-in
3600
4345
  * id — validateSettings rejects that at boot.
3601
4346
  */
3602
- export function configuredProviders(settings: Settings): ResolvedModelProvider[] {
4347
+ export function configuredProviders(
4348
+ settings: Settings,
4349
+ source: NodeJS.ProcessEnv = process.env,
4350
+ ): ResolvedModelProvider[] {
3603
4351
  const credentialSource = builtinCredentialSource(settings);
3604
4352
  const builtin: ResolvedModelProvider = {
3605
4353
  id: builtinProviderId(settings),
@@ -3630,7 +4378,7 @@ export function configuredProviders(settings: Settings): ResolvedModelProvider[]
3630
4378
  wireProfile: provider.wireProfile,
3631
4379
  builtin: false,
3632
4380
  baseUrl: provider.baseUrl,
3633
- apiKey: resolveProviderApiKey(provider),
4381
+ apiKey: resolveProviderApiKey(provider, source),
3634
4382
  defaultQuery: provider.defaultQuery,
3635
4383
  defaultHeaders: provider.defaultHeaders,
3636
4384
  publicDefaultQueryNames: provider.publicDefaultQueryNames,
@@ -3730,8 +4478,14 @@ export function withXaiSubscriptionCatalogProvider(settings: Settings): Settings
3730
4478
  { id: "standard", upstream: "supported", runnable: true },
3731
4479
  { id: "fast", upstream: "supported", runnable: true },
3732
4480
  ];
3733
- capabilities.hostedTools.xSearch = { upstream: "supported", runnable: true };
3734
- capabilities.hostedTools.imageGeneration = { upstream: "supported", runnable: true };
4481
+ capabilities.hostedTools.xSearch = {
4482
+ upstream: "supported",
4483
+ runnable: true,
4484
+ };
4485
+ capabilities.hostedTools.imageGeneration = {
4486
+ upstream: "supported",
4487
+ runnable: true,
4488
+ };
3735
4489
  return {
3736
4490
  id: `${XAI_SUBSCRIPTION_MODEL_ID_PREFIX}${slug}`,
3737
4491
  upstreamModelId: slug,
@@ -3749,7 +4503,10 @@ export function withXaiSubscriptionCatalogProvider(settings: Settings): Settings
3749
4503
  };
3750
4504
  }),
3751
4505
  };
3752
- return { ...settings, modelProvidersJson: JSON.stringify([...providers, provider]) };
4506
+ return {
4507
+ ...settings,
4508
+ modelProvidersJson: JSON.stringify([...providers, provider]),
4509
+ };
3753
4510
  }
3754
4511
 
3755
4512
  /**
@@ -3776,6 +4533,9 @@ export function policyProviderIdForModel(settings: Settings, modelId: string): s
3776
4533
  if (canonicalModelId.startsWith(WORKSPACE_GATEWAY_MODEL_ID_PREFIX)) {
3777
4534
  return WORKSPACE_GATEWAY_PROVIDER_ID;
3778
4535
  }
4536
+ if (canonicalModelId.startsWith(WORKSPACE_OPENROUTER_MODEL_ID_PREFIX)) {
4537
+ return WORKSPACE_OPENROUTER_PROVIDER_ID;
4538
+ }
3779
4539
  const configured = configuredModels(settings).find((model) => model.id === canonicalModelId);
3780
4540
  return configured?.providerId ?? builtinProviderId(settings);
3781
4541
  }
@@ -3803,15 +4563,19 @@ function resolvedExecutionLimits(
3803
4563
  function finalizeConfiguredModel(
3804
4564
  settings: Settings,
3805
4565
  provider: ResolvedModelProvider,
3806
- input: Omit<ConfiguredModel, "schemaVersion" | "definitionVersion" | "executionLimits">,
4566
+ input: Omit<ConfiguredModel, "schemaVersion" | "definitionVersion" | "executionLimits" | "cost">,
3807
4567
  ): ConfiguredModel {
3808
4568
  const requestPolicy =
3809
4569
  provider.kind === "vercel-gateway-managed" || provider.kind === "vercel-gateway-workspace"
3810
- ? gatewayRequestPolicyForUpstreamModel(input.upstreamModelId)
4570
+ ? gatewayRequestPolicyForUpstreamModel(
4571
+ input.upstreamModelId,
4572
+ configuredGatewayCatalogModels(settings),
4573
+ )
3811
4574
  : undefined;
3812
4575
  const modelWithoutVersion: Omit<ConfiguredModel, "definitionVersion"> = {
3813
4576
  schemaVersion: 1,
3814
4577
  ...input,
4578
+ cost: configuredCostForModel(settings, input.id, input.credentialSource),
3815
4579
  ...(requestPolicy ? { requestPolicy } : {}),
3816
4580
  executionLimits: resolvedExecutionLimits(settings, input),
3817
4581
  };
@@ -3862,10 +4626,13 @@ function assertUniqueModelIdentities(models: ConfiguredModel[]): void {
3862
4626
  * default false). De-duplicated by id (first wins) so the default model stays
3863
4627
  * first and the built-in allow-list takes precedence over registry entries.
3864
4628
  */
3865
- export function configuredModels(settings: Settings): ConfiguredModel[] {
4629
+ export function configuredModels(
4630
+ settings: Settings,
4631
+ source: NodeJS.ProcessEnv = process.env,
4632
+ ): ConfiguredModel[] {
3866
4633
  const builtinId = builtinProviderId(settings);
3867
4634
  const builtinLabel = builtinProviderLabel(settings);
3868
- const providers = configuredProviders(settings);
4635
+ const providers = configuredProviders(settings, source);
3869
4636
  const providerById = new Map(providers.map((provider) => [provider.id, provider]));
3870
4637
  const pricingSchedules = configuredModelPricingSchedules(settings);
3871
4638
  // The built-in (OpenAI/Azure) provider must NEVER claim a registry-namespaced
@@ -3996,7 +4763,12 @@ export function configuredModels(settings: Settings): ConfiguredModel[] {
3996
4763
  }
3997
4764
  }
3998
4765
  assertUniqueModelIdentities(out);
3999
- return out;
4766
+ const defaultIndex = out.findIndex(
4767
+ (model) => model.id === settings.openaiModel || model.aliases.includes(settings.openaiModel),
4768
+ );
4769
+ return defaultIndex > 0
4770
+ ? [out[defaultIndex]!, ...out.slice(0, defaultIndex), ...out.slice(defaultIndex + 1)]
4771
+ : out;
4000
4772
  }
4001
4773
 
4002
4774
  /** Resolve a known canonical id or alias. Unknown strings are returned unchanged. */
@@ -4067,11 +4839,40 @@ function settingsForTurnExecutionPolicy(settings: Settings, modelId: string): Se
4067
4839
  return withXaiSubscriptionCatalogProvider(settings);
4068
4840
  }
4069
4841
  if (modelId.startsWith(WORKSPACE_GATEWAY_MODEL_ID_PREFIX)) {
4842
+ // API/worker workspace boundaries may already have overlaid durable custom
4843
+ // rows and, at execution time, the decrypted workspace key. Re-applying an
4844
+ // empty static overlay would silently discard both. Only synthesize the
4845
+ // curated fallback when this exact model is not already executable.
4846
+ if (resolveModelProvider(settings, modelId)) {
4847
+ return settings;
4848
+ }
4070
4849
  return withWorkspaceGatewayCatalogProvider(settings);
4071
4850
  }
4851
+ if (modelId.startsWith(WORKSPACE_OPENROUTER_MODEL_ID_PREFIX)) {
4852
+ if (resolveModelProvider(settings, modelId)) {
4853
+ return settings;
4854
+ }
4855
+ return withWorkspaceOpenRouterCatalogProvider(settings);
4856
+ }
4072
4857
  return settings;
4073
4858
  }
4074
4859
 
4860
+ /**
4861
+ * Resolve the static catalog identity used by an accepted turn. Subscription
4862
+ * overlays contain no account or bearer and do not prove connection readiness;
4863
+ * callers must keep their live credential/readiness gate authoritative.
4864
+ */
4865
+ export function resolveModelProviderForTurn(
4866
+ settings: Settings,
4867
+ modelId: string,
4868
+ ): ReturnType<typeof resolveModelProvider> {
4869
+ const catalogSettings = settingsForTurnExecutionPolicy(settings, modelId);
4870
+ return resolveModelProvider(
4871
+ catalogSettings,
4872
+ canonicalizeConfiguredModelId(catalogSettings, modelId),
4873
+ );
4874
+ }
4875
+
4075
4876
  /**
4076
4877
  * Build a trusted, secret-safe execution policy from the normalized catalog.
4077
4878
  * The Codex overlay here contains static product/provider identity only; it
@@ -4157,11 +4958,22 @@ export function assertTurnExecutionPolicyMatchesConfigV1(
4157
4958
  if (!resolved) {
4158
4959
  throw new Error("Turn execution policy model is no longer configured");
4159
4960
  }
4961
+ // wireProfile was added to the definition digest after policies already
4962
+ // existed in durable in-flight turns. An omitted profile meant exactly
4963
+ // "openai", so accept that one legacy digest only; Azure and every other
4964
+ // executable-definition change remain fail-closed.
4965
+ const legacyImplicitOpenAiDefinitionVersion = legacyImplicitOpenAiDefinitionVersionFor(
4966
+ resolved.model,
4967
+ resolved.provider,
4968
+ );
4969
+ const definitionVersionMatches =
4970
+ parsed.definitionVersion === resolved.model.definitionVersion ||
4971
+ parsed.definitionVersion === legacyImplicitOpenAiDefinitionVersion;
4160
4972
  const mismatched =
4161
4973
  parsed.providerId !== resolved.provider.id ||
4162
4974
  parsed.upstreamModelId !== resolved.model.upstreamModelId ||
4163
4975
  parsed.wireApi !== resolved.model.api ||
4164
- parsed.definitionVersion !== resolved.model.definitionVersion ||
4976
+ !definitionVersionMatches ||
4165
4977
  canonicalJson(parsed.credentialSource) !== canonicalJson(resolved.model.credentialSource) ||
4166
4978
  canonicalJson(parsed.billing) !== canonicalJson(resolved.model.billing);
4167
4979
  if (mismatched) {
@@ -4378,6 +5190,37 @@ export function calculateGatewayReportedCostMicros(
4378
5190
  .creditCostMicros;
4379
5191
  }
4380
5192
 
5193
+ type GatewayReportedCostDecimal = {
5194
+ providerNumerator: bigint;
5195
+ decimalScale: bigint;
5196
+ providerCostMicros: number;
5197
+ };
5198
+
5199
+ function parseGatewayReportedCostDecimal(inferenceCostUsd: string): GatewayReportedCostDecimal {
5200
+ const match = /^(0|[1-9]\d*)(?:\.(\d{1,18}))?$/.exec(inferenceCostUsd);
5201
+ if (!match) {
5202
+ throw new Error("Invalid AI Gateway inference cost");
5203
+ }
5204
+ const fraction = match[2] ?? "";
5205
+ const decimalDigits = BigInt(`${match[1]}${fraction}`);
5206
+ const decimalScale = 10n ** BigInt(fraction.length);
5207
+ const providerNumerator = decimalDigits * 1_000_000n;
5208
+ const providerCostMicros = (providerNumerator + decimalScale - 1n) / decimalScale;
5209
+ if (providerCostMicros > BigInt(Number.MAX_SAFE_INTEGER)) {
5210
+ throw new Error("AI Gateway inference cost exceeds the supported billing range");
5211
+ }
5212
+ return {
5213
+ providerNumerator,
5214
+ decimalScale,
5215
+ providerCostMicros: Number(providerCostMicros),
5216
+ };
5217
+ }
5218
+
5219
+ /** Exact provider-reported Gateway cost without requiring an OpenGeni price schedule. */
5220
+ export function calculateGatewayReportedProviderCostMicros(inferenceCostUsd: string): number {
5221
+ return parseGatewayReportedCostDecimal(inferenceCostUsd).providerCostMicros;
5222
+ }
5223
+
4381
5224
  export function calculateGatewayReportedCostBreakdown(
4382
5225
  settings: Settings,
4383
5226
  model: string,
@@ -4389,27 +5232,17 @@ export function calculateGatewayReportedCostBreakdown(
4389
5232
  throw new Error(`Missing model pricing for ${model}`);
4390
5233
  }
4391
5234
  const pricing = selectModelPricing(schedule, positiveInt(options?.inputTokens));
4392
- const match = /^(0|[1-9]\d*)(?:\.(\d{1,18}))?$/.exec(inferenceCostUsd);
4393
- if (!match) {
4394
- throw new Error("Invalid AI Gateway inference cost");
4395
- }
4396
- const fraction = match[2] ?? "";
4397
- const decimalDigits = BigInt(`${match[1]}${fraction}`);
4398
- const decimalScale = 10n ** BigInt(fraction.length);
4399
- const providerNumerator = decimalDigits * 1_000_000n;
4400
- const providerMicros = (providerNumerator + decimalScale - 1n) / decimalScale;
5235
+ const { providerNumerator, decimalScale, providerCostMicros } =
5236
+ parseGatewayReportedCostDecimal(inferenceCostUsd);
4401
5237
  const marginBps = BigInt(10_000 + (pricing.marginBps ?? 0));
4402
5238
  const numerator = providerNumerator * marginBps;
4403
5239
  const denominator = decimalScale * 10_000n;
4404
5240
  const creditMicros = (numerator + denominator - 1n) / denominator;
4405
- if (
4406
- providerMicros > BigInt(Number.MAX_SAFE_INTEGER) ||
4407
- creditMicros > BigInt(Number.MAX_SAFE_INTEGER)
4408
- ) {
5241
+ if (creditMicros > BigInt(Number.MAX_SAFE_INTEGER)) {
4409
5242
  throw new Error("AI Gateway inference cost exceeds the supported billing range");
4410
5243
  }
4411
5244
  return {
4412
- providerCostMicros: Number(providerMicros),
5245
+ providerCostMicros,
4413
5246
  creditCostMicros: Number(creditMicros),
4414
5247
  };
4415
5248
  }
@@ -5283,7 +6116,7 @@ function isDigestPinnedModalDesktopImage(settings: Settings): boolean {
5283
6116
  );
5284
6117
  }
5285
6118
 
5286
- function validateSettings(settings: Settings): void {
6119
+ function validateSettings(settings: Settings, source: NodeJS.ProcessEnv = process.env): void {
5287
6120
  temporalConnectionOptions(settings);
5288
6121
  if (settings.goalIdleBackoffMs.some((delayMs) => delayMs > settings.goalIdleBackoffMaxMs)) {
5289
6122
  throw new Error(
@@ -5335,6 +6168,41 @@ function validateSettings(settings: Settings): void {
5335
6168
  );
5336
6169
  }
5337
6170
  }
6171
+ if (
6172
+ Boolean(settings.managedAuthGoogleClientId) !== Boolean(settings.managedAuthGoogleClientSecret)
6173
+ ) {
6174
+ throw new Error(
6175
+ "OPENGENI_MANAGED_AUTH_GOOGLE_CLIENT_ID and OPENGENI_MANAGED_AUTH_GOOGLE_CLIENT_SECRET must be configured together",
6176
+ );
6177
+ }
6178
+ if (
6179
+ Boolean(settings.managedAuthGithubClientId) !== Boolean(settings.managedAuthGithubClientSecret)
6180
+ ) {
6181
+ throw new Error(
6182
+ "OPENGENI_MANAGED_AUTH_GITHUB_CLIENT_ID and OPENGENI_MANAGED_AUTH_GITHUB_CLIENT_SECRET must be configured together",
6183
+ );
6184
+ }
6185
+ const managedSocialAuthConfigured = Boolean(
6186
+ settings.managedAuthGoogleClientId || settings.managedAuthGithubClientId,
6187
+ );
6188
+ if (managedSocialAuthConfigured) {
6189
+ if (settings.productAccessMode !== "managed") {
6190
+ throw new Error(
6191
+ "Managed Google/GitHub authentication requires OPENGENI_PRODUCT_ACCESS_MODE=managed",
6192
+ );
6193
+ }
6194
+ const publicOrigin = canonicalPublicOrigin(settings.publicBaseUrl);
6195
+ if (!publicOrigin) {
6196
+ throw new Error(
6197
+ "OPENGENI_PUBLIC_BASE_URL must be a credential-free HTTP(S) origin when managed social authentication is configured",
6198
+ );
6199
+ }
6200
+ if (!publicOrigin.startsWith("https://") && !["local", "test"].includes(settings.environment)) {
6201
+ throw new Error(
6202
+ "OPENGENI_PUBLIC_BASE_URL must use https when managed social authentication is configured outside local/test",
6203
+ );
6204
+ }
6205
+ }
5338
6206
  environmentsEncryptionKeyBytes(settings);
5339
6207
  if (settings.integrationsEnabled) {
5340
6208
  if (settings.productAccessMode === "managed" && !settings.publicBaseUrl) {
@@ -5401,11 +6269,6 @@ function validateSettings(settings: Settings): void {
5401
6269
  "OPENGENI_INTEGRATIONS_ENABLED=true is required when personal GitHub OAuth is enabled",
5402
6270
  );
5403
6271
  }
5404
- if (settings.productAccessMode !== "managed") {
5405
- throw new Error(
5406
- "OPENGENI_GITHUB_PERSONAL_OAUTH_ENABLED=true requires OPENGENI_PRODUCT_ACCESS_MODE=managed",
5407
- );
5408
- }
5409
6272
  if (!settings.githubPersonalOauthClientId || !settings.githubPersonalOauthClientSecret) {
5410
6273
  throw new Error(
5411
6274
  "personal GitHub OAuth requires OPENGENI_GITHUB_PERSONAL_OAUTH_CLIENT_ID and OPENGENI_GITHUB_PERSONAL_OAUTH_CLIENT_SECRET",
@@ -5559,15 +6422,6 @@ function validateSettings(settings: Settings): void {
5559
6422
  if (settings.productAccessMode !== "managed" && settings.billingMode === "stripe") {
5560
6423
  throw new Error("OPENGENI_BILLING_MODE=stripe requires OPENGENI_PRODUCT_ACCESS_MODE=managed");
5561
6424
  }
5562
- if (settings.billingMode === "stripe" || settings.usageLimitsMode === "managed") {
5563
- const pricing = configuredModelPricing(settings);
5564
- const missing = configuredAllowedModels(settings).filter((model) => !pricing[model]);
5565
- if (missing.length > 0) {
5566
- throw new Error(
5567
- `Missing model pricing for managed billing model(s): ${missing.join(", ")}. Set OPENGENI_MODEL_PRICING_JSON.`,
5568
- );
5569
- }
5570
- }
5571
6425
  if (settings.usageLimitsMode === "static") {
5572
6426
  const limits = configuredStaticUsageLimits(settings);
5573
6427
  if (Object.keys(limits).length === 0) {
@@ -5864,15 +6718,23 @@ function validateSettings(settings: Settings): void {
5864
6718
  "OPENGENI_STREAM_TOKEN_SECRET to enable the live desktop stream.",
5865
6719
  );
5866
6720
  }
5867
- // Model provider registry: parse it here so JSON/zod errors surface at boot,
5868
- // reject a registry id colliding with the built-in provider id (it would
5869
- // shadow the built-in in configuredProviders), reject duplicate registry
5870
- // ids, and preserve the existing key requirement for every registry provider
5871
- // except connected Codex and the explicit anonymous opt-in. Anonymous
5872
- // providers are externally metered; a missing key on every other ordinary
5873
- // provider remains a boot error.
5874
- // Registry models flow through configuredAllowedModels, so the managed-billing
5875
- // pricing check above already covers the OpenGeni-credit providers.
6721
+ if (settings.modelCatalogSource === "code") {
6722
+ validateModelCatalogSettings(settings, source);
6723
+ } else {
6724
+ // Database mode resolves membership asynchronously. Only the independent
6725
+ // deployment funding JSON is parsed here; env catalog and note inputs are
6726
+ // intentionally ignored until resolveCatalogSettings applies the singleton.
6727
+ parseModelCostPolicyJson(settings.modelCostPolicyJson);
6728
+ }
6729
+ }
6730
+
6731
+ /** Validate one fully resolved, secret-bearing executable catalog. */
6732
+ export function validateModelCatalogSettings(
6733
+ settings: Settings,
6734
+ source: NodeJS.ProcessEnv = process.env,
6735
+ ): ConfiguredModel[] {
6736
+ const costPolicy = parseModelCostPolicyJson(settings.modelCostPolicyJson);
6737
+ const notes = parseModelNotesJson(settings.modelNotesJson);
5876
6738
  const registryProviders = parseModelProvidersJson(settings.modelProvidersJson);
5877
6739
  const builtinId = builtinProviderId(settings);
5878
6740
  const providerIds = new Set<string>();
@@ -5880,12 +6742,18 @@ function validateSettings(settings: Settings): void {
5880
6742
  if (
5881
6743
  provider.kind === "vercel-gateway-managed" ||
5882
6744
  provider.kind === "vercel-gateway-workspace" ||
6745
+ provider.kind === "openrouter-workspace" ||
5883
6746
  provider.kind === "xai-subscription"
5884
6747
  ) {
5885
6748
  throw new Error(
5886
6749
  `OPENGENI_MODEL_PROVIDERS_JSON provider kind ${provider.kind} is reserved for a reviewed OpenGeni credential broker`,
5887
6750
  );
5888
6751
  }
6752
+ if (RESERVED_MODEL_PROVIDER_IDS.has(provider.id)) {
6753
+ throw new Error(
6754
+ `OPENGENI_MODEL_PROVIDERS_JSON provider id ${provider.id} is reserved for a reviewed OpenGeni provider`,
6755
+ );
6756
+ }
5889
6757
  if (provider.id === builtinId) {
5890
6758
  throw new Error(
5891
6759
  `OPENGENI_MODEL_PROVIDERS_JSON provider id ${provider.id} collides with the built-in provider id`,
@@ -5900,7 +6768,7 @@ function validateSettings(settings: Settings): void {
5900
6768
  if (
5901
6769
  provider.kind !== "codex-subscription" &&
5902
6770
  provider.kind !== "anonymous" &&
5903
- !resolveProviderApiKey(provider)
6771
+ !resolveProviderApiKey(provider, source)
5904
6772
  ) {
5905
6773
  throw new Error(
5906
6774
  `OPENGENI_MODEL_PROVIDERS_JSON provider ${provider.id} requires a resolvable API key (set apiKey or apiKeyEnv)`,
@@ -5910,7 +6778,65 @@ function validateSettings(settings: Settings): void {
5910
6778
  // Materialize the normalized catalog at boot so canonical product ids,
5911
6779
  // aliases, definition digests, and capability/pricing normalization are
5912
6780
  // validated even when managed billing is disabled.
5913
- configuredModels(settings);
6781
+ const models = configuredModels(settings, source);
6782
+ const defaultCatalogSettings = settingsForTurnExecutionPolicy(settings, settings.openaiModel);
6783
+ const defaultCatalogModels =
6784
+ defaultCatalogSettings === settings ? models : configuredModels(defaultCatalogSettings, source);
6785
+ if (models.length === 0 && defaultCatalogModels.length === 0) {
6786
+ throw new Error("The resolved model catalog contains no executable models");
6787
+ }
6788
+ const defaultModelId = canonicalizeConfiguredModelId(
6789
+ defaultCatalogSettings,
6790
+ settings.openaiModel,
6791
+ );
6792
+ if (!defaultCatalogModels.some((model) => model.id === defaultModelId)) {
6793
+ throw new Error(
6794
+ `The default model ${settings.openaiModel} is not executable in the resolved model catalog`,
6795
+ );
6796
+ }
6797
+
6798
+ const deploymentProductIds = new Set(
6799
+ models.filter((model) => model.credentialSource.kind === "deployment").map((model) => model.id),
6800
+ );
6801
+ const noteProductIds = new Set(models.map((model) => model.id));
6802
+ for (const model of configuredGatewayCatalogModels(settings)) {
6803
+ deploymentProductIds.add(model.productId);
6804
+ noteProductIds.add(model.productId);
6805
+ noteProductIds.add(model.workspaceProductId);
6806
+ }
6807
+ for (const model of configuredOpenRouterCatalogModels(settings)) {
6808
+ const productId = `${OPENROUTER_MODEL_ID_PREFIX}${model.upstreamModelId}`;
6809
+ deploymentProductIds.add(productId);
6810
+ noteProductIds.add(productId);
6811
+ noteProductIds.add(`${WORKSPACE_OPENROUTER_MODEL_ID_PREFIX}${model.upstreamModelId}`);
6812
+ }
6813
+ if (settings.modelCatalogSource === "code") {
6814
+ for (const productId of Object.keys(costPolicy)) {
6815
+ if (!deploymentProductIds.has(productId)) {
6816
+ throw new Error(
6817
+ `OPENGENI_MODEL_COST_POLICY_JSON references unknown deployment model ${productId}`,
6818
+ );
6819
+ }
6820
+ }
6821
+ }
6822
+ for (const productId of Object.keys(notes)) {
6823
+ if (!noteProductIds.has(productId)) {
6824
+ throw new Error(`OPENGENI_MODEL_NOTES_JSON references unknown catalog model ${productId}`);
6825
+ }
6826
+ }
6827
+
6828
+ if (settings.billingMode === "stripe" || settings.usageLimitsMode === "managed") {
6829
+ const pricing = configuredModelPricing(settings);
6830
+ const missing = models
6831
+ .filter((model) => model.cost === "credits" && !pricing[model.id])
6832
+ .map((model) => model.id);
6833
+ if (missing.length > 0) {
6834
+ throw new Error(
6835
+ `Missing model pricing for managed billing model(s): ${missing.join(", ")}. Set OPENGENI_MODEL_PRICING_JSON.`,
6836
+ );
6837
+ }
6838
+ }
6839
+ return models;
5914
6840
  }
5915
6841
 
5916
6842
  /**