@opengeni/config 0.6.2 → 0.6.9
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 +376 -8
- package/dist/index.js +742 -65
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/index.ts +1002 -86
package/src/index.ts
CHANGED
|
@@ -6,10 +6,24 @@ import {
|
|
|
6
6
|
ProductAccessMode,
|
|
7
7
|
ReasoningEffort,
|
|
8
8
|
SandboxBackend,
|
|
9
|
+
SessionMcpApprovalPolicy,
|
|
9
10
|
StaticUsageLimits,
|
|
11
|
+
TurnExecutionPolicyV1,
|
|
10
12
|
UsageLimitsMode,
|
|
13
|
+
type TurnExecutionModelSourceV1,
|
|
14
|
+
type TurnExecutionReasoningSourceV1,
|
|
11
15
|
} from "@opengeni/contracts";
|
|
12
|
-
import {
|
|
16
|
+
import { CODEX_MODEL_TOOL_OUTPUT_TRUNCATION_TOKENS } from "@opengeni/codex";
|
|
17
|
+
import {
|
|
18
|
+
CODEX_FALLBACK_MODEL_SLUGS,
|
|
19
|
+
CODEX_MODEL_AUTO_COMPACT_TOKEN_LIMIT,
|
|
20
|
+
CODEX_MODEL_CONTEXT_WINDOW_TOKENS,
|
|
21
|
+
CODEX_MODEL_EFFECTIVE_CONTEXT_WINDOW_TOKENS,
|
|
22
|
+
CODEX_MODEL_ID_PREFIX,
|
|
23
|
+
CODEX_PROVIDER_BASE_URL,
|
|
24
|
+
CODEX_PROVIDER_ID,
|
|
25
|
+
} from "@opengeni/codex/constants";
|
|
26
|
+
import { createHash } from "node:crypto";
|
|
13
27
|
import { z } from "zod";
|
|
14
28
|
|
|
15
29
|
const envName = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
@@ -218,6 +232,9 @@ const SettingsSchema = z.object({
|
|
|
218
232
|
// holder of stream:control gets 403 until this flips. Keeps stream:control a
|
|
219
233
|
// declared-but-inert permission so later hardening is a flag flip.
|
|
220
234
|
streamControlEnabled: EnvBoolean.default(false),
|
|
235
|
+
// Existing-session explicit tool replacement is gated until every API and
|
|
236
|
+
// worker instance understands durable tools_provided provenance.
|
|
237
|
+
sessionTurnToolReplacementEnabled: EnvBoolean.default(false),
|
|
221
238
|
toolspaceEnabled: EnvBoolean.default(false),
|
|
222
239
|
toolspaceMaxCallsPerTurn: z.coerce.number().int().positive().default(200),
|
|
223
240
|
// Optional release-coherent bootstrap hint for custom rigs/connected machines
|
|
@@ -516,6 +533,15 @@ const SettingsSchema = z.object({
|
|
|
516
533
|
// EnvBoolean (NOT z.coerce.boolean(), which would coerce "false" -> true and
|
|
517
534
|
// turn the flag ON the moment anyone set the env var to disable it).
|
|
518
535
|
sandboxOwnershipEnabled: EnvBoolean.default(false),
|
|
536
|
+
// --- standalone rig-verifier ownership rollout flag, default OFF ---
|
|
537
|
+
// Rig verification creates a throwaway provider sandbox outside the normal
|
|
538
|
+
// session-turn path. When enabled, that sandbox must first acquire the same
|
|
539
|
+
// durable lease lifecycle used by session boxes so the global orphan sweep
|
|
540
|
+
// recognizes its exact provider instance. Keep this separate from the general
|
|
541
|
+
// sandboxOwnershipEnabled rollout: every reaper worker must understand verifier
|
|
542
|
+
// leases before dispatch is enabled. When false the verifier fails closed before
|
|
543
|
+
// provider create; it never falls back to the legacy unowned path.
|
|
544
|
+
rigVerificationLeaseOwnershipEnabled: EnvBoolean.default(false),
|
|
519
545
|
// --- lazy sandbox provisioning rollout flag, default OFF ---
|
|
520
546
|
// Only effective when sandboxOwnershipEnabled is ALSO on (lazy provisioning is a
|
|
521
547
|
// property of the owned path — the SDK never creates/resumes an injected session,
|
|
@@ -736,14 +762,8 @@ const SettingsSchema = z.object({
|
|
|
736
762
|
allowedTools: z.array(z.string().min(1)).optional(),
|
|
737
763
|
timeoutMs: z.number().int().positive().optional(),
|
|
738
764
|
cacheToolsList: z.boolean().default(false),
|
|
739
|
-
/**
|
|
740
|
-
|
|
741
|
-
* session MCP server row (never from OPENGENI_MCP_SERVERS). `true` = all
|
|
742
|
-
* tools require approval; a string[] = only the listed UNPREFIXED tool
|
|
743
|
-
* names do; absent = auto-run (the historical default). Enforced in the
|
|
744
|
-
* runtime by attaching `needsApproval` to the matching MCP tools.
|
|
745
|
-
*/
|
|
746
|
-
requireApproval: z.union([z.boolean(), z.array(z.string().min(1))]).optional(),
|
|
765
|
+
/** Runtime approval policy, overlaid from an attempt-frozen session snapshot. */
|
|
766
|
+
requireApproval: SessionMcpApprovalPolicy.optional(),
|
|
747
767
|
/**
|
|
748
768
|
* Extra request headers sent to this MCP server (credential injection
|
|
749
769
|
* for workspace-enabled capability MCPs). Populated at runtime from
|
|
@@ -778,6 +798,15 @@ export type ModelPricing = {
|
|
|
778
798
|
outputMicrosPerMillionTokens: number;
|
|
779
799
|
marginBps?: number | undefined;
|
|
780
800
|
};
|
|
801
|
+
export type ModelPricingScheduleV1 = {
|
|
802
|
+
default: ModelPricing;
|
|
803
|
+
inputTokenTiers?:
|
|
804
|
+
| Array<{
|
|
805
|
+
minimumInputTokens: number;
|
|
806
|
+
pricing: ModelPricing;
|
|
807
|
+
}>
|
|
808
|
+
| undefined;
|
|
809
|
+
};
|
|
781
810
|
export type ModelUsageInput = {
|
|
782
811
|
inputTokens?: number | undefined;
|
|
783
812
|
outputTokens?: number | undefined;
|
|
@@ -796,6 +825,164 @@ const ModelPricingSchema = z.object({
|
|
|
796
825
|
marginBps: z.number().int().min(0).max(100_000).optional(),
|
|
797
826
|
});
|
|
798
827
|
|
|
828
|
+
const ModelPricingScheduleSchema = z
|
|
829
|
+
.object({
|
|
830
|
+
default: ModelPricingSchema,
|
|
831
|
+
inputTokenTiers: z
|
|
832
|
+
.array(
|
|
833
|
+
z.object({
|
|
834
|
+
minimumInputTokens: z.number().int().nonnegative(),
|
|
835
|
+
pricing: ModelPricingSchema,
|
|
836
|
+
}),
|
|
837
|
+
)
|
|
838
|
+
.optional(),
|
|
839
|
+
})
|
|
840
|
+
.superRefine((schedule, ctx) => {
|
|
841
|
+
let previous = -1;
|
|
842
|
+
for (const [index, tier] of (schedule.inputTokenTiers ?? []).entries()) {
|
|
843
|
+
if (tier.minimumInputTokens <= previous) {
|
|
844
|
+
ctx.addIssue({
|
|
845
|
+
code: "custom",
|
|
846
|
+
path: ["inputTokenTiers", index, "minimumInputTokens"],
|
|
847
|
+
message: "input-token tier thresholds must be strictly increasing",
|
|
848
|
+
});
|
|
849
|
+
}
|
|
850
|
+
previous = tier.minimumInputTokens;
|
|
851
|
+
}
|
|
852
|
+
});
|
|
853
|
+
|
|
854
|
+
export const CapabilitySupportV1 = z.enum(["supported", "unsupported", "unknown"]);
|
|
855
|
+
export type CapabilitySupportV1 = z.infer<typeof CapabilitySupportV1>;
|
|
856
|
+
|
|
857
|
+
export const CapabilityStateV1Schema = z
|
|
858
|
+
.object({
|
|
859
|
+
upstream: CapabilitySupportV1,
|
|
860
|
+
runnable: z.boolean(),
|
|
861
|
+
})
|
|
862
|
+
.superRefine((state, ctx) => {
|
|
863
|
+
if (state.upstream === "unsupported" && state.runnable) {
|
|
864
|
+
ctx.addIssue({
|
|
865
|
+
code: "custom",
|
|
866
|
+
path: ["runnable"],
|
|
867
|
+
message: "an upstream-unsupported capability cannot be runnable",
|
|
868
|
+
});
|
|
869
|
+
}
|
|
870
|
+
});
|
|
871
|
+
export type CapabilityStateV1 = z.infer<typeof CapabilityStateV1Schema>;
|
|
872
|
+
|
|
873
|
+
const ModelModalityV1 = z.enum(["text", "image", "audio"]);
|
|
874
|
+
const ModelLatencyModeV1 = z.enum(["standard", "priority", "fast"]);
|
|
875
|
+
|
|
876
|
+
export const ModelCapabilitiesV1Schema = z
|
|
877
|
+
.object({
|
|
878
|
+
reasoning: CapabilityStateV1Schema.extend({
|
|
879
|
+
efforts: z.array(ReasoningEffort),
|
|
880
|
+
defaultEffort: ReasoningEffort.nullable(),
|
|
881
|
+
required: z.boolean(),
|
|
882
|
+
}),
|
|
883
|
+
functionCalling: CapabilityStateV1Schema,
|
|
884
|
+
structuredOutput: CapabilityStateV1Schema,
|
|
885
|
+
hostedTools: z.object({
|
|
886
|
+
webSearch: CapabilityStateV1Schema,
|
|
887
|
+
xSearch: CapabilityStateV1Schema,
|
|
888
|
+
codeExecution: CapabilityStateV1Schema,
|
|
889
|
+
}),
|
|
890
|
+
inputModalities: z.array(ModelModalityV1).min(1),
|
|
891
|
+
outputModalities: z.array(ModelModalityV1).min(1),
|
|
892
|
+
transports: z.object({
|
|
893
|
+
sse: CapabilityStateV1Schema,
|
|
894
|
+
responsesWebSocket: CapabilityStateV1Schema,
|
|
895
|
+
realtimeAudio: CapabilityStateV1Schema,
|
|
896
|
+
}),
|
|
897
|
+
latencyModes: z
|
|
898
|
+
.array(
|
|
899
|
+
z.object({
|
|
900
|
+
id: ModelLatencyModeV1,
|
|
901
|
+
upstream: CapabilitySupportV1,
|
|
902
|
+
runnable: z.boolean(),
|
|
903
|
+
billingMultiplierBps: z.number().int().positive().optional(),
|
|
904
|
+
}),
|
|
905
|
+
)
|
|
906
|
+
.min(1),
|
|
907
|
+
})
|
|
908
|
+
.superRefine((capabilities, ctx) => {
|
|
909
|
+
const efforts = new Set(capabilities.reasoning.efforts);
|
|
910
|
+
if (efforts.size !== capabilities.reasoning.efforts.length) {
|
|
911
|
+
ctx.addIssue({
|
|
912
|
+
code: "custom",
|
|
913
|
+
path: ["reasoning", "efforts"],
|
|
914
|
+
message: "reasoning efforts must be unique",
|
|
915
|
+
});
|
|
916
|
+
}
|
|
917
|
+
if (
|
|
918
|
+
capabilities.reasoning.defaultEffort !== null &&
|
|
919
|
+
!efforts.has(capabilities.reasoning.defaultEffort)
|
|
920
|
+
) {
|
|
921
|
+
ctx.addIssue({
|
|
922
|
+
code: "custom",
|
|
923
|
+
path: ["reasoning", "defaultEffort"],
|
|
924
|
+
message: "the default reasoning effort must be one of the supported efforts",
|
|
925
|
+
});
|
|
926
|
+
}
|
|
927
|
+
if (capabilities.reasoning.runnable && capabilities.reasoning.efforts.length === 0) {
|
|
928
|
+
ctx.addIssue({
|
|
929
|
+
code: "custom",
|
|
930
|
+
path: ["reasoning", "efforts"],
|
|
931
|
+
message: "a runnable reasoning capability must declare at least one effort",
|
|
932
|
+
});
|
|
933
|
+
}
|
|
934
|
+
for (const field of ["inputModalities", "outputModalities"] as const) {
|
|
935
|
+
if (new Set(capabilities[field]).size !== capabilities[field].length) {
|
|
936
|
+
ctx.addIssue({
|
|
937
|
+
code: "custom",
|
|
938
|
+
path: [field],
|
|
939
|
+
message: `${field} must be unique`,
|
|
940
|
+
});
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
const latencyIds = new Set<string>();
|
|
944
|
+
for (const [index, mode] of capabilities.latencyModes.entries()) {
|
|
945
|
+
if (latencyIds.has(mode.id)) {
|
|
946
|
+
ctx.addIssue({
|
|
947
|
+
code: "custom",
|
|
948
|
+
path: ["latencyModes", index, "id"],
|
|
949
|
+
message: "latency mode ids must be unique",
|
|
950
|
+
});
|
|
951
|
+
}
|
|
952
|
+
latencyIds.add(mode.id);
|
|
953
|
+
if (mode.upstream === "unsupported" && mode.runnable) {
|
|
954
|
+
ctx.addIssue({
|
|
955
|
+
code: "custom",
|
|
956
|
+
path: ["latencyModes", index, "runnable"],
|
|
957
|
+
message: "an upstream-unsupported latency mode cannot be runnable",
|
|
958
|
+
});
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
});
|
|
962
|
+
export type ModelCapabilitiesV1 = z.infer<typeof ModelCapabilitiesV1Schema>;
|
|
963
|
+
|
|
964
|
+
export type ModelDeploymentV1 = {
|
|
965
|
+
upstreamModelId: string;
|
|
966
|
+
wireApi: ModelProviderApi;
|
|
967
|
+
};
|
|
968
|
+
|
|
969
|
+
export type ModelExecutionLimitsV1 = {
|
|
970
|
+
contextWindowTokens: number | null;
|
|
971
|
+
effectiveContextWindowTokens: number | null;
|
|
972
|
+
autoCompactTokenLimit: number | null;
|
|
973
|
+
toolOutputTruncationTokens: number | null;
|
|
974
|
+
};
|
|
975
|
+
|
|
976
|
+
export type CredentialSourceV1 =
|
|
977
|
+
| { kind: "deployment"; mechanism: "api_key" | "azure_ad_bearer" }
|
|
978
|
+
| { kind: "connected_subscription"; provider: "codex" }
|
|
979
|
+
| { kind: "workspace_connection"; mechanism: "api_key" };
|
|
980
|
+
|
|
981
|
+
export type BillingAttributionV1 = {
|
|
982
|
+
upstreamPayer: "deployment" | "workspace" | "connected_subscription";
|
|
983
|
+
metering: "opengeni_credits" | "external";
|
|
984
|
+
};
|
|
985
|
+
|
|
799
986
|
/**
|
|
800
987
|
* Wire API a provider speaks. The built-in OpenAI/Azure provider always uses
|
|
801
988
|
* "responses" (the OpenAI Responses API). Extra registry providers default to
|
|
@@ -815,19 +1002,52 @@ export const RegistryProviderKind = z.enum(["api-key", "codex-subscription"]);
|
|
|
815
1002
|
export type RegistryProviderKind = z.infer<typeof RegistryProviderKind>;
|
|
816
1003
|
|
|
817
1004
|
/** A single model exposed by a registry provider. */
|
|
818
|
-
const RegistryModelSchema = z
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
1005
|
+
const RegistryModelSchema = z
|
|
1006
|
+
.object({
|
|
1007
|
+
id: z.string().min(1), // canonical OpenGeni product id
|
|
1008
|
+
upstreamModelId: z.string().min(1).optional(), // exact provider slug; defaults to id
|
|
1009
|
+
aliases: z.array(z.string().min(1)).optional(), // accepted input only; never sent upstream
|
|
1010
|
+
label: z.string().min(1).optional(), // display name; defaults to id
|
|
1011
|
+
contextWindowTokens: z.number().int().positive().optional(),
|
|
1012
|
+
effectiveContextWindowTokens: z.number().int().positive().optional(),
|
|
1013
|
+
autoCompactTokenLimit: z.number().int().positive().optional(),
|
|
1014
|
+
// Canonical model-facing function/tool-result policy. The runtime applies
|
|
1015
|
+
// the same 1.2x serialization allowance as Codex when materializing output.
|
|
1016
|
+
toolOutputTruncationTokens: z.number().int().positive().optional(),
|
|
1017
|
+
reasoningEffort: z.boolean().optional(), // legacy compatibility input/projection
|
|
1018
|
+
hostedWebSearch: z.boolean().optional(), // legacy compatibility input/projection
|
|
1019
|
+
capabilities: ModelCapabilitiesV1Schema.optional(),
|
|
1020
|
+
pricing: z.union([ModelPricingSchema, ModelPricingScheduleSchema]).optional(),
|
|
1021
|
+
// Reserved normalized contracts are derived by OpenGeni in V1. Generic
|
|
1022
|
+
// registry JSON must not opt itself into workspace BYOK or reattribute cost.
|
|
1023
|
+
credentialSource: z.never().optional(),
|
|
1024
|
+
billing: z.never().optional(),
|
|
1025
|
+
})
|
|
1026
|
+
.superRefine((model, ctx) => {
|
|
1027
|
+
if (
|
|
1028
|
+
model.capabilities &&
|
|
1029
|
+
model.reasoningEffort !== undefined &&
|
|
1030
|
+
model.reasoningEffort !== model.capabilities.reasoning.runnable
|
|
1031
|
+
) {
|
|
1032
|
+
ctx.addIssue({
|
|
1033
|
+
code: "custom",
|
|
1034
|
+
path: ["reasoningEffort"],
|
|
1035
|
+
message: "legacy reasoningEffort must agree with capabilities.reasoning.runnable",
|
|
1036
|
+
});
|
|
1037
|
+
}
|
|
1038
|
+
if (
|
|
1039
|
+
model.capabilities &&
|
|
1040
|
+
model.hostedWebSearch !== undefined &&
|
|
1041
|
+
model.hostedWebSearch !== model.capabilities.hostedTools.webSearch.runnable
|
|
1042
|
+
) {
|
|
1043
|
+
ctx.addIssue({
|
|
1044
|
+
code: "custom",
|
|
1045
|
+
path: ["hostedWebSearch"],
|
|
1046
|
+
message:
|
|
1047
|
+
"legacy hostedWebSearch must agree with capabilities.hostedTools.webSearch.runnable",
|
|
1048
|
+
});
|
|
1049
|
+
}
|
|
1050
|
+
});
|
|
831
1051
|
|
|
832
1052
|
/** A non-built-in provider declared by the host via OPENGENI_MODEL_PROVIDERS_JSON. */
|
|
833
1053
|
const RegistryProviderSchema = z.object({
|
|
@@ -840,6 +1060,12 @@ const RegistryProviderSchema = z.object({
|
|
|
840
1060
|
apiKeyEnv: z.string().optional(), // ... OR name of the env var holding the key (preferred)
|
|
841
1061
|
defaultQuery: z.record(z.string(), z.string()).optional(),
|
|
842
1062
|
defaultHeaders: z.record(z.string(), z.string()).optional(),
|
|
1063
|
+
publicDefaultQueryNames: z.array(z.string().min(1)).optional(),
|
|
1064
|
+
publicDefaultHeaderNames: z.array(z.string().min(1)).optional(),
|
|
1065
|
+
// V1 derives these from provider kind. Workspace BYOK is deliberately not a
|
|
1066
|
+
// registry switch and requires a separately reviewed encrypted broker.
|
|
1067
|
+
credentialSource: z.never().optional(),
|
|
1068
|
+
billing: z.never().optional(),
|
|
843
1069
|
models: z.array(RegistryModelSchema).min(1),
|
|
844
1070
|
});
|
|
845
1071
|
export type RegistryProvider = z.infer<typeof RegistryProviderSchema>;
|
|
@@ -870,15 +1096,29 @@ export interface ResolvedModelProvider {
|
|
|
870
1096
|
apiKey?: string | undefined;
|
|
871
1097
|
defaultQuery?: Record<string, string> | undefined;
|
|
872
1098
|
defaultHeaders?: Record<string, string> | undefined;
|
|
1099
|
+
publicDefaultQueryNames?: string[] | undefined;
|
|
1100
|
+
publicDefaultHeaderNames?: string[] | undefined;
|
|
1101
|
+
credentialSource: CredentialSourceV1;
|
|
1102
|
+
billing: BillingAttributionV1;
|
|
873
1103
|
}
|
|
874
1104
|
|
|
875
1105
|
/** A single exposed model + the provider that serves it. */
|
|
876
1106
|
export interface ConfiguredModel {
|
|
1107
|
+
schemaVersion: 1;
|
|
877
1108
|
id: string;
|
|
1109
|
+
aliases: string[];
|
|
878
1110
|
label: string;
|
|
879
1111
|
providerId: string;
|
|
880
1112
|
providerLabel: string;
|
|
881
1113
|
api: ModelProviderApi;
|
|
1114
|
+
upstreamModelId: string;
|
|
1115
|
+
deployment: ModelDeploymentV1;
|
|
1116
|
+
executionLimits: ModelExecutionLimitsV1;
|
|
1117
|
+
credentialSource: CredentialSourceV1;
|
|
1118
|
+
billing: BillingAttributionV1;
|
|
1119
|
+
capabilities: ModelCapabilitiesV1;
|
|
1120
|
+
pricing?: ModelPricingScheduleV1 | undefined;
|
|
1121
|
+
definitionVersion: string;
|
|
882
1122
|
contextWindowTokens?: number | undefined;
|
|
883
1123
|
effectiveContextWindowTokens?: number | undefined;
|
|
884
1124
|
autoCompactTokenLimit?: number | undefined;
|
|
@@ -1076,6 +1316,7 @@ export function getSettings(): Settings {
|
|
|
1076
1316
|
delegationSecret: optional("OPENGENI_DELEGATION_SECRET"),
|
|
1077
1317
|
streamTokenSecret: optional("OPENGENI_STREAM_TOKEN_SECRET"),
|
|
1078
1318
|
streamControlEnabled: optional("OPENGENI_STREAM_CONTROL_ENABLED"),
|
|
1319
|
+
sessionTurnToolReplacementEnabled: optional("OPENGENI_SESSION_TURN_TOOL_REPLACEMENT_ENABLED"),
|
|
1079
1320
|
toolspaceEnabled: optional("OPENGENI_TOOLSPACE_ENABLED"),
|
|
1080
1321
|
toolspaceMaxCallsPerTurn: optional("OPENGENI_TOOLSPACE_MAX_CALLS_PER_TURN"),
|
|
1081
1322
|
ogtoolPackageSpec: optional("OPENGENI_OGTOOL_PACKAGE_SPEC"),
|
|
@@ -1192,6 +1433,9 @@ export function getSettings(): Settings {
|
|
|
1192
1433
|
vercelTeamId: optional("OPENGENI_VERCEL_TEAM_ID"),
|
|
1193
1434
|
vercelRuntime: optional("OPENGENI_VERCEL_RUNTIME"),
|
|
1194
1435
|
sandboxOwnershipEnabled: optional("OPENGENI_SANDBOX_OWNERSHIP_ENABLED"),
|
|
1436
|
+
rigVerificationLeaseOwnershipEnabled: optional(
|
|
1437
|
+
"OPENGENI_RIG_VERIFICATION_LEASE_OWNERSHIP_ENABLED",
|
|
1438
|
+
),
|
|
1195
1439
|
sandboxLazyProvisionEnabled: optional("OPENGENI_SANDBOX_LAZY_PROVISION"),
|
|
1196
1440
|
sandboxSelfhostedEnabled: optional("OPENGENI_SANDBOX_SELFHOSTED_ENABLED"),
|
|
1197
1441
|
agentOpStreamEnabled: optional("OPENGENI_AGENT_OP_STREAM_ENABLED"),
|
|
@@ -1331,6 +1575,353 @@ export function resolveProviderApiKey(
|
|
|
1331
1575
|
return undefined;
|
|
1332
1576
|
}
|
|
1333
1577
|
|
|
1578
|
+
const HTTP_FIELD_NAME = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
|
|
1579
|
+
const CREDENTIAL_LIKE_NAME_PARTS = new Set([
|
|
1580
|
+
"apikey",
|
|
1581
|
+
"auth",
|
|
1582
|
+
"authorization",
|
|
1583
|
+
"bearer",
|
|
1584
|
+
"credential",
|
|
1585
|
+
"cookie",
|
|
1586
|
+
"key",
|
|
1587
|
+
"password",
|
|
1588
|
+
"secret",
|
|
1589
|
+
"session",
|
|
1590
|
+
"signature",
|
|
1591
|
+
"token",
|
|
1592
|
+
]);
|
|
1593
|
+
const REASONING_EFFORT_ORDER = new Map(
|
|
1594
|
+
ReasoningEffort.options.map((effort, index) => [effort, index]),
|
|
1595
|
+
);
|
|
1596
|
+
const MODALITY_ORDER = new Map(["text", "image", "audio"].map((value, index) => [value, index]));
|
|
1597
|
+
const LATENCY_MODE_ORDER = new Map(
|
|
1598
|
+
["standard", "priority", "fast"].map((value, index) => [value, index]),
|
|
1599
|
+
);
|
|
1600
|
+
|
|
1601
|
+
function normalizeRegistryBaseUrl(value: string, providerId: string): string {
|
|
1602
|
+
const url = new URL(value);
|
|
1603
|
+
if (url.username || url.password) {
|
|
1604
|
+
throw new Error(`provider ${providerId} baseUrl must not contain userinfo`);
|
|
1605
|
+
}
|
|
1606
|
+
if (url.search) {
|
|
1607
|
+
throw new Error(
|
|
1608
|
+
`provider ${providerId} baseUrl must not contain a query; move query entries to defaultQuery`,
|
|
1609
|
+
);
|
|
1610
|
+
}
|
|
1611
|
+
if (url.hash) {
|
|
1612
|
+
throw new Error(`provider ${providerId} baseUrl must not contain a fragment`);
|
|
1613
|
+
}
|
|
1614
|
+
return url.toString();
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
function isCredentialLikeMetadataName(name: string): boolean {
|
|
1618
|
+
return name
|
|
1619
|
+
.toLowerCase()
|
|
1620
|
+
.split(/[-_.]/u)
|
|
1621
|
+
.some((part) => CREDENTIAL_LIKE_NAME_PARTS.has(part));
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
function normalizeHeaderMap(
|
|
1625
|
+
providerId: string,
|
|
1626
|
+
headers: Record<string, string> | undefined,
|
|
1627
|
+
): Record<string, string> | undefined {
|
|
1628
|
+
if (!headers) {
|
|
1629
|
+
return undefined;
|
|
1630
|
+
}
|
|
1631
|
+
const normalized: Record<string, string> = {};
|
|
1632
|
+
const rawByNormalized = new Map<string, string>();
|
|
1633
|
+
for (const [rawName, value] of Object.entries(headers)) {
|
|
1634
|
+
if (!HTTP_FIELD_NAME.test(rawName)) {
|
|
1635
|
+
throw new Error(
|
|
1636
|
+
`provider ${providerId} defaultHeaders contains invalid HTTP field name ${JSON.stringify(rawName)}`,
|
|
1637
|
+
);
|
|
1638
|
+
}
|
|
1639
|
+
const name = rawName.toLowerCase();
|
|
1640
|
+
const previous = rawByNormalized.get(name);
|
|
1641
|
+
if (previous !== undefined) {
|
|
1642
|
+
throw new Error(
|
|
1643
|
+
`provider ${providerId} defaultHeaders names ${JSON.stringify(previous)} and ${JSON.stringify(rawName)} collide after lowercase normalization`,
|
|
1644
|
+
);
|
|
1645
|
+
}
|
|
1646
|
+
if (name === "authorization") {
|
|
1647
|
+
throw new Error(
|
|
1648
|
+
`provider ${providerId} defaultHeaders must not override SDK-managed Authorization`,
|
|
1649
|
+
);
|
|
1650
|
+
}
|
|
1651
|
+
rawByNormalized.set(name, rawName);
|
|
1652
|
+
normalized[name] = value;
|
|
1653
|
+
}
|
|
1654
|
+
return normalized;
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1657
|
+
function normalizePublicHeaderNames(
|
|
1658
|
+
providerId: string,
|
|
1659
|
+
names: string[] | undefined,
|
|
1660
|
+
headers: Record<string, string> | undefined,
|
|
1661
|
+
): string[] | undefined {
|
|
1662
|
+
if (!names) {
|
|
1663
|
+
return undefined;
|
|
1664
|
+
}
|
|
1665
|
+
const normalized: string[] = [];
|
|
1666
|
+
const seen = new Set<string>();
|
|
1667
|
+
for (const rawName of names) {
|
|
1668
|
+
if (!HTTP_FIELD_NAME.test(rawName)) {
|
|
1669
|
+
throw new Error(
|
|
1670
|
+
`provider ${providerId} publicDefaultHeaderNames contains invalid HTTP field name ${JSON.stringify(rawName)}`,
|
|
1671
|
+
);
|
|
1672
|
+
}
|
|
1673
|
+
const name = rawName.toLowerCase();
|
|
1674
|
+
if (seen.has(name)) {
|
|
1675
|
+
throw new Error(
|
|
1676
|
+
`provider ${providerId} publicDefaultHeaderNames contains duplicate normalized name ${JSON.stringify(name)}`,
|
|
1677
|
+
);
|
|
1678
|
+
}
|
|
1679
|
+
if (!(name in (headers ?? {}))) {
|
|
1680
|
+
throw new Error(
|
|
1681
|
+
`provider ${providerId} publicDefaultHeaderNames declares absent defaultHeaders entry ${JSON.stringify(name)}`,
|
|
1682
|
+
);
|
|
1683
|
+
}
|
|
1684
|
+
if (isCredentialLikeMetadataName(name)) {
|
|
1685
|
+
throw new Error(
|
|
1686
|
+
`provider ${providerId} publicDefaultHeaderNames cannot classify credential-like name ${JSON.stringify(name)} as public`,
|
|
1687
|
+
);
|
|
1688
|
+
}
|
|
1689
|
+
seen.add(name);
|
|
1690
|
+
normalized.push(name);
|
|
1691
|
+
}
|
|
1692
|
+
return normalized;
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
function normalizeQueryMap(
|
|
1696
|
+
providerId: string,
|
|
1697
|
+
query: Record<string, string> | undefined,
|
|
1698
|
+
): Record<string, string> | undefined {
|
|
1699
|
+
if (!query) {
|
|
1700
|
+
return undefined;
|
|
1701
|
+
}
|
|
1702
|
+
for (const name of Object.keys(query)) {
|
|
1703
|
+
if (!name) {
|
|
1704
|
+
throw new Error(`provider ${providerId} defaultQuery contains an empty name`);
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1707
|
+
return { ...query };
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1710
|
+
function normalizePublicQueryNames(
|
|
1711
|
+
providerId: string,
|
|
1712
|
+
names: string[] | undefined,
|
|
1713
|
+
query: Record<string, string> | undefined,
|
|
1714
|
+
): string[] | undefined {
|
|
1715
|
+
if (!names) {
|
|
1716
|
+
return undefined;
|
|
1717
|
+
}
|
|
1718
|
+
const seen = new Set<string>();
|
|
1719
|
+
for (const name of names) {
|
|
1720
|
+
if (seen.has(name)) {
|
|
1721
|
+
throw new Error(
|
|
1722
|
+
`provider ${providerId} publicDefaultQueryNames contains duplicate name ${JSON.stringify(name)}`,
|
|
1723
|
+
);
|
|
1724
|
+
}
|
|
1725
|
+
if (!(name in (query ?? {}))) {
|
|
1726
|
+
throw new Error(
|
|
1727
|
+
`provider ${providerId} publicDefaultQueryNames declares absent defaultQuery entry ${JSON.stringify(name)}`,
|
|
1728
|
+
);
|
|
1729
|
+
}
|
|
1730
|
+
if (isCredentialLikeMetadataName(name)) {
|
|
1731
|
+
throw new Error(
|
|
1732
|
+
`provider ${providerId} publicDefaultQueryNames cannot classify credential-like name ${JSON.stringify(name)} as public`,
|
|
1733
|
+
);
|
|
1734
|
+
}
|
|
1735
|
+
seen.add(name);
|
|
1736
|
+
}
|
|
1737
|
+
return [...names];
|
|
1738
|
+
}
|
|
1739
|
+
|
|
1740
|
+
function normalizeRegistryProvider(provider: RegistryProvider): RegistryProvider {
|
|
1741
|
+
const defaultHeaders = normalizeHeaderMap(provider.id, provider.defaultHeaders);
|
|
1742
|
+
const defaultQuery = normalizeQueryMap(provider.id, provider.defaultQuery);
|
|
1743
|
+
return {
|
|
1744
|
+
...provider,
|
|
1745
|
+
baseUrl: normalizeRegistryBaseUrl(provider.baseUrl, provider.id),
|
|
1746
|
+
...(defaultHeaders === undefined ? {} : { defaultHeaders }),
|
|
1747
|
+
...(defaultQuery === undefined ? {} : { defaultQuery }),
|
|
1748
|
+
...(provider.publicDefaultHeaderNames === undefined
|
|
1749
|
+
? {}
|
|
1750
|
+
: {
|
|
1751
|
+
publicDefaultHeaderNames: normalizePublicHeaderNames(
|
|
1752
|
+
provider.id,
|
|
1753
|
+
provider.publicDefaultHeaderNames,
|
|
1754
|
+
defaultHeaders,
|
|
1755
|
+
),
|
|
1756
|
+
}),
|
|
1757
|
+
...(provider.publicDefaultQueryNames === undefined
|
|
1758
|
+
? {}
|
|
1759
|
+
: {
|
|
1760
|
+
publicDefaultQueryNames: normalizePublicQueryNames(
|
|
1761
|
+
provider.id,
|
|
1762
|
+
provider.publicDefaultQueryNames,
|
|
1763
|
+
defaultQuery,
|
|
1764
|
+
),
|
|
1765
|
+
}),
|
|
1766
|
+
};
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1769
|
+
function normalizeModelPricingSchedule(
|
|
1770
|
+
pricing: ModelPricing | ModelPricingScheduleV1,
|
|
1771
|
+
): ModelPricingScheduleV1 {
|
|
1772
|
+
return "default" in pricing ? pricing : { default: pricing };
|
|
1773
|
+
}
|
|
1774
|
+
|
|
1775
|
+
function normalizeCapabilities(capabilities: ModelCapabilitiesV1): ModelCapabilitiesV1 {
|
|
1776
|
+
const parsed = ModelCapabilitiesV1Schema.parse(capabilities);
|
|
1777
|
+
return {
|
|
1778
|
+
...parsed,
|
|
1779
|
+
reasoning: {
|
|
1780
|
+
...parsed.reasoning,
|
|
1781
|
+
efforts: [...parsed.reasoning.efforts].sort(
|
|
1782
|
+
(left, right) =>
|
|
1783
|
+
(REASONING_EFFORT_ORDER.get(left) ?? 0) - (REASONING_EFFORT_ORDER.get(right) ?? 0),
|
|
1784
|
+
),
|
|
1785
|
+
},
|
|
1786
|
+
inputModalities: [...parsed.inputModalities].sort(
|
|
1787
|
+
(left, right) => (MODALITY_ORDER.get(left) ?? 0) - (MODALITY_ORDER.get(right) ?? 0),
|
|
1788
|
+
),
|
|
1789
|
+
outputModalities: [...parsed.outputModalities].sort(
|
|
1790
|
+
(left, right) => (MODALITY_ORDER.get(left) ?? 0) - (MODALITY_ORDER.get(right) ?? 0),
|
|
1791
|
+
),
|
|
1792
|
+
latencyModes: [...parsed.latencyModes].sort(
|
|
1793
|
+
(left, right) =>
|
|
1794
|
+
(LATENCY_MODE_ORDER.get(left.id) ?? 0) - (LATENCY_MODE_ORDER.get(right.id) ?? 0),
|
|
1795
|
+
),
|
|
1796
|
+
};
|
|
1797
|
+
}
|
|
1798
|
+
|
|
1799
|
+
function legacyModelCapabilities(
|
|
1800
|
+
settings: Settings,
|
|
1801
|
+
input: { reasoningEffort: boolean; hostedWebSearch: boolean },
|
|
1802
|
+
): ModelCapabilitiesV1 {
|
|
1803
|
+
const reasoningEfforts = input.reasoningEffort ? configuredAllowedReasoningEfforts(settings) : [];
|
|
1804
|
+
return normalizeCapabilities({
|
|
1805
|
+
reasoning: {
|
|
1806
|
+
upstream: input.reasoningEffort ? "supported" : "unknown",
|
|
1807
|
+
runnable: input.reasoningEffort,
|
|
1808
|
+
efforts: reasoningEfforts,
|
|
1809
|
+
defaultEffort: input.reasoningEffort ? settings.openaiReasoningEffort : null,
|
|
1810
|
+
required: false,
|
|
1811
|
+
},
|
|
1812
|
+
functionCalling: { upstream: "unknown", runnable: true },
|
|
1813
|
+
structuredOutput: { upstream: "unknown", runnable: false },
|
|
1814
|
+
hostedTools: {
|
|
1815
|
+
webSearch: {
|
|
1816
|
+
upstream: input.hostedWebSearch ? "supported" : "unknown",
|
|
1817
|
+
runnable: input.hostedWebSearch,
|
|
1818
|
+
},
|
|
1819
|
+
xSearch: { upstream: "unknown", runnable: false },
|
|
1820
|
+
codeExecution: { upstream: "unknown", runnable: false },
|
|
1821
|
+
},
|
|
1822
|
+
inputModalities: ["text"],
|
|
1823
|
+
outputModalities: ["text"],
|
|
1824
|
+
transports: {
|
|
1825
|
+
sse: { upstream: "unknown", runnable: true },
|
|
1826
|
+
responsesWebSocket: { upstream: "unknown", runnable: false },
|
|
1827
|
+
realtimeAudio: { upstream: "unknown", runnable: false },
|
|
1828
|
+
},
|
|
1829
|
+
latencyModes: [{ id: "standard", upstream: "unknown", runnable: true }],
|
|
1830
|
+
});
|
|
1831
|
+
}
|
|
1832
|
+
|
|
1833
|
+
function registryCredentialSource(provider: RegistryProvider): CredentialSourceV1 {
|
|
1834
|
+
return provider.kind === "codex-subscription"
|
|
1835
|
+
? { kind: "connected_subscription", provider: "codex" }
|
|
1836
|
+
: { kind: "deployment", mechanism: "api_key" };
|
|
1837
|
+
}
|
|
1838
|
+
|
|
1839
|
+
function registryBilling(provider: RegistryProvider): BillingAttributionV1 {
|
|
1840
|
+
return provider.kind === "codex-subscription"
|
|
1841
|
+
? { upstreamPayer: "connected_subscription", metering: "external" }
|
|
1842
|
+
: { upstreamPayer: "deployment", metering: "opengeni_credits" };
|
|
1843
|
+
}
|
|
1844
|
+
|
|
1845
|
+
function builtinCredentialSource(settings: Settings): CredentialSourceV1 {
|
|
1846
|
+
if (settings.openaiProvider === "azure" && !settings.azureOpenaiApiKey) {
|
|
1847
|
+
return { kind: "deployment", mechanism: "azure_ad_bearer" };
|
|
1848
|
+
}
|
|
1849
|
+
return { kind: "deployment", mechanism: "api_key" };
|
|
1850
|
+
}
|
|
1851
|
+
|
|
1852
|
+
function staticRequestMetadataForDigest(provider: ResolvedModelProvider): {
|
|
1853
|
+
headers: Array<{ name: string; classification: "public" | "secret"; value?: string }>;
|
|
1854
|
+
query: Array<{ name: string; classification: "public" | "secret"; value?: string }>;
|
|
1855
|
+
} {
|
|
1856
|
+
const publicHeaders = new Set(provider.publicDefaultHeaderNames ?? []);
|
|
1857
|
+
const publicQuery = new Set(provider.publicDefaultQueryNames ?? []);
|
|
1858
|
+
return {
|
|
1859
|
+
headers: Object.entries(provider.defaultHeaders ?? {})
|
|
1860
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
1861
|
+
.map(([name, value]) =>
|
|
1862
|
+
publicHeaders.has(name)
|
|
1863
|
+
? { name, classification: "public" as const, value }
|
|
1864
|
+
: { name, classification: "secret" as const },
|
|
1865
|
+
),
|
|
1866
|
+
query: Object.entries(provider.defaultQuery ?? {})
|
|
1867
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
1868
|
+
.map(([name, value]) =>
|
|
1869
|
+
publicQuery.has(name)
|
|
1870
|
+
? { name, classification: "public" as const, value }
|
|
1871
|
+
: { name, classification: "secret" as const },
|
|
1872
|
+
),
|
|
1873
|
+
};
|
|
1874
|
+
}
|
|
1875
|
+
|
|
1876
|
+
function canonicalJson(value: unknown): string {
|
|
1877
|
+
const normalize = (input: unknown): unknown => {
|
|
1878
|
+
if (Array.isArray(input)) {
|
|
1879
|
+
return input.map((entry) => normalize(entry));
|
|
1880
|
+
}
|
|
1881
|
+
if (input && typeof input === "object") {
|
|
1882
|
+
const out: Record<string, unknown> = {};
|
|
1883
|
+
for (const key of Object.keys(input).sort()) {
|
|
1884
|
+
const child = (input as Record<string, unknown>)[key];
|
|
1885
|
+
if (child !== undefined) {
|
|
1886
|
+
out[key] = normalize(child);
|
|
1887
|
+
}
|
|
1888
|
+
}
|
|
1889
|
+
return out;
|
|
1890
|
+
}
|
|
1891
|
+
return input;
|
|
1892
|
+
};
|
|
1893
|
+
return JSON.stringify(normalize(value));
|
|
1894
|
+
}
|
|
1895
|
+
|
|
1896
|
+
function definitionVersionFor(
|
|
1897
|
+
model: Omit<ConfiguredModel, "definitionVersion">,
|
|
1898
|
+
provider: ResolvedModelProvider,
|
|
1899
|
+
): string {
|
|
1900
|
+
const requestMetadata = staticRequestMetadataForDigest(provider);
|
|
1901
|
+
const digestInput = canonicalJson({
|
|
1902
|
+
schemaVersion: model.schemaVersion,
|
|
1903
|
+
id: model.id,
|
|
1904
|
+
providerId: model.providerId,
|
|
1905
|
+
deployment: model.deployment,
|
|
1906
|
+
provider: {
|
|
1907
|
+
adapterKind: provider.kind,
|
|
1908
|
+
wireApi: provider.api,
|
|
1909
|
+
baseUrl: provider.baseUrl ?? null,
|
|
1910
|
+
defaultHeaders: requestMetadata.headers,
|
|
1911
|
+
defaultQuery: requestMetadata.query,
|
|
1912
|
+
},
|
|
1913
|
+
credentialSource: model.credentialSource,
|
|
1914
|
+
billing: model.billing,
|
|
1915
|
+
executionLimits: model.executionLimits,
|
|
1916
|
+
capabilities: model.capabilities,
|
|
1917
|
+
pricing: model.pricing ?? null,
|
|
1918
|
+
});
|
|
1919
|
+
return `sha256:${createHash("sha256")
|
|
1920
|
+
.update("opengeni:model-definition:v1\n", "utf8")
|
|
1921
|
+
.update(digestInput, "utf8")
|
|
1922
|
+
.digest("hex")}`;
|
|
1923
|
+
}
|
|
1924
|
+
|
|
1334
1925
|
/**
|
|
1335
1926
|
* The built-in provider's stable id: "openai" on the OpenAI platform, "azure"
|
|
1336
1927
|
* on Azure. Exported because the workspace model-policy gate must attribute
|
|
@@ -1355,18 +1946,24 @@ function builtinProviderLabel(settings: Pick<Settings, "openaiProvider">): strin
|
|
|
1355
1946
|
* id — validateSettings rejects that at boot.
|
|
1356
1947
|
*/
|
|
1357
1948
|
export function configuredProviders(settings: Settings): ResolvedModelProvider[] {
|
|
1949
|
+
const credentialSource = builtinCredentialSource(settings);
|
|
1358
1950
|
const builtin: ResolvedModelProvider = {
|
|
1359
1951
|
id: builtinProviderId(settings),
|
|
1360
1952
|
label: builtinProviderLabel(settings),
|
|
1361
1953
|
kind: "api-key",
|
|
1362
1954
|
api: "responses",
|
|
1363
1955
|
builtin: true,
|
|
1956
|
+
credentialSource,
|
|
1957
|
+
billing: { upstreamPayer: "deployment", metering: "opengeni_credits" },
|
|
1364
1958
|
};
|
|
1365
1959
|
if (settings.openaiProvider === "azure") {
|
|
1366
|
-
|
|
1960
|
+
const baseUrl = settings.azureOpenaiBaseUrl ?? settings.azureOpenaiEndpoint;
|
|
1961
|
+
builtin.baseUrl = baseUrl ? normalizeRegistryBaseUrl(baseUrl, builtin.id) : undefined;
|
|
1367
1962
|
builtin.apiKey = settings.azureOpenaiApiKey ?? settings.azureOpenaiAdToken;
|
|
1368
1963
|
} else {
|
|
1369
|
-
builtin.baseUrl = settings.openaiBaseUrl
|
|
1964
|
+
builtin.baseUrl = settings.openaiBaseUrl
|
|
1965
|
+
? normalizeRegistryBaseUrl(settings.openaiBaseUrl, builtin.id)
|
|
1966
|
+
: undefined;
|
|
1370
1967
|
builtin.apiKey = settings.openaiApiKey;
|
|
1371
1968
|
}
|
|
1372
1969
|
const registry = parseModelProvidersJson(settings.modelProvidersJson).map(
|
|
@@ -1380,11 +1977,46 @@ export function configuredProviders(settings: Settings): ResolvedModelProvider[]
|
|
|
1380
1977
|
apiKey: resolveProviderApiKey(provider),
|
|
1381
1978
|
defaultQuery: provider.defaultQuery,
|
|
1382
1979
|
defaultHeaders: provider.defaultHeaders,
|
|
1980
|
+
publicDefaultQueryNames: provider.publicDefaultQueryNames,
|
|
1981
|
+
publicDefaultHeaderNames: provider.publicDefaultHeaderNames,
|
|
1982
|
+
credentialSource: registryCredentialSource(provider),
|
|
1983
|
+
billing: registryBilling(provider),
|
|
1383
1984
|
}),
|
|
1384
1985
|
);
|
|
1385
1986
|
return [builtin, ...registry];
|
|
1386
1987
|
}
|
|
1387
1988
|
|
|
1989
|
+
/**
|
|
1990
|
+
* Pure catalog overlay for a workspace whose existing Codex connection seam
|
|
1991
|
+
* reports ready. This describes product/provider identity only; it does not
|
|
1992
|
+
* select, lease, refresh, or expose a concrete credential; those runtime
|
|
1993
|
+
* operations remain owned by the credential allocator.
|
|
1994
|
+
*/
|
|
1995
|
+
export function withCodexCatalogProvider(settings: Settings): Settings {
|
|
1996
|
+
const providers = parseModelProvidersJson(settings.modelProvidersJson);
|
|
1997
|
+
if (providers.some((provider) => provider.id === CODEX_PROVIDER_ID)) {
|
|
1998
|
+
return settings;
|
|
1999
|
+
}
|
|
2000
|
+
const provider: RegistryProvider = {
|
|
2001
|
+
kind: "codex-subscription",
|
|
2002
|
+
id: CODEX_PROVIDER_ID,
|
|
2003
|
+
label: "Codex (ChatGPT subscription)",
|
|
2004
|
+
api: "responses",
|
|
2005
|
+
baseUrl: CODEX_PROVIDER_BASE_URL,
|
|
2006
|
+
models: CODEX_FALLBACK_MODEL_SLUGS.map((slug) => ({
|
|
2007
|
+
id: `${CODEX_MODEL_ID_PREFIX}${slug}`,
|
|
2008
|
+
upstreamModelId: slug,
|
|
2009
|
+
label: slug,
|
|
2010
|
+
reasoningEffort: true,
|
|
2011
|
+
contextWindowTokens: CODEX_MODEL_CONTEXT_WINDOW_TOKENS,
|
|
2012
|
+
effectiveContextWindowTokens: CODEX_MODEL_EFFECTIVE_CONTEXT_WINDOW_TOKENS,
|
|
2013
|
+
autoCompactTokenLimit: CODEX_MODEL_AUTO_COMPACT_TOKEN_LIMIT,
|
|
2014
|
+
toolOutputTruncationTokens: CODEX_MODEL_TOOL_OUTPUT_TRUNCATION_TOKENS,
|
|
2015
|
+
})),
|
|
2016
|
+
};
|
|
2017
|
+
return { ...settings, modelProvidersJson: JSON.stringify([...providers, provider]) };
|
|
2018
|
+
}
|
|
2019
|
+
|
|
1388
2020
|
/**
|
|
1389
2021
|
* The provider identity a model id resolves to, for workspace model-policy
|
|
1390
2022
|
* evaluation — MUST agree with the real router (resolveTurnModel /
|
|
@@ -1399,13 +2031,83 @@ export function configuredProviders(settings: Settings): ResolvedModelProvider[]
|
|
|
1399
2031
|
* serves. A policy blocking the built-in must block this path too.
|
|
1400
2032
|
*/
|
|
1401
2033
|
export function policyProviderIdForModel(settings: Settings, modelId: string): string {
|
|
1402
|
-
|
|
2034
|
+
const canonicalModelId = canonicalizeConfiguredModelId(settings, modelId);
|
|
2035
|
+
if (canonicalModelId.startsWith(CODEX_MODEL_ID_PREFIX)) {
|
|
1403
2036
|
return CODEX_PROVIDER_ID;
|
|
1404
2037
|
}
|
|
1405
|
-
const configured = configuredModels(settings).find((model) => model.id ===
|
|
2038
|
+
const configured = configuredModels(settings).find((model) => model.id === canonicalModelId);
|
|
1406
2039
|
return configured?.providerId ?? builtinProviderId(settings);
|
|
1407
2040
|
}
|
|
1408
2041
|
|
|
2042
|
+
function resolvedExecutionLimits(
|
|
2043
|
+
settings: Settings,
|
|
2044
|
+
model: {
|
|
2045
|
+
contextWindowTokens?: number | undefined;
|
|
2046
|
+
effectiveContextWindowTokens?: number | undefined;
|
|
2047
|
+
autoCompactTokenLimit?: number | undefined;
|
|
2048
|
+
toolOutputTruncationTokens?: number | undefined;
|
|
2049
|
+
},
|
|
2050
|
+
): ModelExecutionLimitsV1 {
|
|
2051
|
+
return {
|
|
2052
|
+
contextWindowTokens: model.contextWindowTokens ?? settings.contextWindowTokens,
|
|
2053
|
+
effectiveContextWindowTokens:
|
|
2054
|
+
model.effectiveContextWindowTokens ?? settings.contextEffectiveWindowTokens ?? null,
|
|
2055
|
+
autoCompactTokenLimit:
|
|
2056
|
+
model.autoCompactTokenLimit ?? settings.contextAutoCompactThresholdTokens ?? null,
|
|
2057
|
+
toolOutputTruncationTokens:
|
|
2058
|
+
model.toolOutputTruncationTokens ?? settings.modelToolOutputTruncationTokens ?? null,
|
|
2059
|
+
};
|
|
2060
|
+
}
|
|
2061
|
+
|
|
2062
|
+
function finalizeConfiguredModel(
|
|
2063
|
+
settings: Settings,
|
|
2064
|
+
provider: ResolvedModelProvider,
|
|
2065
|
+
input: Omit<ConfiguredModel, "schemaVersion" | "definitionVersion" | "executionLimits">,
|
|
2066
|
+
): ConfiguredModel {
|
|
2067
|
+
const modelWithoutVersion: Omit<ConfiguredModel, "definitionVersion"> = {
|
|
2068
|
+
schemaVersion: 1,
|
|
2069
|
+
...input,
|
|
2070
|
+
executionLimits: resolvedExecutionLimits(settings, input),
|
|
2071
|
+
};
|
|
2072
|
+
return {
|
|
2073
|
+
...modelWithoutVersion,
|
|
2074
|
+
definitionVersion: definitionVersionFor(modelWithoutVersion, provider),
|
|
2075
|
+
};
|
|
2076
|
+
}
|
|
2077
|
+
|
|
2078
|
+
function assertUniqueModelIdentities(models: ConfiguredModel[]): void {
|
|
2079
|
+
const canonicalOwners = new Map<string, string>();
|
|
2080
|
+
for (const model of models) {
|
|
2081
|
+
const previous = canonicalOwners.get(model.id);
|
|
2082
|
+
if (previous !== undefined) {
|
|
2083
|
+
throw new Error(
|
|
2084
|
+
`OPENGENI_MODEL_PROVIDERS_JSON model id ${JSON.stringify(model.id)} is declared by both ${previous} and ${model.providerId}`,
|
|
2085
|
+
);
|
|
2086
|
+
}
|
|
2087
|
+
canonicalOwners.set(model.id, model.providerId);
|
|
2088
|
+
}
|
|
2089
|
+
|
|
2090
|
+
const acceptedInputs = new Map(canonicalOwners);
|
|
2091
|
+
for (const model of models) {
|
|
2092
|
+
const ownAliases = new Set<string>();
|
|
2093
|
+
for (const alias of model.aliases) {
|
|
2094
|
+
if (ownAliases.has(alias)) {
|
|
2095
|
+
throw new Error(
|
|
2096
|
+
`OPENGENI_MODEL_PROVIDERS_JSON model ${JSON.stringify(model.id)} contains duplicate alias ${JSON.stringify(alias)}`,
|
|
2097
|
+
);
|
|
2098
|
+
}
|
|
2099
|
+
ownAliases.add(alias);
|
|
2100
|
+
const previous = acceptedInputs.get(alias);
|
|
2101
|
+
if (previous !== undefined) {
|
|
2102
|
+
throw new Error(
|
|
2103
|
+
`OPENGENI_MODEL_PROVIDERS_JSON alias ${JSON.stringify(alias)} for model ${JSON.stringify(model.id)} collides with model/provider ${previous}`,
|
|
2104
|
+
);
|
|
2105
|
+
}
|
|
2106
|
+
acceptedInputs.set(alias, model.id);
|
|
2107
|
+
}
|
|
2108
|
+
}
|
|
2109
|
+
}
|
|
2110
|
+
|
|
1409
2111
|
/**
|
|
1410
2112
|
* Every model a client may use, the built-in provider's models first
|
|
1411
2113
|
* (configuredAllowedModels-from-openai, mapped to "responses" with
|
|
@@ -1417,6 +2119,9 @@ export function policyProviderIdForModel(settings: Settings, modelId: string): s
|
|
|
1417
2119
|
export function configuredModels(settings: Settings): ConfiguredModel[] {
|
|
1418
2120
|
const builtinId = builtinProviderId(settings);
|
|
1419
2121
|
const builtinLabel = builtinProviderLabel(settings);
|
|
2122
|
+
const providers = configuredProviders(settings);
|
|
2123
|
+
const providerById = new Map(providers.map((provider) => [provider.id, provider]));
|
|
2124
|
+
const pricingSchedules = configuredModelPricingSchedules(settings);
|
|
1420
2125
|
// The built-in (OpenAI/Azure) provider must NEVER claim a registry-namespaced
|
|
1421
2126
|
// model id. The worker overwrites settings.openaiModel with the turn's model
|
|
1422
2127
|
// (apps/worker agent-turn runSettings) — including a `codex/<slug>` id, or a
|
|
@@ -1433,63 +2138,110 @@ export function configuredModels(settings: Settings): ConfiguredModel[] {
|
|
|
1433
2138
|
// a codex/ id has NO codex provider injected (no active subscription) it then
|
|
1434
2139
|
// resolves to nothing and getModel fails loud with
|
|
1435
2140
|
// CodexSubscriptionUnavailableError instead of mis-routing to Azure.
|
|
2141
|
+
const parsedRegistry = parseModelProvidersJson(settings.modelProvidersJson);
|
|
1436
2142
|
const registryOwnedIds = new Set(
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
2143
|
+
parsedRegistry.flatMap((provider) => provider.models.map((model) => model.id)),
|
|
2144
|
+
);
|
|
2145
|
+
const registryAliases = new Set(
|
|
2146
|
+
parsedRegistry.flatMap((provider) => provider.models.flatMap((model) => model.aliases ?? [])),
|
|
1440
2147
|
);
|
|
1441
2148
|
const isRegistryNamespaced = (id: string): boolean =>
|
|
1442
|
-
id.startsWith(CODEX_MODEL_ID_PREFIX) ||
|
|
2149
|
+
id.startsWith(CODEX_MODEL_ID_PREFIX) ||
|
|
2150
|
+
registryAliases.has(id) ||
|
|
2151
|
+
(id.includes("/") && registryOwnedIds.has(id));
|
|
2152
|
+
const builtinProvider = providerById.get(builtinId);
|
|
2153
|
+
if (!builtinProvider) {
|
|
2154
|
+
throw new Error(`Built-in model provider ${builtinId} is not configured`);
|
|
2155
|
+
}
|
|
1443
2156
|
const out: ConfiguredModel[] = uniqueValues([
|
|
1444
2157
|
settings.openaiModel,
|
|
1445
2158
|
...splitCsv(settings.openaiAllowedModels),
|
|
1446
2159
|
])
|
|
1447
2160
|
.filter((id) => !isRegistryNamespaced(id))
|
|
1448
|
-
.map((id) =>
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
2161
|
+
.map((id) => {
|
|
2162
|
+
const capabilities = legacyModelCapabilities(settings, {
|
|
2163
|
+
reasoningEffort: true,
|
|
2164
|
+
hostedWebSearch: settings.webSearchEnabled,
|
|
2165
|
+
});
|
|
2166
|
+
return finalizeConfiguredModel(settings, builtinProvider, {
|
|
2167
|
+
id,
|
|
2168
|
+
aliases: [],
|
|
2169
|
+
label: id,
|
|
2170
|
+
providerId: builtinId,
|
|
2171
|
+
providerLabel: builtinLabel,
|
|
2172
|
+
api: "responses" as const,
|
|
2173
|
+
upstreamModelId: id,
|
|
2174
|
+
deployment: { upstreamModelId: id, wireApi: "responses" },
|
|
2175
|
+
credentialSource: builtinProvider.credentialSource,
|
|
2176
|
+
billing: builtinProvider.billing,
|
|
2177
|
+
capabilities,
|
|
2178
|
+
...(pricingSchedules[id] === undefined ? {} : { pricing: pricingSchedules[id] }),
|
|
2179
|
+
contextWindowTokens: settings.contextWindowTokens,
|
|
2180
|
+
toolOutputTruncationTokens: settings.modelToolOutputTruncationTokens,
|
|
2181
|
+
reasoningEffort: capabilities.reasoning.runnable,
|
|
2182
|
+
hostedWebSearch: capabilities.hostedTools.webSearch.runnable,
|
|
2183
|
+
});
|
|
2184
|
+
});
|
|
2185
|
+
for (const provider of parsedRegistry) {
|
|
1460
2186
|
const providerLabel = provider.label ?? provider.id;
|
|
2187
|
+
const resolvedProvider = providerById.get(provider.id);
|
|
2188
|
+
if (!resolvedProvider) {
|
|
2189
|
+
throw new Error(`Registry model provider ${provider.id} is not configured`);
|
|
2190
|
+
}
|
|
1461
2191
|
for (const model of provider.models) {
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
:
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
:
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
:
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
2192
|
+
const capabilities = model.capabilities
|
|
2193
|
+
? normalizeCapabilities(model.capabilities)
|
|
2194
|
+
: legacyModelCapabilities(settings, {
|
|
2195
|
+
reasoningEffort: model.reasoningEffort ?? false,
|
|
2196
|
+
hostedWebSearch: model.hostedWebSearch ?? false,
|
|
2197
|
+
});
|
|
2198
|
+
const upstreamModelId = model.upstreamModelId ?? model.id;
|
|
2199
|
+
out.push(
|
|
2200
|
+
finalizeConfiguredModel(settings, resolvedProvider, {
|
|
2201
|
+
id: model.id,
|
|
2202
|
+
aliases: [...(model.aliases ?? [])],
|
|
2203
|
+
label: model.label ?? model.id,
|
|
2204
|
+
providerId: provider.id,
|
|
2205
|
+
providerLabel,
|
|
2206
|
+
api: provider.api,
|
|
2207
|
+
upstreamModelId,
|
|
2208
|
+
deployment: { upstreamModelId, wireApi: provider.api },
|
|
2209
|
+
credentialSource: resolvedProvider.credentialSource,
|
|
2210
|
+
billing: resolvedProvider.billing,
|
|
2211
|
+
capabilities,
|
|
2212
|
+
...(pricingSchedules[model.id] === undefined
|
|
2213
|
+
? {}
|
|
2214
|
+
: { pricing: pricingSchedules[model.id] }),
|
|
2215
|
+
...(model.contextWindowTokens === undefined
|
|
2216
|
+
? {}
|
|
2217
|
+
: { contextWindowTokens: model.contextWindowTokens }),
|
|
2218
|
+
...(model.effectiveContextWindowTokens === undefined
|
|
2219
|
+
? {}
|
|
2220
|
+
: { effectiveContextWindowTokens: model.effectiveContextWindowTokens }),
|
|
2221
|
+
...(model.autoCompactTokenLimit === undefined
|
|
2222
|
+
? {}
|
|
2223
|
+
: { autoCompactTokenLimit: model.autoCompactTokenLimit }),
|
|
2224
|
+
...(model.toolOutputTruncationTokens === undefined
|
|
2225
|
+
? {}
|
|
2226
|
+
: { toolOutputTruncationTokens: model.toolOutputTruncationTokens }),
|
|
2227
|
+
reasoningEffort: capabilities.reasoning.runnable,
|
|
2228
|
+
hostedWebSearch: capabilities.hostedTools.webSearch.runnable,
|
|
2229
|
+
}),
|
|
2230
|
+
);
|
|
1483
2231
|
}
|
|
1484
2232
|
}
|
|
1485
|
-
|
|
1486
|
-
return out
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
2233
|
+
assertUniqueModelIdentities(out);
|
|
2234
|
+
return out;
|
|
2235
|
+
}
|
|
2236
|
+
|
|
2237
|
+
/** Resolve a known canonical id or alias. Unknown strings are returned unchanged. */
|
|
2238
|
+
export function canonicalizeConfiguredModelId(settings: Settings, modelId: string): string {
|
|
2239
|
+
const models = configuredModels(settings);
|
|
2240
|
+
const canonical = models.find((model) => model.id === modelId);
|
|
2241
|
+
if (canonical) {
|
|
2242
|
+
return canonical.id;
|
|
2243
|
+
}
|
|
2244
|
+
return models.find((model) => model.aliases.includes(modelId))?.id ?? modelId;
|
|
1493
2245
|
}
|
|
1494
2246
|
|
|
1495
2247
|
/**
|
|
@@ -1513,7 +2265,8 @@ export function resolveModelProvider(
|
|
|
1513
2265
|
settings: Settings,
|
|
1514
2266
|
modelId: string,
|
|
1515
2267
|
): { provider: ResolvedModelProvider; model: ConfiguredModel } | undefined {
|
|
1516
|
-
const
|
|
2268
|
+
const canonicalModelId = canonicalizeConfiguredModelId(settings, modelId);
|
|
2269
|
+
const model = configuredModels(settings).find((candidate) => candidate.id === canonicalModelId);
|
|
1517
2270
|
if (!model) {
|
|
1518
2271
|
return undefined;
|
|
1519
2272
|
}
|
|
@@ -1526,28 +2279,169 @@ export function resolveModelProvider(
|
|
|
1526
2279
|
return { provider, model };
|
|
1527
2280
|
}
|
|
1528
2281
|
|
|
2282
|
+
export type ResolveTurnExecutionPolicyV1Input = {
|
|
2283
|
+
/** Effective persisted turn model. Aliases are accepted and canonicalized. */
|
|
2284
|
+
modelId: string;
|
|
2285
|
+
/** Exact caller-supplied input before canonicalization, only for explicit switches. */
|
|
2286
|
+
requestedModelId: string | null;
|
|
2287
|
+
modelSource: TurnExecutionModelSourceV1;
|
|
2288
|
+
reasoningEffort: Settings["openaiReasoningEffort"];
|
|
2289
|
+
reasoningSource: TurnExecutionReasoningSourceV1;
|
|
2290
|
+
};
|
|
2291
|
+
|
|
2292
|
+
function settingsForTurnExecutionPolicy(settings: Settings, modelId: string): Settings {
|
|
2293
|
+
return settings.codexSubscriptionEnabled && modelId.startsWith(CODEX_MODEL_ID_PREFIX)
|
|
2294
|
+
? withCodexCatalogProvider(settings)
|
|
2295
|
+
: settings;
|
|
2296
|
+
}
|
|
2297
|
+
|
|
1529
2298
|
/**
|
|
1530
|
-
*
|
|
1531
|
-
*
|
|
1532
|
-
*
|
|
2299
|
+
* Build a trusted, secret-safe execution policy from the normalized catalog.
|
|
2300
|
+
* The Codex overlay here contains static product/provider identity only; it
|
|
2301
|
+
* neither proves readiness nor chooses, decrypts, leases, or exposes an account.
|
|
1533
2302
|
*/
|
|
1534
|
-
export function
|
|
1535
|
-
|
|
2303
|
+
export function resolveTurnExecutionPolicyV1(
|
|
2304
|
+
settings: Settings,
|
|
2305
|
+
input: ResolveTurnExecutionPolicyV1Input,
|
|
2306
|
+
): TurnExecutionPolicyV1 {
|
|
2307
|
+
const catalogSettings = settingsForTurnExecutionPolicy(settings, input.modelId);
|
|
2308
|
+
const productModelId = canonicalizeConfiguredModelId(catalogSettings, input.modelId);
|
|
2309
|
+
const resolved = resolveModelProvider(catalogSettings, productModelId);
|
|
2310
|
+
if (!resolved) {
|
|
2311
|
+
throw new Error("Turn execution policy model is not present in the configured catalog");
|
|
2312
|
+
}
|
|
2313
|
+
if (
|
|
2314
|
+
input.requestedModelId !== null &&
|
|
2315
|
+
canonicalizeConfiguredModelId(catalogSettings, input.requestedModelId) !== productModelId
|
|
2316
|
+
) {
|
|
2317
|
+
throw new Error("Turn execution policy requested model does not canonicalize to its product");
|
|
2318
|
+
}
|
|
2319
|
+
return TurnExecutionPolicyV1.parse({
|
|
2320
|
+
schemaVersion: 1,
|
|
2321
|
+
productModelId,
|
|
2322
|
+
requestedModelId: input.requestedModelId,
|
|
2323
|
+
modelSource: input.modelSource,
|
|
2324
|
+
reasoningEffort: input.reasoningEffort,
|
|
2325
|
+
reasoningSource: input.reasoningSource,
|
|
2326
|
+
providerId: resolved.provider.id,
|
|
2327
|
+
upstreamModelId: resolved.model.upstreamModelId,
|
|
2328
|
+
wireApi: resolved.model.api,
|
|
2329
|
+
credentialSource: resolved.model.credentialSource,
|
|
2330
|
+
billing: resolved.model.billing,
|
|
2331
|
+
definitionVersion: resolved.model.definitionVersion,
|
|
2332
|
+
});
|
|
2333
|
+
}
|
|
2334
|
+
|
|
2335
|
+
/**
|
|
2336
|
+
* Parse-time validation lives in @opengeni/contracts; this verifier binds a
|
|
2337
|
+
* present snapshot to the current executable definition and exact turn row.
|
|
2338
|
+
* Any deployment/provider drift fails before a provider or compaction call.
|
|
2339
|
+
*/
|
|
2340
|
+
export function assertTurnExecutionPolicyMatchesConfigV1(
|
|
2341
|
+
settings: Settings,
|
|
2342
|
+
policy: TurnExecutionPolicyV1,
|
|
2343
|
+
expected: {
|
|
2344
|
+
modelId: string;
|
|
2345
|
+
reasoningEffort: Settings["openaiReasoningEffort"];
|
|
2346
|
+
},
|
|
2347
|
+
): {
|
|
2348
|
+
policy: TurnExecutionPolicyV1;
|
|
2349
|
+
provider: ResolvedModelProvider;
|
|
2350
|
+
model: ConfiguredModel;
|
|
2351
|
+
} {
|
|
2352
|
+
const parsed = TurnExecutionPolicyV1.parse(policy);
|
|
2353
|
+
const catalogSettings = settingsForTurnExecutionPolicy(settings, parsed.productModelId);
|
|
2354
|
+
const canonicalExpectedModel = canonicalizeConfiguredModelId(catalogSettings, expected.modelId);
|
|
2355
|
+
if (
|
|
2356
|
+
parsed.productModelId !== canonicalExpectedModel ||
|
|
2357
|
+
parsed.reasoningEffort !== expected.reasoningEffort
|
|
2358
|
+
) {
|
|
2359
|
+
throw new Error("Turn execution policy does not match the accepted turn model/reasoning");
|
|
2360
|
+
}
|
|
2361
|
+
if (
|
|
2362
|
+
parsed.requestedModelId !== null &&
|
|
2363
|
+
canonicalizeConfiguredModelId(catalogSettings, parsed.requestedModelId) !==
|
|
2364
|
+
parsed.productModelId
|
|
2365
|
+
) {
|
|
2366
|
+
throw new Error("Turn execution policy requested model does not match its product model");
|
|
2367
|
+
}
|
|
2368
|
+
const resolved = resolveModelProvider(catalogSettings, parsed.productModelId);
|
|
2369
|
+
if (!resolved) {
|
|
2370
|
+
throw new Error("Turn execution policy model is no longer configured");
|
|
2371
|
+
}
|
|
2372
|
+
const mismatched =
|
|
2373
|
+
parsed.providerId !== resolved.provider.id ||
|
|
2374
|
+
parsed.upstreamModelId !== resolved.model.upstreamModelId ||
|
|
2375
|
+
parsed.wireApi !== resolved.model.api ||
|
|
2376
|
+
parsed.definitionVersion !== resolved.model.definitionVersion ||
|
|
2377
|
+
canonicalJson(parsed.credentialSource) !== canonicalJson(resolved.model.credentialSource) ||
|
|
2378
|
+
canonicalJson(parsed.billing) !== canonicalJson(resolved.model.billing);
|
|
2379
|
+
if (mismatched) {
|
|
2380
|
+
throw new Error("Turn execution policy does not match the current provider definition");
|
|
2381
|
+
}
|
|
2382
|
+
return { policy: parsed, provider: resolved.provider, model: resolved.model };
|
|
2383
|
+
}
|
|
2384
|
+
|
|
2385
|
+
/**
|
|
2386
|
+
* Effective per-model pricing schedules. Merge order (later wins): built-in
|
|
2387
|
+
* flat defaults → registry model flat/scheduled pricing → explicit legacy flat
|
|
2388
|
+
* OPENGENI_MODEL_PRICING_JSON. The explicit legacy map intentionally replaces
|
|
2389
|
+
* a registry schedule with one flat default so its historical precedence stays
|
|
2390
|
+
* exact.
|
|
2391
|
+
*/
|
|
2392
|
+
export function configuredModelPricingSchedules(
|
|
2393
|
+
settings: Settings,
|
|
2394
|
+
): Record<string, ModelPricingScheduleV1> {
|
|
2395
|
+
const defaults = Object.fromEntries(
|
|
2396
|
+
Object.entries(defaultModelPricing).map(([model, pricing]) => [model, { default: pricing }]),
|
|
2397
|
+
);
|
|
2398
|
+
const registry: Record<string, ModelPricingScheduleV1> = {};
|
|
1536
2399
|
for (const provider of parseModelProvidersJson(settings.modelProvidersJson)) {
|
|
1537
2400
|
for (const model of provider.models) {
|
|
1538
2401
|
if (model.pricing) {
|
|
1539
|
-
registry[model.id] = model.pricing;
|
|
2402
|
+
registry[model.id] = normalizeModelPricingSchedule(model.pricing);
|
|
1540
2403
|
}
|
|
1541
2404
|
}
|
|
1542
2405
|
}
|
|
1543
|
-
const configured =
|
|
2406
|
+
const configured = Object.fromEntries(
|
|
2407
|
+
Object.entries(parseModelPricingJson(settings.modelPricingJson)).map(([model, pricing]) => [
|
|
2408
|
+
model,
|
|
2409
|
+
{ default: pricing },
|
|
2410
|
+
]),
|
|
2411
|
+
);
|
|
1544
2412
|
return {
|
|
1545
|
-
...
|
|
2413
|
+
...defaults,
|
|
1546
2414
|
...registry,
|
|
1547
2415
|
...configured,
|
|
1548
2416
|
};
|
|
1549
2417
|
}
|
|
1550
2418
|
|
|
2419
|
+
/** Legacy flat projection: returns the default/below-threshold price. */
|
|
2420
|
+
export function configuredModelPricing(settings: Settings): Record<string, ModelPricing> {
|
|
2421
|
+
return Object.fromEntries(
|
|
2422
|
+
Object.entries(configuredModelPricingSchedules(settings)).map(([model, schedule]) => [
|
|
2423
|
+
model,
|
|
2424
|
+
schedule.default,
|
|
2425
|
+
]),
|
|
2426
|
+
);
|
|
2427
|
+
}
|
|
2428
|
+
|
|
2429
|
+
/** Select the per-provider-request price at an exact input-token threshold. */
|
|
2430
|
+
export function selectModelPricing(
|
|
2431
|
+
schedule: ModelPricingScheduleV1,
|
|
2432
|
+
inputTokens: number,
|
|
2433
|
+
): ModelPricing {
|
|
2434
|
+
const normalizedInputTokens = Math.max(0, Math.floor(inputTokens));
|
|
2435
|
+
let selected = schedule.default;
|
|
2436
|
+
for (const tier of schedule.inputTokenTiers ?? []) {
|
|
2437
|
+
if (normalizedInputTokens < tier.minimumInputTokens) {
|
|
2438
|
+
break;
|
|
2439
|
+
}
|
|
2440
|
+
selected = tier.pricing;
|
|
2441
|
+
}
|
|
2442
|
+
return selected;
|
|
2443
|
+
}
|
|
2444
|
+
|
|
1551
2445
|
/**
|
|
1552
2446
|
* Usable input-token budget: an explicit model-catalog effective window when
|
|
1553
2447
|
* available, otherwise the deployment window minus its output reserve.
|
|
@@ -1627,17 +2521,28 @@ export function calculateModelUsageCostMicros(
|
|
|
1627
2521
|
model: string,
|
|
1628
2522
|
usage: ModelUsageInput,
|
|
1629
2523
|
): number {
|
|
1630
|
-
const
|
|
1631
|
-
if (!
|
|
2524
|
+
const schedule = configuredModelPricingSchedules(settings)[model];
|
|
2525
|
+
if (!schedule) {
|
|
1632
2526
|
throw new Error(`Missing model pricing for ${model}`);
|
|
1633
2527
|
}
|
|
1634
2528
|
const entries =
|
|
1635
2529
|
usage.requestUsageEntries && usage.requestUsageEntries.length > 0
|
|
1636
2530
|
? usage.requestUsageEntries
|
|
1637
2531
|
: [usage];
|
|
1638
|
-
const
|
|
1639
|
-
const
|
|
1640
|
-
|
|
2532
|
+
const rawCostByPricing = new Map<ModelPricing, number>();
|
|
2533
|
+
for (const entry of entries) {
|
|
2534
|
+
const pricing = selectModelPricing(schedule, positiveInt(entry.inputTokens));
|
|
2535
|
+
rawCostByPricing.set(
|
|
2536
|
+
pricing,
|
|
2537
|
+
(rawCostByPricing.get(pricing) ?? 0) + calculateEntryCostMicros(pricing, entry),
|
|
2538
|
+
);
|
|
2539
|
+
}
|
|
2540
|
+
let total = 0;
|
|
2541
|
+
for (const [pricing, rawCost] of rawCostByPricing) {
|
|
2542
|
+
const marginBps = pricing.marginBps ?? 0;
|
|
2543
|
+
total += Math.ceil((rawCost * (10_000 + marginBps)) / 10_000);
|
|
2544
|
+
}
|
|
2545
|
+
return total;
|
|
1641
2546
|
}
|
|
1642
2547
|
|
|
1643
2548
|
export function configuredAllowedReasoningEfforts(
|
|
@@ -2151,7 +3056,14 @@ export function parseModelProvidersJson(raw: string): RegistryProvider[] {
|
|
|
2151
3056
|
`OPENGENI_MODEL_PROVIDERS_JSON provider[${index}] is invalid: ${result.error.message}`,
|
|
2152
3057
|
);
|
|
2153
3058
|
}
|
|
2154
|
-
|
|
3059
|
+
try {
|
|
3060
|
+
return normalizeRegistryProvider(result.data);
|
|
3061
|
+
} catch (error) {
|
|
3062
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3063
|
+
throw new Error(`OPENGENI_MODEL_PROVIDERS_JSON provider[${index}] is invalid: ${message}`, {
|
|
3064
|
+
cause: error,
|
|
3065
|
+
});
|
|
3066
|
+
}
|
|
2155
3067
|
});
|
|
2156
3068
|
}
|
|
2157
3069
|
|
|
@@ -2720,6 +3632,10 @@ function validateSettings(settings: Settings): void {
|
|
|
2720
3632
|
);
|
|
2721
3633
|
}
|
|
2722
3634
|
}
|
|
3635
|
+
// Materialize the normalized catalog at boot so canonical product ids,
|
|
3636
|
+
// aliases, definition digests, and capability/pricing normalization are
|
|
3637
|
+
// validated even when managed billing is disabled.
|
|
3638
|
+
configuredModels(settings);
|
|
2723
3639
|
}
|
|
2724
3640
|
|
|
2725
3641
|
/**
|