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