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