@opengeni/config 0.6.2 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +379 -8
- package/dist/index.js +756 -65
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/index.ts +1016 -86
package/src/index.ts
CHANGED
|
@@ -3,13 +3,28 @@ import {
|
|
|
3
3
|
CAPABILITY_DESCRIPTORS,
|
|
4
4
|
Entitlements,
|
|
5
5
|
EntitlementsMode,
|
|
6
|
+
MAX_NESTED_AGENT_DEPTH,
|
|
6
7
|
ProductAccessMode,
|
|
7
8
|
ReasoningEffort,
|
|
8
9
|
SandboxBackend,
|
|
10
|
+
SessionMcpApprovalPolicy,
|
|
9
11
|
StaticUsageLimits,
|
|
12
|
+
TurnExecutionPolicyV1,
|
|
10
13
|
UsageLimitsMode,
|
|
14
|
+
type TurnExecutionModelSourceV1,
|
|
15
|
+
type TurnExecutionReasoningSourceV1,
|
|
11
16
|
} from "@opengeni/contracts";
|
|
12
|
-
import {
|
|
17
|
+
import { CODEX_MODEL_TOOL_OUTPUT_TRUNCATION_TOKENS } from "@opengeni/codex";
|
|
18
|
+
import {
|
|
19
|
+
CODEX_FALLBACK_MODEL_SLUGS,
|
|
20
|
+
CODEX_MODEL_AUTO_COMPACT_TOKEN_LIMIT,
|
|
21
|
+
CODEX_MODEL_CONTEXT_WINDOW_TOKENS,
|
|
22
|
+
CODEX_MODEL_EFFECTIVE_CONTEXT_WINDOW_TOKENS,
|
|
23
|
+
CODEX_MODEL_ID_PREFIX,
|
|
24
|
+
CODEX_PROVIDER_BASE_URL,
|
|
25
|
+
CODEX_PROVIDER_ID,
|
|
26
|
+
} from "@opengeni/codex/constants";
|
|
27
|
+
import { createHash } from "node:crypto";
|
|
13
28
|
import { z } from "zod";
|
|
14
29
|
|
|
15
30
|
const envName = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
@@ -218,6 +233,9 @@ const SettingsSchema = z.object({
|
|
|
218
233
|
// holder of stream:control gets 403 until this flips. Keeps stream:control a
|
|
219
234
|
// declared-but-inert permission so later hardening is a flag flip.
|
|
220
235
|
streamControlEnabled: EnvBoolean.default(false),
|
|
236
|
+
// Existing-session explicit tool replacement is gated until every API and
|
|
237
|
+
// worker instance understands durable tools_provided provenance.
|
|
238
|
+
sessionTurnToolReplacementEnabled: EnvBoolean.default(false),
|
|
221
239
|
toolspaceEnabled: EnvBoolean.default(false),
|
|
222
240
|
toolspaceMaxCallsPerTurn: z.coerce.number().int().positive().default(200),
|
|
223
241
|
// Optional release-coherent bootstrap hint for custom rigs/connected machines
|
|
@@ -232,6 +250,9 @@ const SettingsSchema = z.object({
|
|
|
232
250
|
integrationsStateSecret: z.string().optional(),
|
|
233
251
|
integrationsAllowPrivateNetworkTargets: EnvBoolean.default(false),
|
|
234
252
|
integrationsOauthClientsJson: z.string().default("{}"),
|
|
253
|
+
// Undefined is meaningful: the migration boundary persists the product
|
|
254
|
+
// default of 3 when no deployment override is supplied.
|
|
255
|
+
maxNestedAgentDepth: z.coerce.number().int().nonnegative().max(MAX_NESTED_AGENT_DEPTH).optional(),
|
|
235
256
|
// Session goal guard rails. Goals are designed for runs that legitimately
|
|
236
257
|
// span days, so length is bounded by pathology detection (no-progress
|
|
237
258
|
// streaks, budget exhaustion), never by count. goalMaxAutoContinuations is
|
|
@@ -316,6 +337,14 @@ const SettingsSchema = z.object({
|
|
|
316
337
|
// enable. Turning it off restores the legacy sticky selector without a schema
|
|
317
338
|
// rollback; the additive lease table/cursor columns become inert.
|
|
318
339
|
codexCredentialLeasingEnabled: EnvBoolean.default(false),
|
|
340
|
+
// Decision-observability fence. When enabled, the worker emits one
|
|
341
|
+
// bounded, metadata-only adaptive-policy replay record alongside the unchanged
|
|
342
|
+
// sticky-sharded decision. It never changes placement/admission/failover.
|
|
343
|
+
codexFleetPolicyShadowEnabled: EnvBoolean.default(false),
|
|
344
|
+
// Multi-account P3 (auto-rotation): an account is "near exhaustion" — ineligible to be
|
|
345
|
+
// rotated TO — when EITHER usage window (5h/weekly) is at/over this percent. Default 90 to
|
|
346
|
+
// match the UI danger flip (UsageBar danger at pct >= 90). OPENGENI_CODEX_ROTATION_NEAR_EXHAUSTION_PCT.
|
|
347
|
+
codexRotationNearExhaustionPct: z.coerce.number().int().min(1).max(100).default(90),
|
|
319
348
|
openaiReasoningEffort: ReasoningEffort.default("low"),
|
|
320
349
|
openaiAllowedReasoningEfforts: z.string().default("low,medium,high,xhigh"),
|
|
321
350
|
openaiResponsesTransport: z.enum(["http", "websocket"]).default("http"),
|
|
@@ -516,6 +545,15 @@ const SettingsSchema = z.object({
|
|
|
516
545
|
// EnvBoolean (NOT z.coerce.boolean(), which would coerce "false" -> true and
|
|
517
546
|
// turn the flag ON the moment anyone set the env var to disable it).
|
|
518
547
|
sandboxOwnershipEnabled: EnvBoolean.default(false),
|
|
548
|
+
// --- standalone rig-verifier ownership rollout flag, default OFF ---
|
|
549
|
+
// Rig verification creates a throwaway provider sandbox outside the normal
|
|
550
|
+
// session-turn path. When enabled, that sandbox must first acquire the same
|
|
551
|
+
// durable lease lifecycle used by session boxes so the global orphan sweep
|
|
552
|
+
// recognizes its exact provider instance. Keep this separate from the general
|
|
553
|
+
// sandboxOwnershipEnabled rollout: every reaper worker must understand verifier
|
|
554
|
+
// leases before dispatch is enabled. When false the verifier fails closed before
|
|
555
|
+
// provider create; it never falls back to the legacy unowned path.
|
|
556
|
+
rigVerificationLeaseOwnershipEnabled: EnvBoolean.default(false),
|
|
519
557
|
// --- lazy sandbox provisioning rollout flag, default OFF ---
|
|
520
558
|
// Only effective when sandboxOwnershipEnabled is ALSO on (lazy provisioning is a
|
|
521
559
|
// property of the owned path — the SDK never creates/resumes an injected session,
|
|
@@ -736,14 +774,8 @@ const SettingsSchema = z.object({
|
|
|
736
774
|
allowedTools: z.array(z.string().min(1)).optional(),
|
|
737
775
|
timeoutMs: z.number().int().positive().optional(),
|
|
738
776
|
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(),
|
|
777
|
+
/** Runtime approval policy, overlaid from an attempt-frozen session snapshot. */
|
|
778
|
+
requireApproval: SessionMcpApprovalPolicy.optional(),
|
|
747
779
|
/**
|
|
748
780
|
* Extra request headers sent to this MCP server (credential injection
|
|
749
781
|
* for workspace-enabled capability MCPs). Populated at runtime from
|
|
@@ -778,6 +810,15 @@ export type ModelPricing = {
|
|
|
778
810
|
outputMicrosPerMillionTokens: number;
|
|
779
811
|
marginBps?: number | undefined;
|
|
780
812
|
};
|
|
813
|
+
export type ModelPricingScheduleV1 = {
|
|
814
|
+
default: ModelPricing;
|
|
815
|
+
inputTokenTiers?:
|
|
816
|
+
| Array<{
|
|
817
|
+
minimumInputTokens: number;
|
|
818
|
+
pricing: ModelPricing;
|
|
819
|
+
}>
|
|
820
|
+
| undefined;
|
|
821
|
+
};
|
|
781
822
|
export type ModelUsageInput = {
|
|
782
823
|
inputTokens?: number | undefined;
|
|
783
824
|
outputTokens?: number | undefined;
|
|
@@ -796,6 +837,164 @@ const ModelPricingSchema = z.object({
|
|
|
796
837
|
marginBps: z.number().int().min(0).max(100_000).optional(),
|
|
797
838
|
});
|
|
798
839
|
|
|
840
|
+
const ModelPricingScheduleSchema = z
|
|
841
|
+
.object({
|
|
842
|
+
default: ModelPricingSchema,
|
|
843
|
+
inputTokenTiers: z
|
|
844
|
+
.array(
|
|
845
|
+
z.object({
|
|
846
|
+
minimumInputTokens: z.number().int().nonnegative(),
|
|
847
|
+
pricing: ModelPricingSchema,
|
|
848
|
+
}),
|
|
849
|
+
)
|
|
850
|
+
.optional(),
|
|
851
|
+
})
|
|
852
|
+
.superRefine((schedule, ctx) => {
|
|
853
|
+
let previous = -1;
|
|
854
|
+
for (const [index, tier] of (schedule.inputTokenTiers ?? []).entries()) {
|
|
855
|
+
if (tier.minimumInputTokens <= previous) {
|
|
856
|
+
ctx.addIssue({
|
|
857
|
+
code: "custom",
|
|
858
|
+
path: ["inputTokenTiers", index, "minimumInputTokens"],
|
|
859
|
+
message: "input-token tier thresholds must be strictly increasing",
|
|
860
|
+
});
|
|
861
|
+
}
|
|
862
|
+
previous = tier.minimumInputTokens;
|
|
863
|
+
}
|
|
864
|
+
});
|
|
865
|
+
|
|
866
|
+
export const CapabilitySupportV1 = z.enum(["supported", "unsupported", "unknown"]);
|
|
867
|
+
export type CapabilitySupportV1 = z.infer<typeof CapabilitySupportV1>;
|
|
868
|
+
|
|
869
|
+
export const CapabilityStateV1Schema = z
|
|
870
|
+
.object({
|
|
871
|
+
upstream: CapabilitySupportV1,
|
|
872
|
+
runnable: z.boolean(),
|
|
873
|
+
})
|
|
874
|
+
.superRefine((state, ctx) => {
|
|
875
|
+
if (state.upstream === "unsupported" && state.runnable) {
|
|
876
|
+
ctx.addIssue({
|
|
877
|
+
code: "custom",
|
|
878
|
+
path: ["runnable"],
|
|
879
|
+
message: "an upstream-unsupported capability cannot be runnable",
|
|
880
|
+
});
|
|
881
|
+
}
|
|
882
|
+
});
|
|
883
|
+
export type CapabilityStateV1 = z.infer<typeof CapabilityStateV1Schema>;
|
|
884
|
+
|
|
885
|
+
const ModelModalityV1 = z.enum(["text", "image", "audio"]);
|
|
886
|
+
const ModelLatencyModeV1 = z.enum(["standard", "priority", "fast"]);
|
|
887
|
+
|
|
888
|
+
export const ModelCapabilitiesV1Schema = z
|
|
889
|
+
.object({
|
|
890
|
+
reasoning: CapabilityStateV1Schema.extend({
|
|
891
|
+
efforts: z.array(ReasoningEffort),
|
|
892
|
+
defaultEffort: ReasoningEffort.nullable(),
|
|
893
|
+
required: z.boolean(),
|
|
894
|
+
}),
|
|
895
|
+
functionCalling: CapabilityStateV1Schema,
|
|
896
|
+
structuredOutput: CapabilityStateV1Schema,
|
|
897
|
+
hostedTools: z.object({
|
|
898
|
+
webSearch: CapabilityStateV1Schema,
|
|
899
|
+
xSearch: CapabilityStateV1Schema,
|
|
900
|
+
codeExecution: CapabilityStateV1Schema,
|
|
901
|
+
}),
|
|
902
|
+
inputModalities: z.array(ModelModalityV1).min(1),
|
|
903
|
+
outputModalities: z.array(ModelModalityV1).min(1),
|
|
904
|
+
transports: z.object({
|
|
905
|
+
sse: CapabilityStateV1Schema,
|
|
906
|
+
responsesWebSocket: CapabilityStateV1Schema,
|
|
907
|
+
realtimeAudio: CapabilityStateV1Schema,
|
|
908
|
+
}),
|
|
909
|
+
latencyModes: z
|
|
910
|
+
.array(
|
|
911
|
+
z.object({
|
|
912
|
+
id: ModelLatencyModeV1,
|
|
913
|
+
upstream: CapabilitySupportV1,
|
|
914
|
+
runnable: z.boolean(),
|
|
915
|
+
billingMultiplierBps: z.number().int().positive().optional(),
|
|
916
|
+
}),
|
|
917
|
+
)
|
|
918
|
+
.min(1),
|
|
919
|
+
})
|
|
920
|
+
.superRefine((capabilities, ctx) => {
|
|
921
|
+
const efforts = new Set(capabilities.reasoning.efforts);
|
|
922
|
+
if (efforts.size !== capabilities.reasoning.efforts.length) {
|
|
923
|
+
ctx.addIssue({
|
|
924
|
+
code: "custom",
|
|
925
|
+
path: ["reasoning", "efforts"],
|
|
926
|
+
message: "reasoning efforts must be unique",
|
|
927
|
+
});
|
|
928
|
+
}
|
|
929
|
+
if (
|
|
930
|
+
capabilities.reasoning.defaultEffort !== null &&
|
|
931
|
+
!efforts.has(capabilities.reasoning.defaultEffort)
|
|
932
|
+
) {
|
|
933
|
+
ctx.addIssue({
|
|
934
|
+
code: "custom",
|
|
935
|
+
path: ["reasoning", "defaultEffort"],
|
|
936
|
+
message: "the default reasoning effort must be one of the supported efforts",
|
|
937
|
+
});
|
|
938
|
+
}
|
|
939
|
+
if (capabilities.reasoning.runnable && capabilities.reasoning.efforts.length === 0) {
|
|
940
|
+
ctx.addIssue({
|
|
941
|
+
code: "custom",
|
|
942
|
+
path: ["reasoning", "efforts"],
|
|
943
|
+
message: "a runnable reasoning capability must declare at least one effort",
|
|
944
|
+
});
|
|
945
|
+
}
|
|
946
|
+
for (const field of ["inputModalities", "outputModalities"] as const) {
|
|
947
|
+
if (new Set(capabilities[field]).size !== capabilities[field].length) {
|
|
948
|
+
ctx.addIssue({
|
|
949
|
+
code: "custom",
|
|
950
|
+
path: [field],
|
|
951
|
+
message: `${field} must be unique`,
|
|
952
|
+
});
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
const latencyIds = new Set<string>();
|
|
956
|
+
for (const [index, mode] of capabilities.latencyModes.entries()) {
|
|
957
|
+
if (latencyIds.has(mode.id)) {
|
|
958
|
+
ctx.addIssue({
|
|
959
|
+
code: "custom",
|
|
960
|
+
path: ["latencyModes", index, "id"],
|
|
961
|
+
message: "latency mode ids must be unique",
|
|
962
|
+
});
|
|
963
|
+
}
|
|
964
|
+
latencyIds.add(mode.id);
|
|
965
|
+
if (mode.upstream === "unsupported" && mode.runnable) {
|
|
966
|
+
ctx.addIssue({
|
|
967
|
+
code: "custom",
|
|
968
|
+
path: ["latencyModes", index, "runnable"],
|
|
969
|
+
message: "an upstream-unsupported latency mode cannot be runnable",
|
|
970
|
+
});
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
});
|
|
974
|
+
export type ModelCapabilitiesV1 = z.infer<typeof ModelCapabilitiesV1Schema>;
|
|
975
|
+
|
|
976
|
+
export type ModelDeploymentV1 = {
|
|
977
|
+
upstreamModelId: string;
|
|
978
|
+
wireApi: ModelProviderApi;
|
|
979
|
+
};
|
|
980
|
+
|
|
981
|
+
export type ModelExecutionLimitsV1 = {
|
|
982
|
+
contextWindowTokens: number | null;
|
|
983
|
+
effectiveContextWindowTokens: number | null;
|
|
984
|
+
autoCompactTokenLimit: number | null;
|
|
985
|
+
toolOutputTruncationTokens: number | null;
|
|
986
|
+
};
|
|
987
|
+
|
|
988
|
+
export type CredentialSourceV1 =
|
|
989
|
+
| { kind: "deployment"; mechanism: "api_key" | "azure_ad_bearer" }
|
|
990
|
+
| { kind: "connected_subscription"; provider: "codex" }
|
|
991
|
+
| { kind: "workspace_connection"; mechanism: "api_key" };
|
|
992
|
+
|
|
993
|
+
export type BillingAttributionV1 = {
|
|
994
|
+
upstreamPayer: "deployment" | "workspace" | "connected_subscription";
|
|
995
|
+
metering: "opengeni_credits" | "external";
|
|
996
|
+
};
|
|
997
|
+
|
|
799
998
|
/**
|
|
800
999
|
* Wire API a provider speaks. The built-in OpenAI/Azure provider always uses
|
|
801
1000
|
* "responses" (the OpenAI Responses API). Extra registry providers default to
|
|
@@ -815,19 +1014,52 @@ export const RegistryProviderKind = z.enum(["api-key", "codex-subscription"]);
|
|
|
815
1014
|
export type RegistryProviderKind = z.infer<typeof RegistryProviderKind>;
|
|
816
1015
|
|
|
817
1016
|
/** A single model exposed by a registry provider. */
|
|
818
|
-
const RegistryModelSchema = z
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
1017
|
+
const RegistryModelSchema = z
|
|
1018
|
+
.object({
|
|
1019
|
+
id: z.string().min(1), // canonical OpenGeni product id
|
|
1020
|
+
upstreamModelId: z.string().min(1).optional(), // exact provider slug; defaults to id
|
|
1021
|
+
aliases: z.array(z.string().min(1)).optional(), // accepted input only; never sent upstream
|
|
1022
|
+
label: z.string().min(1).optional(), // display name; defaults to id
|
|
1023
|
+
contextWindowTokens: z.number().int().positive().optional(),
|
|
1024
|
+
effectiveContextWindowTokens: z.number().int().positive().optional(),
|
|
1025
|
+
autoCompactTokenLimit: z.number().int().positive().optional(),
|
|
1026
|
+
// Canonical model-facing function/tool-result policy. The runtime applies
|
|
1027
|
+
// the same 1.2x serialization allowance as Codex when materializing output.
|
|
1028
|
+
toolOutputTruncationTokens: z.number().int().positive().optional(),
|
|
1029
|
+
reasoningEffort: z.boolean().optional(), // legacy compatibility input/projection
|
|
1030
|
+
hostedWebSearch: z.boolean().optional(), // legacy compatibility input/projection
|
|
1031
|
+
capabilities: ModelCapabilitiesV1Schema.optional(),
|
|
1032
|
+
pricing: z.union([ModelPricingSchema, ModelPricingScheduleSchema]).optional(),
|
|
1033
|
+
// Reserved normalized contracts are derived by OpenGeni in V1. Generic
|
|
1034
|
+
// registry JSON must not opt itself into workspace BYOK or reattribute cost.
|
|
1035
|
+
credentialSource: z.never().optional(),
|
|
1036
|
+
billing: z.never().optional(),
|
|
1037
|
+
})
|
|
1038
|
+
.superRefine((model, ctx) => {
|
|
1039
|
+
if (
|
|
1040
|
+
model.capabilities &&
|
|
1041
|
+
model.reasoningEffort !== undefined &&
|
|
1042
|
+
model.reasoningEffort !== model.capabilities.reasoning.runnable
|
|
1043
|
+
) {
|
|
1044
|
+
ctx.addIssue({
|
|
1045
|
+
code: "custom",
|
|
1046
|
+
path: ["reasoningEffort"],
|
|
1047
|
+
message: "legacy reasoningEffort must agree with capabilities.reasoning.runnable",
|
|
1048
|
+
});
|
|
1049
|
+
}
|
|
1050
|
+
if (
|
|
1051
|
+
model.capabilities &&
|
|
1052
|
+
model.hostedWebSearch !== undefined &&
|
|
1053
|
+
model.hostedWebSearch !== model.capabilities.hostedTools.webSearch.runnable
|
|
1054
|
+
) {
|
|
1055
|
+
ctx.addIssue({
|
|
1056
|
+
code: "custom",
|
|
1057
|
+
path: ["hostedWebSearch"],
|
|
1058
|
+
message:
|
|
1059
|
+
"legacy hostedWebSearch must agree with capabilities.hostedTools.webSearch.runnable",
|
|
1060
|
+
});
|
|
1061
|
+
}
|
|
1062
|
+
});
|
|
831
1063
|
|
|
832
1064
|
/** A non-built-in provider declared by the host via OPENGENI_MODEL_PROVIDERS_JSON. */
|
|
833
1065
|
const RegistryProviderSchema = z.object({
|
|
@@ -840,6 +1072,12 @@ const RegistryProviderSchema = z.object({
|
|
|
840
1072
|
apiKeyEnv: z.string().optional(), // ... OR name of the env var holding the key (preferred)
|
|
841
1073
|
defaultQuery: z.record(z.string(), z.string()).optional(),
|
|
842
1074
|
defaultHeaders: z.record(z.string(), z.string()).optional(),
|
|
1075
|
+
publicDefaultQueryNames: z.array(z.string().min(1)).optional(),
|
|
1076
|
+
publicDefaultHeaderNames: z.array(z.string().min(1)).optional(),
|
|
1077
|
+
// V1 derives these from provider kind. Workspace BYOK is deliberately not a
|
|
1078
|
+
// registry switch and requires a separately reviewed encrypted broker.
|
|
1079
|
+
credentialSource: z.never().optional(),
|
|
1080
|
+
billing: z.never().optional(),
|
|
843
1081
|
models: z.array(RegistryModelSchema).min(1),
|
|
844
1082
|
});
|
|
845
1083
|
export type RegistryProvider = z.infer<typeof RegistryProviderSchema>;
|
|
@@ -870,15 +1108,29 @@ export interface ResolvedModelProvider {
|
|
|
870
1108
|
apiKey?: string | undefined;
|
|
871
1109
|
defaultQuery?: Record<string, string> | undefined;
|
|
872
1110
|
defaultHeaders?: Record<string, string> | undefined;
|
|
1111
|
+
publicDefaultQueryNames?: string[] | undefined;
|
|
1112
|
+
publicDefaultHeaderNames?: string[] | undefined;
|
|
1113
|
+
credentialSource: CredentialSourceV1;
|
|
1114
|
+
billing: BillingAttributionV1;
|
|
873
1115
|
}
|
|
874
1116
|
|
|
875
1117
|
/** A single exposed model + the provider that serves it. */
|
|
876
1118
|
export interface ConfiguredModel {
|
|
1119
|
+
schemaVersion: 1;
|
|
877
1120
|
id: string;
|
|
1121
|
+
aliases: string[];
|
|
878
1122
|
label: string;
|
|
879
1123
|
providerId: string;
|
|
880
1124
|
providerLabel: string;
|
|
881
1125
|
api: ModelProviderApi;
|
|
1126
|
+
upstreamModelId: string;
|
|
1127
|
+
deployment: ModelDeploymentV1;
|
|
1128
|
+
executionLimits: ModelExecutionLimitsV1;
|
|
1129
|
+
credentialSource: CredentialSourceV1;
|
|
1130
|
+
billing: BillingAttributionV1;
|
|
1131
|
+
capabilities: ModelCapabilitiesV1;
|
|
1132
|
+
pricing?: ModelPricingScheduleV1 | undefined;
|
|
1133
|
+
definitionVersion: string;
|
|
882
1134
|
contextWindowTokens?: number | undefined;
|
|
883
1135
|
effectiveContextWindowTokens?: number | undefined;
|
|
884
1136
|
autoCompactTokenLimit?: number | undefined;
|
|
@@ -1076,6 +1328,7 @@ export function getSettings(): Settings {
|
|
|
1076
1328
|
delegationSecret: optional("OPENGENI_DELEGATION_SECRET"),
|
|
1077
1329
|
streamTokenSecret: optional("OPENGENI_STREAM_TOKEN_SECRET"),
|
|
1078
1330
|
streamControlEnabled: optional("OPENGENI_STREAM_CONTROL_ENABLED"),
|
|
1331
|
+
sessionTurnToolReplacementEnabled: optional("OPENGENI_SESSION_TURN_TOOL_REPLACEMENT_ENABLED"),
|
|
1079
1332
|
toolspaceEnabled: optional("OPENGENI_TOOLSPACE_ENABLED"),
|
|
1080
1333
|
toolspaceMaxCallsPerTurn: optional("OPENGENI_TOOLSPACE_MAX_CALLS_PER_TURN"),
|
|
1081
1334
|
ogtoolPackageSpec: optional("OPENGENI_OGTOOL_PACKAGE_SPEC"),
|
|
@@ -1086,6 +1339,7 @@ export function getSettings(): Settings {
|
|
|
1086
1339
|
"OPENGENI_INTEGRATIONS_ALLOW_PRIVATE_NETWORK_TARGETS",
|
|
1087
1340
|
),
|
|
1088
1341
|
integrationsOauthClientsJson: optional("OPENGENI_INTEGRATIONS_OAUTH_CLIENTS_JSON"),
|
|
1342
|
+
maxNestedAgentDepth: optional("OPENGENI_MAX_NESTED_AGENT_DEPTH"),
|
|
1089
1343
|
goalMaxAutoContinuations: optional("OPENGENI_GOAL_MAX_AUTO_CONTINUATIONS"),
|
|
1090
1344
|
goalNoProgressLimit: optional("OPENGENI_GOAL_NO_PROGRESS_LIMIT"),
|
|
1091
1345
|
agentMaxModelCallsPerTurn: optional("OPENGENI_AGENT_MAX_MODEL_CALLS_PER_TURN"),
|
|
@@ -1114,6 +1368,7 @@ export function getSettings(): Settings {
|
|
|
1114
1368
|
codexSubscriptionEnabled: optional("OPENGENI_CODEX_SUBSCRIPTION_ENABLED"),
|
|
1115
1369
|
codexToolSearchEnabled: optional("OPENGENI_CODEX_TOOL_SEARCH_ENABLED"),
|
|
1116
1370
|
codexCredentialLeasingEnabled: optional("OPENGENI_CODEX_CREDENTIAL_LEASING_ENABLED"),
|
|
1371
|
+
codexFleetPolicyShadowEnabled: optional("OPENGENI_CODEX_FLEET_POLICY_SHADOW_ENABLED"),
|
|
1117
1372
|
codexProductSku: optional("OPENGENI_CODEX_PRODUCT_SKU"),
|
|
1118
1373
|
openaiReasoningEffort: optional("OPENGENI_OPENAI_REASONING_EFFORT"),
|
|
1119
1374
|
openaiAllowedReasoningEfforts: optional("OPENGENI_OPENAI_ALLOWED_REASONING_EFFORTS"),
|
|
@@ -1192,6 +1447,9 @@ export function getSettings(): Settings {
|
|
|
1192
1447
|
vercelTeamId: optional("OPENGENI_VERCEL_TEAM_ID"),
|
|
1193
1448
|
vercelRuntime: optional("OPENGENI_VERCEL_RUNTIME"),
|
|
1194
1449
|
sandboxOwnershipEnabled: optional("OPENGENI_SANDBOX_OWNERSHIP_ENABLED"),
|
|
1450
|
+
rigVerificationLeaseOwnershipEnabled: optional(
|
|
1451
|
+
"OPENGENI_RIG_VERIFICATION_LEASE_OWNERSHIP_ENABLED",
|
|
1452
|
+
),
|
|
1195
1453
|
sandboxLazyProvisionEnabled: optional("OPENGENI_SANDBOX_LAZY_PROVISION"),
|
|
1196
1454
|
sandboxSelfhostedEnabled: optional("OPENGENI_SANDBOX_SELFHOSTED_ENABLED"),
|
|
1197
1455
|
agentOpStreamEnabled: optional("OPENGENI_AGENT_OP_STREAM_ENABLED"),
|
|
@@ -1331,6 +1589,353 @@ export function resolveProviderApiKey(
|
|
|
1331
1589
|
return undefined;
|
|
1332
1590
|
}
|
|
1333
1591
|
|
|
1592
|
+
const HTTP_FIELD_NAME = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
|
|
1593
|
+
const CREDENTIAL_LIKE_NAME_PARTS = new Set([
|
|
1594
|
+
"apikey",
|
|
1595
|
+
"auth",
|
|
1596
|
+
"authorization",
|
|
1597
|
+
"bearer",
|
|
1598
|
+
"credential",
|
|
1599
|
+
"cookie",
|
|
1600
|
+
"key",
|
|
1601
|
+
"password",
|
|
1602
|
+
"secret",
|
|
1603
|
+
"session",
|
|
1604
|
+
"signature",
|
|
1605
|
+
"token",
|
|
1606
|
+
]);
|
|
1607
|
+
const REASONING_EFFORT_ORDER = new Map(
|
|
1608
|
+
ReasoningEffort.options.map((effort, index) => [effort, index]),
|
|
1609
|
+
);
|
|
1610
|
+
const MODALITY_ORDER = new Map(["text", "image", "audio"].map((value, index) => [value, index]));
|
|
1611
|
+
const LATENCY_MODE_ORDER = new Map(
|
|
1612
|
+
["standard", "priority", "fast"].map((value, index) => [value, index]),
|
|
1613
|
+
);
|
|
1614
|
+
|
|
1615
|
+
function normalizeRegistryBaseUrl(value: string, providerId: string): string {
|
|
1616
|
+
const url = new URL(value);
|
|
1617
|
+
if (url.username || url.password) {
|
|
1618
|
+
throw new Error(`provider ${providerId} baseUrl must not contain userinfo`);
|
|
1619
|
+
}
|
|
1620
|
+
if (url.search) {
|
|
1621
|
+
throw new Error(
|
|
1622
|
+
`provider ${providerId} baseUrl must not contain a query; move query entries to defaultQuery`,
|
|
1623
|
+
);
|
|
1624
|
+
}
|
|
1625
|
+
if (url.hash) {
|
|
1626
|
+
throw new Error(`provider ${providerId} baseUrl must not contain a fragment`);
|
|
1627
|
+
}
|
|
1628
|
+
return url.toString();
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
function isCredentialLikeMetadataName(name: string): boolean {
|
|
1632
|
+
return name
|
|
1633
|
+
.toLowerCase()
|
|
1634
|
+
.split(/[-_.]/u)
|
|
1635
|
+
.some((part) => CREDENTIAL_LIKE_NAME_PARTS.has(part));
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
function normalizeHeaderMap(
|
|
1639
|
+
providerId: string,
|
|
1640
|
+
headers: Record<string, string> | undefined,
|
|
1641
|
+
): Record<string, string> | undefined {
|
|
1642
|
+
if (!headers) {
|
|
1643
|
+
return undefined;
|
|
1644
|
+
}
|
|
1645
|
+
const normalized: Record<string, string> = {};
|
|
1646
|
+
const rawByNormalized = new Map<string, string>();
|
|
1647
|
+
for (const [rawName, value] of Object.entries(headers)) {
|
|
1648
|
+
if (!HTTP_FIELD_NAME.test(rawName)) {
|
|
1649
|
+
throw new Error(
|
|
1650
|
+
`provider ${providerId} defaultHeaders contains invalid HTTP field name ${JSON.stringify(rawName)}`,
|
|
1651
|
+
);
|
|
1652
|
+
}
|
|
1653
|
+
const name = rawName.toLowerCase();
|
|
1654
|
+
const previous = rawByNormalized.get(name);
|
|
1655
|
+
if (previous !== undefined) {
|
|
1656
|
+
throw new Error(
|
|
1657
|
+
`provider ${providerId} defaultHeaders names ${JSON.stringify(previous)} and ${JSON.stringify(rawName)} collide after lowercase normalization`,
|
|
1658
|
+
);
|
|
1659
|
+
}
|
|
1660
|
+
if (name === "authorization") {
|
|
1661
|
+
throw new Error(
|
|
1662
|
+
`provider ${providerId} defaultHeaders must not override SDK-managed Authorization`,
|
|
1663
|
+
);
|
|
1664
|
+
}
|
|
1665
|
+
rawByNormalized.set(name, rawName);
|
|
1666
|
+
normalized[name] = value;
|
|
1667
|
+
}
|
|
1668
|
+
return normalized;
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1671
|
+
function normalizePublicHeaderNames(
|
|
1672
|
+
providerId: string,
|
|
1673
|
+
names: string[] | undefined,
|
|
1674
|
+
headers: Record<string, string> | undefined,
|
|
1675
|
+
): string[] | undefined {
|
|
1676
|
+
if (!names) {
|
|
1677
|
+
return undefined;
|
|
1678
|
+
}
|
|
1679
|
+
const normalized: string[] = [];
|
|
1680
|
+
const seen = new Set<string>();
|
|
1681
|
+
for (const rawName of names) {
|
|
1682
|
+
if (!HTTP_FIELD_NAME.test(rawName)) {
|
|
1683
|
+
throw new Error(
|
|
1684
|
+
`provider ${providerId} publicDefaultHeaderNames contains invalid HTTP field name ${JSON.stringify(rawName)}`,
|
|
1685
|
+
);
|
|
1686
|
+
}
|
|
1687
|
+
const name = rawName.toLowerCase();
|
|
1688
|
+
if (seen.has(name)) {
|
|
1689
|
+
throw new Error(
|
|
1690
|
+
`provider ${providerId} publicDefaultHeaderNames contains duplicate normalized name ${JSON.stringify(name)}`,
|
|
1691
|
+
);
|
|
1692
|
+
}
|
|
1693
|
+
if (!(name in (headers ?? {}))) {
|
|
1694
|
+
throw new Error(
|
|
1695
|
+
`provider ${providerId} publicDefaultHeaderNames declares absent defaultHeaders entry ${JSON.stringify(name)}`,
|
|
1696
|
+
);
|
|
1697
|
+
}
|
|
1698
|
+
if (isCredentialLikeMetadataName(name)) {
|
|
1699
|
+
throw new Error(
|
|
1700
|
+
`provider ${providerId} publicDefaultHeaderNames cannot classify credential-like name ${JSON.stringify(name)} as public`,
|
|
1701
|
+
);
|
|
1702
|
+
}
|
|
1703
|
+
seen.add(name);
|
|
1704
|
+
normalized.push(name);
|
|
1705
|
+
}
|
|
1706
|
+
return normalized;
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1709
|
+
function normalizeQueryMap(
|
|
1710
|
+
providerId: string,
|
|
1711
|
+
query: Record<string, string> | undefined,
|
|
1712
|
+
): Record<string, string> | undefined {
|
|
1713
|
+
if (!query) {
|
|
1714
|
+
return undefined;
|
|
1715
|
+
}
|
|
1716
|
+
for (const name of Object.keys(query)) {
|
|
1717
|
+
if (!name) {
|
|
1718
|
+
throw new Error(`provider ${providerId} defaultQuery contains an empty name`);
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1721
|
+
return { ...query };
|
|
1722
|
+
}
|
|
1723
|
+
|
|
1724
|
+
function normalizePublicQueryNames(
|
|
1725
|
+
providerId: string,
|
|
1726
|
+
names: string[] | undefined,
|
|
1727
|
+
query: Record<string, string> | undefined,
|
|
1728
|
+
): string[] | undefined {
|
|
1729
|
+
if (!names) {
|
|
1730
|
+
return undefined;
|
|
1731
|
+
}
|
|
1732
|
+
const seen = new Set<string>();
|
|
1733
|
+
for (const name of names) {
|
|
1734
|
+
if (seen.has(name)) {
|
|
1735
|
+
throw new Error(
|
|
1736
|
+
`provider ${providerId} publicDefaultQueryNames contains duplicate name ${JSON.stringify(name)}`,
|
|
1737
|
+
);
|
|
1738
|
+
}
|
|
1739
|
+
if (!(name in (query ?? {}))) {
|
|
1740
|
+
throw new Error(
|
|
1741
|
+
`provider ${providerId} publicDefaultQueryNames declares absent defaultQuery entry ${JSON.stringify(name)}`,
|
|
1742
|
+
);
|
|
1743
|
+
}
|
|
1744
|
+
if (isCredentialLikeMetadataName(name)) {
|
|
1745
|
+
throw new Error(
|
|
1746
|
+
`provider ${providerId} publicDefaultQueryNames cannot classify credential-like name ${JSON.stringify(name)} as public`,
|
|
1747
|
+
);
|
|
1748
|
+
}
|
|
1749
|
+
seen.add(name);
|
|
1750
|
+
}
|
|
1751
|
+
return [...names];
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1754
|
+
function normalizeRegistryProvider(provider: RegistryProvider): RegistryProvider {
|
|
1755
|
+
const defaultHeaders = normalizeHeaderMap(provider.id, provider.defaultHeaders);
|
|
1756
|
+
const defaultQuery = normalizeQueryMap(provider.id, provider.defaultQuery);
|
|
1757
|
+
return {
|
|
1758
|
+
...provider,
|
|
1759
|
+
baseUrl: normalizeRegistryBaseUrl(provider.baseUrl, provider.id),
|
|
1760
|
+
...(defaultHeaders === undefined ? {} : { defaultHeaders }),
|
|
1761
|
+
...(defaultQuery === undefined ? {} : { defaultQuery }),
|
|
1762
|
+
...(provider.publicDefaultHeaderNames === undefined
|
|
1763
|
+
? {}
|
|
1764
|
+
: {
|
|
1765
|
+
publicDefaultHeaderNames: normalizePublicHeaderNames(
|
|
1766
|
+
provider.id,
|
|
1767
|
+
provider.publicDefaultHeaderNames,
|
|
1768
|
+
defaultHeaders,
|
|
1769
|
+
),
|
|
1770
|
+
}),
|
|
1771
|
+
...(provider.publicDefaultQueryNames === undefined
|
|
1772
|
+
? {}
|
|
1773
|
+
: {
|
|
1774
|
+
publicDefaultQueryNames: normalizePublicQueryNames(
|
|
1775
|
+
provider.id,
|
|
1776
|
+
provider.publicDefaultQueryNames,
|
|
1777
|
+
defaultQuery,
|
|
1778
|
+
),
|
|
1779
|
+
}),
|
|
1780
|
+
};
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1783
|
+
function normalizeModelPricingSchedule(
|
|
1784
|
+
pricing: ModelPricing | ModelPricingScheduleV1,
|
|
1785
|
+
): ModelPricingScheduleV1 {
|
|
1786
|
+
return "default" in pricing ? pricing : { default: pricing };
|
|
1787
|
+
}
|
|
1788
|
+
|
|
1789
|
+
function normalizeCapabilities(capabilities: ModelCapabilitiesV1): ModelCapabilitiesV1 {
|
|
1790
|
+
const parsed = ModelCapabilitiesV1Schema.parse(capabilities);
|
|
1791
|
+
return {
|
|
1792
|
+
...parsed,
|
|
1793
|
+
reasoning: {
|
|
1794
|
+
...parsed.reasoning,
|
|
1795
|
+
efforts: [...parsed.reasoning.efforts].sort(
|
|
1796
|
+
(left, right) =>
|
|
1797
|
+
(REASONING_EFFORT_ORDER.get(left) ?? 0) - (REASONING_EFFORT_ORDER.get(right) ?? 0),
|
|
1798
|
+
),
|
|
1799
|
+
},
|
|
1800
|
+
inputModalities: [...parsed.inputModalities].sort(
|
|
1801
|
+
(left, right) => (MODALITY_ORDER.get(left) ?? 0) - (MODALITY_ORDER.get(right) ?? 0),
|
|
1802
|
+
),
|
|
1803
|
+
outputModalities: [...parsed.outputModalities].sort(
|
|
1804
|
+
(left, right) => (MODALITY_ORDER.get(left) ?? 0) - (MODALITY_ORDER.get(right) ?? 0),
|
|
1805
|
+
),
|
|
1806
|
+
latencyModes: [...parsed.latencyModes].sort(
|
|
1807
|
+
(left, right) =>
|
|
1808
|
+
(LATENCY_MODE_ORDER.get(left.id) ?? 0) - (LATENCY_MODE_ORDER.get(right.id) ?? 0),
|
|
1809
|
+
),
|
|
1810
|
+
};
|
|
1811
|
+
}
|
|
1812
|
+
|
|
1813
|
+
function legacyModelCapabilities(
|
|
1814
|
+
settings: Settings,
|
|
1815
|
+
input: { reasoningEffort: boolean; hostedWebSearch: boolean },
|
|
1816
|
+
): ModelCapabilitiesV1 {
|
|
1817
|
+
const reasoningEfforts = input.reasoningEffort ? configuredAllowedReasoningEfforts(settings) : [];
|
|
1818
|
+
return normalizeCapabilities({
|
|
1819
|
+
reasoning: {
|
|
1820
|
+
upstream: input.reasoningEffort ? "supported" : "unknown",
|
|
1821
|
+
runnable: input.reasoningEffort,
|
|
1822
|
+
efforts: reasoningEfforts,
|
|
1823
|
+
defaultEffort: input.reasoningEffort ? settings.openaiReasoningEffort : null,
|
|
1824
|
+
required: false,
|
|
1825
|
+
},
|
|
1826
|
+
functionCalling: { upstream: "unknown", runnable: true },
|
|
1827
|
+
structuredOutput: { upstream: "unknown", runnable: false },
|
|
1828
|
+
hostedTools: {
|
|
1829
|
+
webSearch: {
|
|
1830
|
+
upstream: input.hostedWebSearch ? "supported" : "unknown",
|
|
1831
|
+
runnable: input.hostedWebSearch,
|
|
1832
|
+
},
|
|
1833
|
+
xSearch: { upstream: "unknown", runnable: false },
|
|
1834
|
+
codeExecution: { upstream: "unknown", runnable: false },
|
|
1835
|
+
},
|
|
1836
|
+
inputModalities: ["text"],
|
|
1837
|
+
outputModalities: ["text"],
|
|
1838
|
+
transports: {
|
|
1839
|
+
sse: { upstream: "unknown", runnable: true },
|
|
1840
|
+
responsesWebSocket: { upstream: "unknown", runnable: false },
|
|
1841
|
+
realtimeAudio: { upstream: "unknown", runnable: false },
|
|
1842
|
+
},
|
|
1843
|
+
latencyModes: [{ id: "standard", upstream: "unknown", runnable: true }],
|
|
1844
|
+
});
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1847
|
+
function registryCredentialSource(provider: RegistryProvider): CredentialSourceV1 {
|
|
1848
|
+
return provider.kind === "codex-subscription"
|
|
1849
|
+
? { kind: "connected_subscription", provider: "codex" }
|
|
1850
|
+
: { kind: "deployment", mechanism: "api_key" };
|
|
1851
|
+
}
|
|
1852
|
+
|
|
1853
|
+
function registryBilling(provider: RegistryProvider): BillingAttributionV1 {
|
|
1854
|
+
return provider.kind === "codex-subscription"
|
|
1855
|
+
? { upstreamPayer: "connected_subscription", metering: "external" }
|
|
1856
|
+
: { upstreamPayer: "deployment", metering: "opengeni_credits" };
|
|
1857
|
+
}
|
|
1858
|
+
|
|
1859
|
+
function builtinCredentialSource(settings: Settings): CredentialSourceV1 {
|
|
1860
|
+
if (settings.openaiProvider === "azure" && !settings.azureOpenaiApiKey) {
|
|
1861
|
+
return { kind: "deployment", mechanism: "azure_ad_bearer" };
|
|
1862
|
+
}
|
|
1863
|
+
return { kind: "deployment", mechanism: "api_key" };
|
|
1864
|
+
}
|
|
1865
|
+
|
|
1866
|
+
function staticRequestMetadataForDigest(provider: ResolvedModelProvider): {
|
|
1867
|
+
headers: Array<{ name: string; classification: "public" | "secret"; value?: string }>;
|
|
1868
|
+
query: Array<{ name: string; classification: "public" | "secret"; value?: string }>;
|
|
1869
|
+
} {
|
|
1870
|
+
const publicHeaders = new Set(provider.publicDefaultHeaderNames ?? []);
|
|
1871
|
+
const publicQuery = new Set(provider.publicDefaultQueryNames ?? []);
|
|
1872
|
+
return {
|
|
1873
|
+
headers: Object.entries(provider.defaultHeaders ?? {})
|
|
1874
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
1875
|
+
.map(([name, value]) =>
|
|
1876
|
+
publicHeaders.has(name)
|
|
1877
|
+
? { name, classification: "public" as const, value }
|
|
1878
|
+
: { name, classification: "secret" as const },
|
|
1879
|
+
),
|
|
1880
|
+
query: Object.entries(provider.defaultQuery ?? {})
|
|
1881
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
1882
|
+
.map(([name, value]) =>
|
|
1883
|
+
publicQuery.has(name)
|
|
1884
|
+
? { name, classification: "public" as const, value }
|
|
1885
|
+
: { name, classification: "secret" as const },
|
|
1886
|
+
),
|
|
1887
|
+
};
|
|
1888
|
+
}
|
|
1889
|
+
|
|
1890
|
+
function canonicalJson(value: unknown): string {
|
|
1891
|
+
const normalize = (input: unknown): unknown => {
|
|
1892
|
+
if (Array.isArray(input)) {
|
|
1893
|
+
return input.map((entry) => normalize(entry));
|
|
1894
|
+
}
|
|
1895
|
+
if (input && typeof input === "object") {
|
|
1896
|
+
const out: Record<string, unknown> = {};
|
|
1897
|
+
for (const key of Object.keys(input).sort()) {
|
|
1898
|
+
const child = (input as Record<string, unknown>)[key];
|
|
1899
|
+
if (child !== undefined) {
|
|
1900
|
+
out[key] = normalize(child);
|
|
1901
|
+
}
|
|
1902
|
+
}
|
|
1903
|
+
return out;
|
|
1904
|
+
}
|
|
1905
|
+
return input;
|
|
1906
|
+
};
|
|
1907
|
+
return JSON.stringify(normalize(value));
|
|
1908
|
+
}
|
|
1909
|
+
|
|
1910
|
+
function definitionVersionFor(
|
|
1911
|
+
model: Omit<ConfiguredModel, "definitionVersion">,
|
|
1912
|
+
provider: ResolvedModelProvider,
|
|
1913
|
+
): string {
|
|
1914
|
+
const requestMetadata = staticRequestMetadataForDigest(provider);
|
|
1915
|
+
const digestInput = canonicalJson({
|
|
1916
|
+
schemaVersion: model.schemaVersion,
|
|
1917
|
+
id: model.id,
|
|
1918
|
+
providerId: model.providerId,
|
|
1919
|
+
deployment: model.deployment,
|
|
1920
|
+
provider: {
|
|
1921
|
+
adapterKind: provider.kind,
|
|
1922
|
+
wireApi: provider.api,
|
|
1923
|
+
baseUrl: provider.baseUrl ?? null,
|
|
1924
|
+
defaultHeaders: requestMetadata.headers,
|
|
1925
|
+
defaultQuery: requestMetadata.query,
|
|
1926
|
+
},
|
|
1927
|
+
credentialSource: model.credentialSource,
|
|
1928
|
+
billing: model.billing,
|
|
1929
|
+
executionLimits: model.executionLimits,
|
|
1930
|
+
capabilities: model.capabilities,
|
|
1931
|
+
pricing: model.pricing ?? null,
|
|
1932
|
+
});
|
|
1933
|
+
return `sha256:${createHash("sha256")
|
|
1934
|
+
.update("opengeni:model-definition:v1\n", "utf8")
|
|
1935
|
+
.update(digestInput, "utf8")
|
|
1936
|
+
.digest("hex")}`;
|
|
1937
|
+
}
|
|
1938
|
+
|
|
1334
1939
|
/**
|
|
1335
1940
|
* The built-in provider's stable id: "openai" on the OpenAI platform, "azure"
|
|
1336
1941
|
* on Azure. Exported because the workspace model-policy gate must attribute
|
|
@@ -1355,18 +1960,24 @@ function builtinProviderLabel(settings: Pick<Settings, "openaiProvider">): strin
|
|
|
1355
1960
|
* id — validateSettings rejects that at boot.
|
|
1356
1961
|
*/
|
|
1357
1962
|
export function configuredProviders(settings: Settings): ResolvedModelProvider[] {
|
|
1963
|
+
const credentialSource = builtinCredentialSource(settings);
|
|
1358
1964
|
const builtin: ResolvedModelProvider = {
|
|
1359
1965
|
id: builtinProviderId(settings),
|
|
1360
1966
|
label: builtinProviderLabel(settings),
|
|
1361
1967
|
kind: "api-key",
|
|
1362
1968
|
api: "responses",
|
|
1363
1969
|
builtin: true,
|
|
1970
|
+
credentialSource,
|
|
1971
|
+
billing: { upstreamPayer: "deployment", metering: "opengeni_credits" },
|
|
1364
1972
|
};
|
|
1365
1973
|
if (settings.openaiProvider === "azure") {
|
|
1366
|
-
|
|
1974
|
+
const baseUrl = settings.azureOpenaiBaseUrl ?? settings.azureOpenaiEndpoint;
|
|
1975
|
+
builtin.baseUrl = baseUrl ? normalizeRegistryBaseUrl(baseUrl, builtin.id) : undefined;
|
|
1367
1976
|
builtin.apiKey = settings.azureOpenaiApiKey ?? settings.azureOpenaiAdToken;
|
|
1368
1977
|
} else {
|
|
1369
|
-
builtin.baseUrl = settings.openaiBaseUrl
|
|
1978
|
+
builtin.baseUrl = settings.openaiBaseUrl
|
|
1979
|
+
? normalizeRegistryBaseUrl(settings.openaiBaseUrl, builtin.id)
|
|
1980
|
+
: undefined;
|
|
1370
1981
|
builtin.apiKey = settings.openaiApiKey;
|
|
1371
1982
|
}
|
|
1372
1983
|
const registry = parseModelProvidersJson(settings.modelProvidersJson).map(
|
|
@@ -1380,11 +1991,46 @@ export function configuredProviders(settings: Settings): ResolvedModelProvider[]
|
|
|
1380
1991
|
apiKey: resolveProviderApiKey(provider),
|
|
1381
1992
|
defaultQuery: provider.defaultQuery,
|
|
1382
1993
|
defaultHeaders: provider.defaultHeaders,
|
|
1994
|
+
publicDefaultQueryNames: provider.publicDefaultQueryNames,
|
|
1995
|
+
publicDefaultHeaderNames: provider.publicDefaultHeaderNames,
|
|
1996
|
+
credentialSource: registryCredentialSource(provider),
|
|
1997
|
+
billing: registryBilling(provider),
|
|
1383
1998
|
}),
|
|
1384
1999
|
);
|
|
1385
2000
|
return [builtin, ...registry];
|
|
1386
2001
|
}
|
|
1387
2002
|
|
|
2003
|
+
/**
|
|
2004
|
+
* Pure catalog overlay for a workspace whose existing Codex connection seam
|
|
2005
|
+
* reports ready. This describes product/provider identity only; it does not
|
|
2006
|
+
* select, lease, refresh, or expose a concrete credential; those runtime
|
|
2007
|
+
* operations remain owned by the credential allocator.
|
|
2008
|
+
*/
|
|
2009
|
+
export function withCodexCatalogProvider(settings: Settings): Settings {
|
|
2010
|
+
const providers = parseModelProvidersJson(settings.modelProvidersJson);
|
|
2011
|
+
if (providers.some((provider) => provider.id === CODEX_PROVIDER_ID)) {
|
|
2012
|
+
return settings;
|
|
2013
|
+
}
|
|
2014
|
+
const provider: RegistryProvider = {
|
|
2015
|
+
kind: "codex-subscription",
|
|
2016
|
+
id: CODEX_PROVIDER_ID,
|
|
2017
|
+
label: "Codex (ChatGPT subscription)",
|
|
2018
|
+
api: "responses",
|
|
2019
|
+
baseUrl: CODEX_PROVIDER_BASE_URL,
|
|
2020
|
+
models: CODEX_FALLBACK_MODEL_SLUGS.map((slug) => ({
|
|
2021
|
+
id: `${CODEX_MODEL_ID_PREFIX}${slug}`,
|
|
2022
|
+
upstreamModelId: slug,
|
|
2023
|
+
label: slug,
|
|
2024
|
+
reasoningEffort: true,
|
|
2025
|
+
contextWindowTokens: CODEX_MODEL_CONTEXT_WINDOW_TOKENS,
|
|
2026
|
+
effectiveContextWindowTokens: CODEX_MODEL_EFFECTIVE_CONTEXT_WINDOW_TOKENS,
|
|
2027
|
+
autoCompactTokenLimit: CODEX_MODEL_AUTO_COMPACT_TOKEN_LIMIT,
|
|
2028
|
+
toolOutputTruncationTokens: CODEX_MODEL_TOOL_OUTPUT_TRUNCATION_TOKENS,
|
|
2029
|
+
})),
|
|
2030
|
+
};
|
|
2031
|
+
return { ...settings, modelProvidersJson: JSON.stringify([...providers, provider]) };
|
|
2032
|
+
}
|
|
2033
|
+
|
|
1388
2034
|
/**
|
|
1389
2035
|
* The provider identity a model id resolves to, for workspace model-policy
|
|
1390
2036
|
* evaluation — MUST agree with the real router (resolveTurnModel /
|
|
@@ -1399,13 +2045,83 @@ export function configuredProviders(settings: Settings): ResolvedModelProvider[]
|
|
|
1399
2045
|
* serves. A policy blocking the built-in must block this path too.
|
|
1400
2046
|
*/
|
|
1401
2047
|
export function policyProviderIdForModel(settings: Settings, modelId: string): string {
|
|
1402
|
-
|
|
2048
|
+
const canonicalModelId = canonicalizeConfiguredModelId(settings, modelId);
|
|
2049
|
+
if (canonicalModelId.startsWith(CODEX_MODEL_ID_PREFIX)) {
|
|
1403
2050
|
return CODEX_PROVIDER_ID;
|
|
1404
2051
|
}
|
|
1405
|
-
const configured = configuredModels(settings).find((model) => model.id ===
|
|
2052
|
+
const configured = configuredModels(settings).find((model) => model.id === canonicalModelId);
|
|
1406
2053
|
return configured?.providerId ?? builtinProviderId(settings);
|
|
1407
2054
|
}
|
|
1408
2055
|
|
|
2056
|
+
function resolvedExecutionLimits(
|
|
2057
|
+
settings: Settings,
|
|
2058
|
+
model: {
|
|
2059
|
+
contextWindowTokens?: number | undefined;
|
|
2060
|
+
effectiveContextWindowTokens?: number | undefined;
|
|
2061
|
+
autoCompactTokenLimit?: number | undefined;
|
|
2062
|
+
toolOutputTruncationTokens?: number | undefined;
|
|
2063
|
+
},
|
|
2064
|
+
): ModelExecutionLimitsV1 {
|
|
2065
|
+
return {
|
|
2066
|
+
contextWindowTokens: model.contextWindowTokens ?? settings.contextWindowTokens,
|
|
2067
|
+
effectiveContextWindowTokens:
|
|
2068
|
+
model.effectiveContextWindowTokens ?? settings.contextEffectiveWindowTokens ?? null,
|
|
2069
|
+
autoCompactTokenLimit:
|
|
2070
|
+
model.autoCompactTokenLimit ?? settings.contextAutoCompactThresholdTokens ?? null,
|
|
2071
|
+
toolOutputTruncationTokens:
|
|
2072
|
+
model.toolOutputTruncationTokens ?? settings.modelToolOutputTruncationTokens ?? null,
|
|
2073
|
+
};
|
|
2074
|
+
}
|
|
2075
|
+
|
|
2076
|
+
function finalizeConfiguredModel(
|
|
2077
|
+
settings: Settings,
|
|
2078
|
+
provider: ResolvedModelProvider,
|
|
2079
|
+
input: Omit<ConfiguredModel, "schemaVersion" | "definitionVersion" | "executionLimits">,
|
|
2080
|
+
): ConfiguredModel {
|
|
2081
|
+
const modelWithoutVersion: Omit<ConfiguredModel, "definitionVersion"> = {
|
|
2082
|
+
schemaVersion: 1,
|
|
2083
|
+
...input,
|
|
2084
|
+
executionLimits: resolvedExecutionLimits(settings, input),
|
|
2085
|
+
};
|
|
2086
|
+
return {
|
|
2087
|
+
...modelWithoutVersion,
|
|
2088
|
+
definitionVersion: definitionVersionFor(modelWithoutVersion, provider),
|
|
2089
|
+
};
|
|
2090
|
+
}
|
|
2091
|
+
|
|
2092
|
+
function assertUniqueModelIdentities(models: ConfiguredModel[]): void {
|
|
2093
|
+
const canonicalOwners = new Map<string, string>();
|
|
2094
|
+
for (const model of models) {
|
|
2095
|
+
const previous = canonicalOwners.get(model.id);
|
|
2096
|
+
if (previous !== undefined) {
|
|
2097
|
+
throw new Error(
|
|
2098
|
+
`OPENGENI_MODEL_PROVIDERS_JSON model id ${JSON.stringify(model.id)} is declared by both ${previous} and ${model.providerId}`,
|
|
2099
|
+
);
|
|
2100
|
+
}
|
|
2101
|
+
canonicalOwners.set(model.id, model.providerId);
|
|
2102
|
+
}
|
|
2103
|
+
|
|
2104
|
+
const acceptedInputs = new Map(canonicalOwners);
|
|
2105
|
+
for (const model of models) {
|
|
2106
|
+
const ownAliases = new Set<string>();
|
|
2107
|
+
for (const alias of model.aliases) {
|
|
2108
|
+
if (ownAliases.has(alias)) {
|
|
2109
|
+
throw new Error(
|
|
2110
|
+
`OPENGENI_MODEL_PROVIDERS_JSON model ${JSON.stringify(model.id)} contains duplicate alias ${JSON.stringify(alias)}`,
|
|
2111
|
+
);
|
|
2112
|
+
}
|
|
2113
|
+
ownAliases.add(alias);
|
|
2114
|
+
const previous = acceptedInputs.get(alias);
|
|
2115
|
+
if (previous !== undefined) {
|
|
2116
|
+
throw new Error(
|
|
2117
|
+
`OPENGENI_MODEL_PROVIDERS_JSON alias ${JSON.stringify(alias)} for model ${JSON.stringify(model.id)} collides with model/provider ${previous}`,
|
|
2118
|
+
);
|
|
2119
|
+
}
|
|
2120
|
+
acceptedInputs.set(alias, model.id);
|
|
2121
|
+
}
|
|
2122
|
+
}
|
|
2123
|
+
}
|
|
2124
|
+
|
|
1409
2125
|
/**
|
|
1410
2126
|
* Every model a client may use, the built-in provider's models first
|
|
1411
2127
|
* (configuredAllowedModels-from-openai, mapped to "responses" with
|
|
@@ -1417,6 +2133,9 @@ export function policyProviderIdForModel(settings: Settings, modelId: string): s
|
|
|
1417
2133
|
export function configuredModels(settings: Settings): ConfiguredModel[] {
|
|
1418
2134
|
const builtinId = builtinProviderId(settings);
|
|
1419
2135
|
const builtinLabel = builtinProviderLabel(settings);
|
|
2136
|
+
const providers = configuredProviders(settings);
|
|
2137
|
+
const providerById = new Map(providers.map((provider) => [provider.id, provider]));
|
|
2138
|
+
const pricingSchedules = configuredModelPricingSchedules(settings);
|
|
1420
2139
|
// The built-in (OpenAI/Azure) provider must NEVER claim a registry-namespaced
|
|
1421
2140
|
// model id. The worker overwrites settings.openaiModel with the turn's model
|
|
1422
2141
|
// (apps/worker agent-turn runSettings) — including a `codex/<slug>` id, or a
|
|
@@ -1433,63 +2152,110 @@ export function configuredModels(settings: Settings): ConfiguredModel[] {
|
|
|
1433
2152
|
// a codex/ id has NO codex provider injected (no active subscription) it then
|
|
1434
2153
|
// resolves to nothing and getModel fails loud with
|
|
1435
2154
|
// CodexSubscriptionUnavailableError instead of mis-routing to Azure.
|
|
2155
|
+
const parsedRegistry = parseModelProvidersJson(settings.modelProvidersJson);
|
|
1436
2156
|
const registryOwnedIds = new Set(
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
2157
|
+
parsedRegistry.flatMap((provider) => provider.models.map((model) => model.id)),
|
|
2158
|
+
);
|
|
2159
|
+
const registryAliases = new Set(
|
|
2160
|
+
parsedRegistry.flatMap((provider) => provider.models.flatMap((model) => model.aliases ?? [])),
|
|
1440
2161
|
);
|
|
1441
2162
|
const isRegistryNamespaced = (id: string): boolean =>
|
|
1442
|
-
id.startsWith(CODEX_MODEL_ID_PREFIX) ||
|
|
2163
|
+
id.startsWith(CODEX_MODEL_ID_PREFIX) ||
|
|
2164
|
+
registryAliases.has(id) ||
|
|
2165
|
+
(id.includes("/") && registryOwnedIds.has(id));
|
|
2166
|
+
const builtinProvider = providerById.get(builtinId);
|
|
2167
|
+
if (!builtinProvider) {
|
|
2168
|
+
throw new Error(`Built-in model provider ${builtinId} is not configured`);
|
|
2169
|
+
}
|
|
1443
2170
|
const out: ConfiguredModel[] = uniqueValues([
|
|
1444
2171
|
settings.openaiModel,
|
|
1445
2172
|
...splitCsv(settings.openaiAllowedModels),
|
|
1446
2173
|
])
|
|
1447
2174
|
.filter((id) => !isRegistryNamespaced(id))
|
|
1448
|
-
.map((id) =>
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
2175
|
+
.map((id) => {
|
|
2176
|
+
const capabilities = legacyModelCapabilities(settings, {
|
|
2177
|
+
reasoningEffort: true,
|
|
2178
|
+
hostedWebSearch: settings.webSearchEnabled,
|
|
2179
|
+
});
|
|
2180
|
+
return finalizeConfiguredModel(settings, builtinProvider, {
|
|
2181
|
+
id,
|
|
2182
|
+
aliases: [],
|
|
2183
|
+
label: id,
|
|
2184
|
+
providerId: builtinId,
|
|
2185
|
+
providerLabel: builtinLabel,
|
|
2186
|
+
api: "responses" as const,
|
|
2187
|
+
upstreamModelId: id,
|
|
2188
|
+
deployment: { upstreamModelId: id, wireApi: "responses" },
|
|
2189
|
+
credentialSource: builtinProvider.credentialSource,
|
|
2190
|
+
billing: builtinProvider.billing,
|
|
2191
|
+
capabilities,
|
|
2192
|
+
...(pricingSchedules[id] === undefined ? {} : { pricing: pricingSchedules[id] }),
|
|
2193
|
+
contextWindowTokens: settings.contextWindowTokens,
|
|
2194
|
+
toolOutputTruncationTokens: settings.modelToolOutputTruncationTokens,
|
|
2195
|
+
reasoningEffort: capabilities.reasoning.runnable,
|
|
2196
|
+
hostedWebSearch: capabilities.hostedTools.webSearch.runnable,
|
|
2197
|
+
});
|
|
2198
|
+
});
|
|
2199
|
+
for (const provider of parsedRegistry) {
|
|
1460
2200
|
const providerLabel = provider.label ?? provider.id;
|
|
2201
|
+
const resolvedProvider = providerById.get(provider.id);
|
|
2202
|
+
if (!resolvedProvider) {
|
|
2203
|
+
throw new Error(`Registry model provider ${provider.id} is not configured`);
|
|
2204
|
+
}
|
|
1461
2205
|
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
|
-
|
|
2206
|
+
const capabilities = model.capabilities
|
|
2207
|
+
? normalizeCapabilities(model.capabilities)
|
|
2208
|
+
: legacyModelCapabilities(settings, {
|
|
2209
|
+
reasoningEffort: model.reasoningEffort ?? false,
|
|
2210
|
+
hostedWebSearch: model.hostedWebSearch ?? false,
|
|
2211
|
+
});
|
|
2212
|
+
const upstreamModelId = model.upstreamModelId ?? model.id;
|
|
2213
|
+
out.push(
|
|
2214
|
+
finalizeConfiguredModel(settings, resolvedProvider, {
|
|
2215
|
+
id: model.id,
|
|
2216
|
+
aliases: [...(model.aliases ?? [])],
|
|
2217
|
+
label: model.label ?? model.id,
|
|
2218
|
+
providerId: provider.id,
|
|
2219
|
+
providerLabel,
|
|
2220
|
+
api: provider.api,
|
|
2221
|
+
upstreamModelId,
|
|
2222
|
+
deployment: { upstreamModelId, wireApi: provider.api },
|
|
2223
|
+
credentialSource: resolvedProvider.credentialSource,
|
|
2224
|
+
billing: resolvedProvider.billing,
|
|
2225
|
+
capabilities,
|
|
2226
|
+
...(pricingSchedules[model.id] === undefined
|
|
2227
|
+
? {}
|
|
2228
|
+
: { pricing: pricingSchedules[model.id] }),
|
|
2229
|
+
...(model.contextWindowTokens === undefined
|
|
2230
|
+
? {}
|
|
2231
|
+
: { contextWindowTokens: model.contextWindowTokens }),
|
|
2232
|
+
...(model.effectiveContextWindowTokens === undefined
|
|
2233
|
+
? {}
|
|
2234
|
+
: { effectiveContextWindowTokens: model.effectiveContextWindowTokens }),
|
|
2235
|
+
...(model.autoCompactTokenLimit === undefined
|
|
2236
|
+
? {}
|
|
2237
|
+
: { autoCompactTokenLimit: model.autoCompactTokenLimit }),
|
|
2238
|
+
...(model.toolOutputTruncationTokens === undefined
|
|
2239
|
+
? {}
|
|
2240
|
+
: { toolOutputTruncationTokens: model.toolOutputTruncationTokens }),
|
|
2241
|
+
reasoningEffort: capabilities.reasoning.runnable,
|
|
2242
|
+
hostedWebSearch: capabilities.hostedTools.webSearch.runnable,
|
|
2243
|
+
}),
|
|
2244
|
+
);
|
|
1483
2245
|
}
|
|
1484
2246
|
}
|
|
1485
|
-
|
|
1486
|
-
return out
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
2247
|
+
assertUniqueModelIdentities(out);
|
|
2248
|
+
return out;
|
|
2249
|
+
}
|
|
2250
|
+
|
|
2251
|
+
/** Resolve a known canonical id or alias. Unknown strings are returned unchanged. */
|
|
2252
|
+
export function canonicalizeConfiguredModelId(settings: Settings, modelId: string): string {
|
|
2253
|
+
const models = configuredModels(settings);
|
|
2254
|
+
const canonical = models.find((model) => model.id === modelId);
|
|
2255
|
+
if (canonical) {
|
|
2256
|
+
return canonical.id;
|
|
2257
|
+
}
|
|
2258
|
+
return models.find((model) => model.aliases.includes(modelId))?.id ?? modelId;
|
|
1493
2259
|
}
|
|
1494
2260
|
|
|
1495
2261
|
/**
|
|
@@ -1513,7 +2279,8 @@ export function resolveModelProvider(
|
|
|
1513
2279
|
settings: Settings,
|
|
1514
2280
|
modelId: string,
|
|
1515
2281
|
): { provider: ResolvedModelProvider; model: ConfiguredModel } | undefined {
|
|
1516
|
-
const
|
|
2282
|
+
const canonicalModelId = canonicalizeConfiguredModelId(settings, modelId);
|
|
2283
|
+
const model = configuredModels(settings).find((candidate) => candidate.id === canonicalModelId);
|
|
1517
2284
|
if (!model) {
|
|
1518
2285
|
return undefined;
|
|
1519
2286
|
}
|
|
@@ -1526,28 +2293,169 @@ export function resolveModelProvider(
|
|
|
1526
2293
|
return { provider, model };
|
|
1527
2294
|
}
|
|
1528
2295
|
|
|
2296
|
+
export type ResolveTurnExecutionPolicyV1Input = {
|
|
2297
|
+
/** Effective persisted turn model. Aliases are accepted and canonicalized. */
|
|
2298
|
+
modelId: string;
|
|
2299
|
+
/** Exact caller-supplied input before canonicalization, only for explicit switches. */
|
|
2300
|
+
requestedModelId: string | null;
|
|
2301
|
+
modelSource: TurnExecutionModelSourceV1;
|
|
2302
|
+
reasoningEffort: Settings["openaiReasoningEffort"];
|
|
2303
|
+
reasoningSource: TurnExecutionReasoningSourceV1;
|
|
2304
|
+
};
|
|
2305
|
+
|
|
2306
|
+
function settingsForTurnExecutionPolicy(settings: Settings, modelId: string): Settings {
|
|
2307
|
+
return settings.codexSubscriptionEnabled && modelId.startsWith(CODEX_MODEL_ID_PREFIX)
|
|
2308
|
+
? withCodexCatalogProvider(settings)
|
|
2309
|
+
: settings;
|
|
2310
|
+
}
|
|
2311
|
+
|
|
1529
2312
|
/**
|
|
1530
|
-
*
|
|
1531
|
-
*
|
|
1532
|
-
*
|
|
2313
|
+
* Build a trusted, secret-safe execution policy from the normalized catalog.
|
|
2314
|
+
* The Codex overlay here contains static product/provider identity only; it
|
|
2315
|
+
* neither proves readiness nor chooses, decrypts, leases, or exposes an account.
|
|
1533
2316
|
*/
|
|
1534
|
-
export function
|
|
1535
|
-
|
|
2317
|
+
export function resolveTurnExecutionPolicyV1(
|
|
2318
|
+
settings: Settings,
|
|
2319
|
+
input: ResolveTurnExecutionPolicyV1Input,
|
|
2320
|
+
): TurnExecutionPolicyV1 {
|
|
2321
|
+
const catalogSettings = settingsForTurnExecutionPolicy(settings, input.modelId);
|
|
2322
|
+
const productModelId = canonicalizeConfiguredModelId(catalogSettings, input.modelId);
|
|
2323
|
+
const resolved = resolveModelProvider(catalogSettings, productModelId);
|
|
2324
|
+
if (!resolved) {
|
|
2325
|
+
throw new Error("Turn execution policy model is not present in the configured catalog");
|
|
2326
|
+
}
|
|
2327
|
+
if (
|
|
2328
|
+
input.requestedModelId !== null &&
|
|
2329
|
+
canonicalizeConfiguredModelId(catalogSettings, input.requestedModelId) !== productModelId
|
|
2330
|
+
) {
|
|
2331
|
+
throw new Error("Turn execution policy requested model does not canonicalize to its product");
|
|
2332
|
+
}
|
|
2333
|
+
return TurnExecutionPolicyV1.parse({
|
|
2334
|
+
schemaVersion: 1,
|
|
2335
|
+
productModelId,
|
|
2336
|
+
requestedModelId: input.requestedModelId,
|
|
2337
|
+
modelSource: input.modelSource,
|
|
2338
|
+
reasoningEffort: input.reasoningEffort,
|
|
2339
|
+
reasoningSource: input.reasoningSource,
|
|
2340
|
+
providerId: resolved.provider.id,
|
|
2341
|
+
upstreamModelId: resolved.model.upstreamModelId,
|
|
2342
|
+
wireApi: resolved.model.api,
|
|
2343
|
+
credentialSource: resolved.model.credentialSource,
|
|
2344
|
+
billing: resolved.model.billing,
|
|
2345
|
+
definitionVersion: resolved.model.definitionVersion,
|
|
2346
|
+
});
|
|
2347
|
+
}
|
|
2348
|
+
|
|
2349
|
+
/**
|
|
2350
|
+
* Parse-time validation lives in @opengeni/contracts; this verifier binds a
|
|
2351
|
+
* present snapshot to the current executable definition and exact turn row.
|
|
2352
|
+
* Any deployment/provider drift fails before a provider or compaction call.
|
|
2353
|
+
*/
|
|
2354
|
+
export function assertTurnExecutionPolicyMatchesConfigV1(
|
|
2355
|
+
settings: Settings,
|
|
2356
|
+
policy: TurnExecutionPolicyV1,
|
|
2357
|
+
expected: {
|
|
2358
|
+
modelId: string;
|
|
2359
|
+
reasoningEffort: Settings["openaiReasoningEffort"];
|
|
2360
|
+
},
|
|
2361
|
+
): {
|
|
2362
|
+
policy: TurnExecutionPolicyV1;
|
|
2363
|
+
provider: ResolvedModelProvider;
|
|
2364
|
+
model: ConfiguredModel;
|
|
2365
|
+
} {
|
|
2366
|
+
const parsed = TurnExecutionPolicyV1.parse(policy);
|
|
2367
|
+
const catalogSettings = settingsForTurnExecutionPolicy(settings, parsed.productModelId);
|
|
2368
|
+
const canonicalExpectedModel = canonicalizeConfiguredModelId(catalogSettings, expected.modelId);
|
|
2369
|
+
if (
|
|
2370
|
+
parsed.productModelId !== canonicalExpectedModel ||
|
|
2371
|
+
parsed.reasoningEffort !== expected.reasoningEffort
|
|
2372
|
+
) {
|
|
2373
|
+
throw new Error("Turn execution policy does not match the accepted turn model/reasoning");
|
|
2374
|
+
}
|
|
2375
|
+
if (
|
|
2376
|
+
parsed.requestedModelId !== null &&
|
|
2377
|
+
canonicalizeConfiguredModelId(catalogSettings, parsed.requestedModelId) !==
|
|
2378
|
+
parsed.productModelId
|
|
2379
|
+
) {
|
|
2380
|
+
throw new Error("Turn execution policy requested model does not match its product model");
|
|
2381
|
+
}
|
|
2382
|
+
const resolved = resolveModelProvider(catalogSettings, parsed.productModelId);
|
|
2383
|
+
if (!resolved) {
|
|
2384
|
+
throw new Error("Turn execution policy model is no longer configured");
|
|
2385
|
+
}
|
|
2386
|
+
const mismatched =
|
|
2387
|
+
parsed.providerId !== resolved.provider.id ||
|
|
2388
|
+
parsed.upstreamModelId !== resolved.model.upstreamModelId ||
|
|
2389
|
+
parsed.wireApi !== resolved.model.api ||
|
|
2390
|
+
parsed.definitionVersion !== resolved.model.definitionVersion ||
|
|
2391
|
+
canonicalJson(parsed.credentialSource) !== canonicalJson(resolved.model.credentialSource) ||
|
|
2392
|
+
canonicalJson(parsed.billing) !== canonicalJson(resolved.model.billing);
|
|
2393
|
+
if (mismatched) {
|
|
2394
|
+
throw new Error("Turn execution policy does not match the current provider definition");
|
|
2395
|
+
}
|
|
2396
|
+
return { policy: parsed, provider: resolved.provider, model: resolved.model };
|
|
2397
|
+
}
|
|
2398
|
+
|
|
2399
|
+
/**
|
|
2400
|
+
* Effective per-model pricing schedules. Merge order (later wins): built-in
|
|
2401
|
+
* flat defaults → registry model flat/scheduled pricing → explicit legacy flat
|
|
2402
|
+
* OPENGENI_MODEL_PRICING_JSON. The explicit legacy map intentionally replaces
|
|
2403
|
+
* a registry schedule with one flat default so its historical precedence stays
|
|
2404
|
+
* exact.
|
|
2405
|
+
*/
|
|
2406
|
+
export function configuredModelPricingSchedules(
|
|
2407
|
+
settings: Settings,
|
|
2408
|
+
): Record<string, ModelPricingScheduleV1> {
|
|
2409
|
+
const defaults = Object.fromEntries(
|
|
2410
|
+
Object.entries(defaultModelPricing).map(([model, pricing]) => [model, { default: pricing }]),
|
|
2411
|
+
);
|
|
2412
|
+
const registry: Record<string, ModelPricingScheduleV1> = {};
|
|
1536
2413
|
for (const provider of parseModelProvidersJson(settings.modelProvidersJson)) {
|
|
1537
2414
|
for (const model of provider.models) {
|
|
1538
2415
|
if (model.pricing) {
|
|
1539
|
-
registry[model.id] = model.pricing;
|
|
2416
|
+
registry[model.id] = normalizeModelPricingSchedule(model.pricing);
|
|
1540
2417
|
}
|
|
1541
2418
|
}
|
|
1542
2419
|
}
|
|
1543
|
-
const configured =
|
|
2420
|
+
const configured = Object.fromEntries(
|
|
2421
|
+
Object.entries(parseModelPricingJson(settings.modelPricingJson)).map(([model, pricing]) => [
|
|
2422
|
+
model,
|
|
2423
|
+
{ default: pricing },
|
|
2424
|
+
]),
|
|
2425
|
+
);
|
|
1544
2426
|
return {
|
|
1545
|
-
...
|
|
2427
|
+
...defaults,
|
|
1546
2428
|
...registry,
|
|
1547
2429
|
...configured,
|
|
1548
2430
|
};
|
|
1549
2431
|
}
|
|
1550
2432
|
|
|
2433
|
+
/** Legacy flat projection: returns the default/below-threshold price. */
|
|
2434
|
+
export function configuredModelPricing(settings: Settings): Record<string, ModelPricing> {
|
|
2435
|
+
return Object.fromEntries(
|
|
2436
|
+
Object.entries(configuredModelPricingSchedules(settings)).map(([model, schedule]) => [
|
|
2437
|
+
model,
|
|
2438
|
+
schedule.default,
|
|
2439
|
+
]),
|
|
2440
|
+
);
|
|
2441
|
+
}
|
|
2442
|
+
|
|
2443
|
+
/** Select the per-provider-request price at an exact input-token threshold. */
|
|
2444
|
+
export function selectModelPricing(
|
|
2445
|
+
schedule: ModelPricingScheduleV1,
|
|
2446
|
+
inputTokens: number,
|
|
2447
|
+
): ModelPricing {
|
|
2448
|
+
const normalizedInputTokens = Math.max(0, Math.floor(inputTokens));
|
|
2449
|
+
let selected = schedule.default;
|
|
2450
|
+
for (const tier of schedule.inputTokenTiers ?? []) {
|
|
2451
|
+
if (normalizedInputTokens < tier.minimumInputTokens) {
|
|
2452
|
+
break;
|
|
2453
|
+
}
|
|
2454
|
+
selected = tier.pricing;
|
|
2455
|
+
}
|
|
2456
|
+
return selected;
|
|
2457
|
+
}
|
|
2458
|
+
|
|
1551
2459
|
/**
|
|
1552
2460
|
* Usable input-token budget: an explicit model-catalog effective window when
|
|
1553
2461
|
* available, otherwise the deployment window minus its output reserve.
|
|
@@ -1627,17 +2535,28 @@ export function calculateModelUsageCostMicros(
|
|
|
1627
2535
|
model: string,
|
|
1628
2536
|
usage: ModelUsageInput,
|
|
1629
2537
|
): number {
|
|
1630
|
-
const
|
|
1631
|
-
if (!
|
|
2538
|
+
const schedule = configuredModelPricingSchedules(settings)[model];
|
|
2539
|
+
if (!schedule) {
|
|
1632
2540
|
throw new Error(`Missing model pricing for ${model}`);
|
|
1633
2541
|
}
|
|
1634
2542
|
const entries =
|
|
1635
2543
|
usage.requestUsageEntries && usage.requestUsageEntries.length > 0
|
|
1636
2544
|
? usage.requestUsageEntries
|
|
1637
2545
|
: [usage];
|
|
1638
|
-
const
|
|
1639
|
-
const
|
|
1640
|
-
|
|
2546
|
+
const rawCostByPricing = new Map<ModelPricing, number>();
|
|
2547
|
+
for (const entry of entries) {
|
|
2548
|
+
const pricing = selectModelPricing(schedule, positiveInt(entry.inputTokens));
|
|
2549
|
+
rawCostByPricing.set(
|
|
2550
|
+
pricing,
|
|
2551
|
+
(rawCostByPricing.get(pricing) ?? 0) + calculateEntryCostMicros(pricing, entry),
|
|
2552
|
+
);
|
|
2553
|
+
}
|
|
2554
|
+
let total = 0;
|
|
2555
|
+
for (const [pricing, rawCost] of rawCostByPricing) {
|
|
2556
|
+
const marginBps = pricing.marginBps ?? 0;
|
|
2557
|
+
total += Math.ceil((rawCost * (10_000 + marginBps)) / 10_000);
|
|
2558
|
+
}
|
|
2559
|
+
return total;
|
|
1641
2560
|
}
|
|
1642
2561
|
|
|
1643
2562
|
export function configuredAllowedReasoningEfforts(
|
|
@@ -2151,7 +3070,14 @@ export function parseModelProvidersJson(raw: string): RegistryProvider[] {
|
|
|
2151
3070
|
`OPENGENI_MODEL_PROVIDERS_JSON provider[${index}] is invalid: ${result.error.message}`,
|
|
2152
3071
|
);
|
|
2153
3072
|
}
|
|
2154
|
-
|
|
3073
|
+
try {
|
|
3074
|
+
return normalizeRegistryProvider(result.data);
|
|
3075
|
+
} catch (error) {
|
|
3076
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3077
|
+
throw new Error(`OPENGENI_MODEL_PROVIDERS_JSON provider[${index}] is invalid: ${message}`, {
|
|
3078
|
+
cause: error,
|
|
3079
|
+
});
|
|
3080
|
+
}
|
|
2155
3081
|
});
|
|
2156
3082
|
}
|
|
2157
3083
|
|
|
@@ -2720,6 +3646,10 @@ function validateSettings(settings: Settings): void {
|
|
|
2720
3646
|
);
|
|
2721
3647
|
}
|
|
2722
3648
|
}
|
|
3649
|
+
// Materialize the normalized catalog at boot so canonical product ids,
|
|
3650
|
+
// aliases, definition digests, and capability/pricing normalization are
|
|
3651
|
+
// validated even when managed billing is disabled.
|
|
3652
|
+
configuredModels(settings);
|
|
2723
3653
|
}
|
|
2724
3654
|
|
|
2725
3655
|
/**
|