@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/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 { CODEX_MODEL_ID_PREFIX, CODEX_PROVIDER_ID } from "@opengeni/codex/constants";
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
- * Human-approval policy for this server's tools, overlaid per-run from a
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.object({
819
- id: z.string().min(1), // model id sent to the provider, e.g. "accounts/fireworks/models/glm-5p2"
820
- label: z.string().min(1).optional(), // display name; defaults to id
821
- contextWindowTokens: z.number().int().positive().optional(),
822
- effectiveContextWindowTokens: z.number().int().positive().optional(),
823
- autoCompactTokenLimit: z.number().int().positive().optional(),
824
- // Canonical model-facing function/tool-result policy. The runtime applies
825
- // the same 1.2x serialization allowance as Codex when materializing output.
826
- toolOutputTruncationTokens: z.number().int().positive().optional(),
827
- reasoningEffort: z.boolean().optional(), // model accepts a reasoning-effort control
828
- hostedWebSearch: z.boolean().optional(), // provider executes the hosted web_search tool for this model
829
- pricing: ModelPricingSchema.optional(),
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
- builtin.baseUrl = settings.azureOpenaiBaseUrl ?? settings.azureOpenaiEndpoint;
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
- if (modelId.startsWith(CODEX_MODEL_ID_PREFIX)) {
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 === modelId);
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
- parseModelProvidersJson(settings.modelProvidersJson).flatMap((provider) =>
1438
- provider.models.map((model) => model.id),
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) || (id.includes("/") && registryOwnedIds.has(id));
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
- id,
1450
- label: id,
1451
- providerId: builtinId,
1452
- providerLabel: builtinLabel,
1453
- api: "responses" as const,
1454
- contextWindowTokens: settings.contextWindowTokens,
1455
- toolOutputTruncationTokens: settings.modelToolOutputTruncationTokens,
1456
- reasoningEffort: true,
1457
- hostedWebSearch: settings.webSearchEnabled,
1458
- }));
1459
- for (const provider of parseModelProvidersJson(settings.modelProvidersJson)) {
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
- out.push({
1463
- id: model.id,
1464
- label: model.label ?? model.id,
1465
- providerId: provider.id,
1466
- providerLabel,
1467
- api: provider.api,
1468
- ...(model.contextWindowTokens === undefined
1469
- ? {}
1470
- : { contextWindowTokens: model.contextWindowTokens }),
1471
- ...(model.effectiveContextWindowTokens === undefined
1472
- ? {}
1473
- : { effectiveContextWindowTokens: model.effectiveContextWindowTokens }),
1474
- ...(model.autoCompactTokenLimit === undefined
1475
- ? {}
1476
- : { autoCompactTokenLimit: model.autoCompactTokenLimit }),
1477
- ...(model.toolOutputTruncationTokens === undefined
1478
- ? {}
1479
- : { toolOutputTruncationTokens: model.toolOutputTruncationTokens }),
1480
- reasoningEffort: model.reasoningEffort ?? false,
1481
- hostedWebSearch: model.hostedWebSearch ?? false,
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
- const seen = new Set<string>();
1486
- return out.filter((model) => {
1487
- if (seen.has(model.id)) {
1488
- return false;
1489
- }
1490
- seen.add(model.id);
1491
- return true;
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 model = configuredModels(settings).find((candidate) => candidate.id === modelId);
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
- * Effective per-model pricing. Merge order (later wins):
1531
- * defaultModelPricing registry model `pricing` entries (keyed by model id)
1532
- * parseModelPricingJson(settings.modelPricingJson) (explicit JSON wins).
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 configuredModelPricing(settings: Settings): Record<string, ModelPricing> {
1535
- const registry: Record<string, ModelPricing> = {};
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 = parseModelPricingJson(settings.modelPricingJson);
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
- ...defaultModelPricing,
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 pricing = configuredModelPricing(settings)[model];
1631
- if (!pricing) {
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 rawCost = entries.reduce((sum, entry) => sum + calculateEntryCostMicros(pricing, entry), 0);
1639
- const marginBps = pricing.marginBps ?? 0;
1640
- return Math.ceil((rawCost * (10_000 + marginBps)) / 10_000);
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
- return result.data;
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
  /**