@opengeni/config 0.5.1 → 0.6.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.ts CHANGED
@@ -6,10 +6,24 @@ import {
6
6
  ProductAccessMode,
7
7
  ReasoningEffort,
8
8
  SandboxBackend,
9
+ SessionMcpApprovalPolicy,
9
10
  StaticUsageLimits,
11
+ TurnExecutionPolicyV1,
10
12
  UsageLimitsMode,
13
+ type TurnExecutionModelSourceV1,
14
+ type TurnExecutionReasoningSourceV1,
11
15
  } from "@opengeni/contracts";
12
- import { CODEX_MODEL_ID_PREFIX, CODEX_PROVIDER_ID } from "@opengeni/codex/constants";
16
+ import { CODEX_MODEL_TOOL_OUTPUT_TRUNCATION_TOKENS } from "@opengeni/codex";
17
+ import {
18
+ CODEX_FALLBACK_MODEL_SLUGS,
19
+ CODEX_MODEL_AUTO_COMPACT_TOKEN_LIMIT,
20
+ CODEX_MODEL_CONTEXT_WINDOW_TOKENS,
21
+ CODEX_MODEL_EFFECTIVE_CONTEXT_WINDOW_TOKENS,
22
+ CODEX_MODEL_ID_PREFIX,
23
+ CODEX_PROVIDER_BASE_URL,
24
+ CODEX_PROVIDER_ID,
25
+ } from "@opengeni/codex/constants";
26
+ import { createHash } from "node:crypto";
13
27
  import { z } from "zod";
14
28
 
15
29
  const envName = /^[A-Za-z_][A-Za-z0-9_]*$/;
@@ -90,7 +104,7 @@ export const DEFAULT_AGENT_INSTRUCTIONS = [
90
104
  "You are an OpenGeni workspace agent.",
91
105
  "Follow the user's task and any enabled pack or skill instructions for the current role.",
92
106
  "Work inside the sandbox workspace and use filesystem and shell tools when useful.",
93
- "Repository resources are mounted under repos/<owner>/<repo>.",
107
+ "Repository resources are mounted under repos/<host>/<owner>/<repo> unless the session specifies another collision-free mount path.",
94
108
  "File resources are mounted under files/<file-id>/ unless the session specifies another mount path.",
95
109
  "Attached files are mounted read-only; copy them before modifying.",
96
110
  "Bundled skills are under .agents/ and can include infrastructure, marketing, or other role-specific guidance.",
@@ -103,14 +117,59 @@ export const DEFAULT_AGENT_INSTRUCTIONS = [
103
117
 
104
118
  export const McpServerConnectionRefSchema = z
105
119
  .object({
106
- connectionId: z.string().uuid().optional(),
120
+ // Standalone ids are UUIDs; embedded hosts may use any stable opaque id.
121
+ connectionId: z.string().min(1).optional(),
122
+ provider: z.string().min(1).max(128).optional(),
107
123
  providerDomain: z.string().min(1),
108
124
  kind: z.enum(["oauth2", "api_key", "app_install", "delegated"]).optional(),
109
125
  scopes: z.array(z.string().min(1)).optional(),
110
126
  resource: z.string().min(1).optional(),
127
+ selectedResources: z
128
+ .array(
129
+ z
130
+ .object({
131
+ id: z.string().min(1).max(512),
132
+ kind: z.literal("repository"),
133
+ })
134
+ .strict(),
135
+ )
136
+ .min(1)
137
+ .max(256)
138
+ .superRefine((resources, context) => {
139
+ const seen = new Set<string>();
140
+ for (const [index, resource] of resources.entries()) {
141
+ const key = `${resource.kind}\0${resource.id}`;
142
+ if (seen.has(key)) {
143
+ context.addIssue({
144
+ code: "custom",
145
+ message: "selectedResources must not contain duplicates",
146
+ path: [index],
147
+ });
148
+ }
149
+ seen.add(key);
150
+ }
151
+ })
152
+ .optional(),
111
153
  subjectScope: z.enum(["workspace", "subject"]).optional(),
112
154
  })
113
- .strict();
155
+ .strict()
156
+ .superRefine((reference, context) => {
157
+ if (!reference.selectedResources) return;
158
+ if (!reference.connectionId) {
159
+ context.addIssue({
160
+ code: "custom",
161
+ message: "selectedResources requires connectionId",
162
+ path: ["connectionId"],
163
+ });
164
+ }
165
+ if (!reference.provider) {
166
+ context.addIssue({
167
+ code: "custom",
168
+ message: "selectedResources requires provider",
169
+ path: ["provider"],
170
+ });
171
+ }
172
+ });
114
173
  export type McpServerConnectionRef = z.infer<typeof McpServerConnectionRefSchema>;
115
174
 
116
175
  const SettingsSchema = z.object({
@@ -125,7 +184,7 @@ const SettingsSchema = z.object({
125
184
  // topology. Default "" → standalone: no search_path scoping, server default
126
185
  // (`public`). When set (e.g. "opengeni"), the db handle + the managed-auth
127
186
  // pool send `search_path = "<dbSchema>","opengeni_private","public"` so every
128
- // query resolves into the dedicated schema with NO query rewrite (SPIKE-1 F1).
187
+ // query resolves into the dedicated schema with NO query rewrite (schema-isolation contract F1).
129
188
  dbSchema: z.string().default(""),
130
189
  // Step I (§7.7). RLS posture. "force" (default) = today's FORCE-RLS via the
131
190
  // non-owner `opengeni_app` role. "scoped" = the embedded owner-role path (the
@@ -135,6 +194,12 @@ const SettingsSchema = z.object({
135
194
  temporalHost: z.string().default("127.0.0.1:7233"),
136
195
  temporalNamespace: z.string().default("default"),
137
196
  temporalTaskQueue: z.string().default("opengeni-runs-ts"),
197
+ temporalTlsEnabled: EnvBoolean.default(false),
198
+ temporalApiKey: z.string().optional(),
199
+ temporalTlsServerName: z.string().optional(),
200
+ temporalTlsRootCaCertificateBase64: z.string().optional(),
201
+ temporalTlsClientCertificateBase64: z.string().optional(),
202
+ temporalTlsClientPrivateKeyBase64: z.string().optional(),
138
203
  startupDependencyRetryAttempts: z.coerce.number().int().positive().default(30),
139
204
  startupDependencyRetryInitialDelayMs: z.coerce.number().int().positive().default(1000),
140
205
  startupDependencyRetryMaxDelayMs: z.coerce.number().int().positive().default(5000),
@@ -157,18 +222,28 @@ const SettingsSchema = z.object({
157
222
  staticEntitlementsJson: z.string().default("{}"),
158
223
  staticUsageLimitsJson: z.string().default("{}"),
159
224
  delegationSecret: z.string().optional(),
160
- // Sandbox-surfacing scoped stream-token HMAC secret (master-spine §C.3 / I8).
225
+ // sandbox workspace scoped stream-token HMAC secret (sandbox contract §C.3 / stream-token availability contract).
161
226
  // When unset, the API falls back to `delegationSecret` (the same HMAC envelope
162
227
  // family, `ogs_` vs `ogd_` prefix). REQUIRED-WHEN-DESKTOP, but the absence of
163
228
  // BOTH while sandboxDesktopEnabled=true is a GRACEFUL DEGRADE (DesktopStream
164
- // transport:null + a loud boot warning), NOT a hard boot-fail (I8/OD-8).
229
+ // transport:null + a loud boot warning), NOT a hard boot-fail (stream-token availability contract).
165
230
  streamTokenSecret: z.string().optional(),
166
231
  // The desktop input plane (raw stream:control writes) is OFF in v1: even a
167
232
  // holder of stream:control gets 403 until this flips. Keeps stream:control a
168
233
  // declared-but-inert permission so later hardening is a flag flip.
169
234
  streamControlEnabled: EnvBoolean.default(false),
235
+ // Existing-session explicit tool replacement is gated until every API and
236
+ // worker instance understands durable tools_provided provenance.
237
+ sessionTurnToolReplacementEnabled: EnvBoolean.default(false),
170
238
  toolspaceEnabled: EnvBoolean.default(false),
171
239
  toolspaceMaxCallsPerTurn: z.coerce.number().int().positive().default(200),
240
+ // Optional release-coherent bootstrap hint for custom rigs/connected machines
241
+ // that do not carry the stock-image ogtool binary. Exact stable versions only:
242
+ // the agent must never guess a tag or silently install `latest`.
243
+ ogtoolPackageSpec: z
244
+ .string()
245
+ .regex(/^@opengeni\/ogtool@(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)$/u)
246
+ .optional(),
172
247
  environmentsEncryptionKey: z.string().optional(),
173
248
  integrationsEnabled: EnvBoolean.default(false),
174
249
  integrationsStateSecret: z.string().optional(),
@@ -216,6 +291,10 @@ const SettingsSchema = z.object({
216
291
  // Model-catalog auto-compact limit. When present it is clamped to
217
292
  // 90% of the raw window, matching Codex core's auto_compact_token_limit().
218
293
  contextAutoCompactThresholdTokens: z.coerce.number().int().positive().optional(),
294
+ // Provider-neutral fallback for canonical model-facing tool-result text.
295
+ // The current stable Codex catalog policy is 10k tokens; the truncator adds
296
+ // Codex's 1.2x JSON serialization allowance when applying it.
297
+ modelToolOutputTruncationTokens: z.coerce.number().int().positive().default(10_000),
219
298
  authRequired: EnvBoolean.default(false),
220
299
  accessKey: z.string().optional(),
221
300
  authAllowHealth: EnvBoolean.default(true),
@@ -249,15 +328,11 @@ const SettingsSchema = z.object({
249
328
  // tool that BM25-discloses only the matching connectors. Default OFF — a codex
250
329
  // turn is byte-for-byte unchanged until enabled. OPENGENI_CODEX_TOOL_SEARCH_ENABLED
251
330
  codexToolSearchEnabled: EnvBoolean.default(false),
252
- // OPE-21 atomic, workspace-local credential allocation. Default OFF is a
331
+ // credential allocator atomic, workspace-local credential allocation. Default OFF is a
253
332
  // deliberate rolling-deploy fence: migrate + roll every worker first, then
254
333
  // enable. Turning it off restores the legacy sticky selector without a schema
255
334
  // rollback; the additive lease table/cursor columns become inert.
256
335
  codexCredentialLeasingEnabled: EnvBoolean.default(false),
257
- // Multi-account P3 (auto-rotation): an account is "near exhaustion" — ineligible to be
258
- // rotated TO — when EITHER usage window (5h/weekly) is at/over this percent. Default 90 to
259
- // match the UI danger flip (UsageBar danger at pct >= 90). OPENGENI_CODEX_ROTATION_NEAR_EXHAUSTION_PCT.
260
- codexRotationNearExhaustionPct: z.coerce.number().int().min(1).max(100).default(90),
261
336
  openaiReasoningEffort: ReasoningEffort.default("low"),
262
337
  openaiAllowedReasoningEfforts: z.string().default("low,medium,high,xhigh"),
263
338
  openaiResponsesTransport: z.enum(["http", "websocket"]).default("http"),
@@ -398,7 +473,7 @@ const SettingsSchema = z.object({
398
473
  // recordingMaxSeconds is the ffmpeg -t hard ceiling (bounds a multi-day turn).
399
474
  recordingEnabled: EnvBoolean.default(true),
400
475
  recordingDefaultCodec: z.enum(["h264-mp4", "vp9-webm"]).default("h264-mp4"),
401
- // Workbench v2 turn-end workspace capture (dossier §10.1). When on, the turn
476
+ // Workbench v2 turn-end workspace capture. When on, the turn
402
477
  // activity probes the box's changed files off the live box at turn end and
403
478
  // persists a capture revision (blobs in @opengeni/storage) so the workbench
404
479
  // paints cold/offline sessions with zero machine round-trips. Best-effort and
@@ -458,6 +533,15 @@ const SettingsSchema = z.object({
458
533
  // EnvBoolean (NOT z.coerce.boolean(), which would coerce "false" -> true and
459
534
  // turn the flag ON the moment anyone set the env var to disable it).
460
535
  sandboxOwnershipEnabled: EnvBoolean.default(false),
536
+ // --- standalone rig-verifier ownership rollout flag, default OFF ---
537
+ // Rig verification creates a throwaway provider sandbox outside the normal
538
+ // session-turn path. When enabled, that sandbox must first acquire the same
539
+ // durable lease lifecycle used by session boxes so the global orphan sweep
540
+ // recognizes its exact provider instance. Keep this separate from the general
541
+ // sandboxOwnershipEnabled rollout: every reaper worker must understand verifier
542
+ // leases before dispatch is enabled. When false the verifier fails closed before
543
+ // provider create; it never falls back to the legacy unowned path.
544
+ rigVerificationLeaseOwnershipEnabled: EnvBoolean.default(false),
461
545
  // --- lazy sandbox provisioning rollout flag, default OFF ---
462
546
  // Only effective when sandboxOwnershipEnabled is ALSO on (lazy provisioning is a
463
547
  // property of the owned path — the SDK never creates/resumes an injected session,
@@ -477,7 +561,7 @@ const SettingsSchema = z.object({
477
561
  // 404 (invisible — the surface does not exist for this deployment) and the
478
562
  // selfhosted backend is inert; boot is unaffected. EnvBoolean (NOT
479
563
  // z.coerce.boolean(), which coerces "false" -> true). Flipped per-environment via
480
- // the deploy-staging IaC secret/configmap pattern (dossier §17/§25.1).
564
+ // the deploy-staging IaC secret/configmap pattern.
481
565
  sandboxSelfhostedEnabled: EnvBoolean.default(false),
482
566
  // Gates the op-stream (streaming exec) transport to Connected Machines. The
483
567
  // runner must ALSO advertise Capabilities.op_stream; default off, and legacy
@@ -497,7 +581,7 @@ const SettingsSchema = z.object({
497
581
  selfhostedNatsUrl: z.string().optional(),
498
582
  selfhostedRelayUrl: z.string().optional(),
499
583
  // The HMAC secret the control plane signs the agent's relay PRODUCER token with
500
- // (the `ogr_` envelope threaded into EnrollmentCredentials.relayToken; M8b/dossier
584
+ // (the `ogr_` envelope threaded into EnrollmentCredentials.relayToken; M8b/design
501
585
  // §10.5). The relay verifies the producer token with the SAME secret. Optional:
502
586
  // when ABSENT the poll returns an empty relayToken (graceful degrade — the stream
503
587
  // plane is simply unavailable until configured). Falls back to streamTokenSecret /
@@ -507,7 +591,7 @@ const SettingsSchema = z.object({
507
591
  // The minisign PUBLIC key the agent pins for self-update verification (handed to
508
592
  // the agent in EnrollmentCredentials; the SECRET key lives only in CI).
509
593
  agentUpdatePublicKey: z.string().optional(),
510
- // --- NATS auth-callout tenancy boundary (bring-your-own-compute M-AUTH; dossier
594
+ // --- NATS auth-callout tenancy boundary (bring-your-own-compute M-AUTH; design
511
595
  // §10.1 NATS Accounts per workspace + §17 the isolation smoke) -------------
512
596
  // nats-server is configured with AUTH CALLOUT: an external agent connects
513
597
  // presenting its `oge_` enrollment bearer as the connect auth-token; the server
@@ -678,14 +762,8 @@ const SettingsSchema = z.object({
678
762
  allowedTools: z.array(z.string().min(1)).optional(),
679
763
  timeoutMs: z.number().int().positive().optional(),
680
764
  cacheToolsList: z.boolean().default(false),
681
- /**
682
- * Human-approval policy for this server's tools, overlaid per-run from a
683
- * session MCP server row (never from OPENGENI_MCP_SERVERS). `true` = all
684
- * tools require approval; a string[] = only the listed UNPREFIXED tool
685
- * names do; absent = auto-run (the historical default). Enforced in the
686
- * runtime by attaching `needsApproval` to the matching MCP tools.
687
- */
688
- requireApproval: z.union([z.boolean(), z.array(z.string().min(1))]).optional(),
765
+ /** Runtime approval policy, overlaid from an attempt-frozen session snapshot. */
766
+ requireApproval: SessionMcpApprovalPolicy.optional(),
689
767
  /**
690
768
  * Extra request headers sent to this MCP server (credential injection
691
769
  * for workspace-enabled capability MCPs). Populated at runtime from
@@ -701,12 +779,34 @@ const SettingsSchema = z.object({
701
779
 
702
780
  export type Settings = z.infer<typeof SettingsSchema>;
703
781
  export type McpServerConfig = Settings["mcpServers"][number];
782
+ export type TemporalTlsConnectionConfig = {
783
+ serverNameOverride?: string;
784
+ serverRootCACertificate?: Uint8Array;
785
+ clientCertPair?: {
786
+ crt: Uint8Array;
787
+ key: Uint8Array;
788
+ };
789
+ };
790
+ export type TemporalConnectionOptions = {
791
+ address: string;
792
+ tls?: true | TemporalTlsConnectionConfig;
793
+ apiKey?: string;
794
+ };
704
795
  export type ModelPricing = {
705
796
  inputMicrosPerMillionTokens: number;
706
797
  cachedInputMicrosPerMillionTokens?: number | undefined;
707
798
  outputMicrosPerMillionTokens: number;
708
799
  marginBps?: number | undefined;
709
800
  };
801
+ export type ModelPricingScheduleV1 = {
802
+ default: ModelPricing;
803
+ inputTokenTiers?:
804
+ | Array<{
805
+ minimumInputTokens: number;
806
+ pricing: ModelPricing;
807
+ }>
808
+ | undefined;
809
+ };
710
810
  export type ModelUsageInput = {
711
811
  inputTokens?: number | undefined;
712
812
  outputTokens?: number | undefined;
@@ -725,6 +825,164 @@ const ModelPricingSchema = z.object({
725
825
  marginBps: z.number().int().min(0).max(100_000).optional(),
726
826
  });
727
827
 
828
+ const ModelPricingScheduleSchema = z
829
+ .object({
830
+ default: ModelPricingSchema,
831
+ inputTokenTiers: z
832
+ .array(
833
+ z.object({
834
+ minimumInputTokens: z.number().int().nonnegative(),
835
+ pricing: ModelPricingSchema,
836
+ }),
837
+ )
838
+ .optional(),
839
+ })
840
+ .superRefine((schedule, ctx) => {
841
+ let previous = -1;
842
+ for (const [index, tier] of (schedule.inputTokenTiers ?? []).entries()) {
843
+ if (tier.minimumInputTokens <= previous) {
844
+ ctx.addIssue({
845
+ code: "custom",
846
+ path: ["inputTokenTiers", index, "minimumInputTokens"],
847
+ message: "input-token tier thresholds must be strictly increasing",
848
+ });
849
+ }
850
+ previous = tier.minimumInputTokens;
851
+ }
852
+ });
853
+
854
+ export const CapabilitySupportV1 = z.enum(["supported", "unsupported", "unknown"]);
855
+ export type CapabilitySupportV1 = z.infer<typeof CapabilitySupportV1>;
856
+
857
+ export const CapabilityStateV1Schema = z
858
+ .object({
859
+ upstream: CapabilitySupportV1,
860
+ runnable: z.boolean(),
861
+ })
862
+ .superRefine((state, ctx) => {
863
+ if (state.upstream === "unsupported" && state.runnable) {
864
+ ctx.addIssue({
865
+ code: "custom",
866
+ path: ["runnable"],
867
+ message: "an upstream-unsupported capability cannot be runnable",
868
+ });
869
+ }
870
+ });
871
+ export type CapabilityStateV1 = z.infer<typeof CapabilityStateV1Schema>;
872
+
873
+ const ModelModalityV1 = z.enum(["text", "image", "audio"]);
874
+ const ModelLatencyModeV1 = z.enum(["standard", "priority", "fast"]);
875
+
876
+ export const ModelCapabilitiesV1Schema = z
877
+ .object({
878
+ reasoning: CapabilityStateV1Schema.extend({
879
+ efforts: z.array(ReasoningEffort),
880
+ defaultEffort: ReasoningEffort.nullable(),
881
+ required: z.boolean(),
882
+ }),
883
+ functionCalling: CapabilityStateV1Schema,
884
+ structuredOutput: CapabilityStateV1Schema,
885
+ hostedTools: z.object({
886
+ webSearch: CapabilityStateV1Schema,
887
+ xSearch: CapabilityStateV1Schema,
888
+ codeExecution: CapabilityStateV1Schema,
889
+ }),
890
+ inputModalities: z.array(ModelModalityV1).min(1),
891
+ outputModalities: z.array(ModelModalityV1).min(1),
892
+ transports: z.object({
893
+ sse: CapabilityStateV1Schema,
894
+ responsesWebSocket: CapabilityStateV1Schema,
895
+ realtimeAudio: CapabilityStateV1Schema,
896
+ }),
897
+ latencyModes: z
898
+ .array(
899
+ z.object({
900
+ id: ModelLatencyModeV1,
901
+ upstream: CapabilitySupportV1,
902
+ runnable: z.boolean(),
903
+ billingMultiplierBps: z.number().int().positive().optional(),
904
+ }),
905
+ )
906
+ .min(1),
907
+ })
908
+ .superRefine((capabilities, ctx) => {
909
+ const efforts = new Set(capabilities.reasoning.efforts);
910
+ if (efforts.size !== capabilities.reasoning.efforts.length) {
911
+ ctx.addIssue({
912
+ code: "custom",
913
+ path: ["reasoning", "efforts"],
914
+ message: "reasoning efforts must be unique",
915
+ });
916
+ }
917
+ if (
918
+ capabilities.reasoning.defaultEffort !== null &&
919
+ !efforts.has(capabilities.reasoning.defaultEffort)
920
+ ) {
921
+ ctx.addIssue({
922
+ code: "custom",
923
+ path: ["reasoning", "defaultEffort"],
924
+ message: "the default reasoning effort must be one of the supported efforts",
925
+ });
926
+ }
927
+ if (capabilities.reasoning.runnable && capabilities.reasoning.efforts.length === 0) {
928
+ ctx.addIssue({
929
+ code: "custom",
930
+ path: ["reasoning", "efforts"],
931
+ message: "a runnable reasoning capability must declare at least one effort",
932
+ });
933
+ }
934
+ for (const field of ["inputModalities", "outputModalities"] as const) {
935
+ if (new Set(capabilities[field]).size !== capabilities[field].length) {
936
+ ctx.addIssue({
937
+ code: "custom",
938
+ path: [field],
939
+ message: `${field} must be unique`,
940
+ });
941
+ }
942
+ }
943
+ const latencyIds = new Set<string>();
944
+ for (const [index, mode] of capabilities.latencyModes.entries()) {
945
+ if (latencyIds.has(mode.id)) {
946
+ ctx.addIssue({
947
+ code: "custom",
948
+ path: ["latencyModes", index, "id"],
949
+ message: "latency mode ids must be unique",
950
+ });
951
+ }
952
+ latencyIds.add(mode.id);
953
+ if (mode.upstream === "unsupported" && mode.runnable) {
954
+ ctx.addIssue({
955
+ code: "custom",
956
+ path: ["latencyModes", index, "runnable"],
957
+ message: "an upstream-unsupported latency mode cannot be runnable",
958
+ });
959
+ }
960
+ }
961
+ });
962
+ export type ModelCapabilitiesV1 = z.infer<typeof ModelCapabilitiesV1Schema>;
963
+
964
+ export type ModelDeploymentV1 = {
965
+ upstreamModelId: string;
966
+ wireApi: ModelProviderApi;
967
+ };
968
+
969
+ export type ModelExecutionLimitsV1 = {
970
+ contextWindowTokens: number | null;
971
+ effectiveContextWindowTokens: number | null;
972
+ autoCompactTokenLimit: number | null;
973
+ toolOutputTruncationTokens: number | null;
974
+ };
975
+
976
+ export type CredentialSourceV1 =
977
+ | { kind: "deployment"; mechanism: "api_key" | "azure_ad_bearer" }
978
+ | { kind: "connected_subscription"; provider: "codex" }
979
+ | { kind: "workspace_connection"; mechanism: "api_key" };
980
+
981
+ export type BillingAttributionV1 = {
982
+ upstreamPayer: "deployment" | "workspace" | "connected_subscription";
983
+ metering: "opengeni_credits" | "external";
984
+ };
985
+
728
986
  /**
729
987
  * Wire API a provider speaks. The built-in OpenAI/Azure provider always uses
730
988
  * "responses" (the OpenAI Responses API). Extra registry providers default to
@@ -744,16 +1002,52 @@ export const RegistryProviderKind = z.enum(["api-key", "codex-subscription"]);
744
1002
  export type RegistryProviderKind = z.infer<typeof RegistryProviderKind>;
745
1003
 
746
1004
  /** A single model exposed by a registry provider. */
747
- const RegistryModelSchema = z.object({
748
- id: z.string().min(1), // model id sent to the provider, e.g. "accounts/fireworks/models/glm-5p2"
749
- label: z.string().min(1).optional(), // display name; defaults to id
750
- contextWindowTokens: z.number().int().positive().optional(),
751
- effectiveContextWindowTokens: z.number().int().positive().optional(),
752
- autoCompactTokenLimit: z.number().int().positive().optional(),
753
- reasoningEffort: z.boolean().optional(), // model accepts a reasoning-effort control
754
- hostedWebSearch: z.boolean().optional(), // provider executes the hosted web_search tool for this model
755
- pricing: ModelPricingSchema.optional(),
756
- });
1005
+ const RegistryModelSchema = z
1006
+ .object({
1007
+ id: z.string().min(1), // canonical OpenGeni product id
1008
+ upstreamModelId: z.string().min(1).optional(), // exact provider slug; defaults to id
1009
+ aliases: z.array(z.string().min(1)).optional(), // accepted input only; never sent upstream
1010
+ label: z.string().min(1).optional(), // display name; defaults to id
1011
+ contextWindowTokens: z.number().int().positive().optional(),
1012
+ effectiveContextWindowTokens: z.number().int().positive().optional(),
1013
+ autoCompactTokenLimit: z.number().int().positive().optional(),
1014
+ // Canonical model-facing function/tool-result policy. The runtime applies
1015
+ // the same 1.2x serialization allowance as Codex when materializing output.
1016
+ toolOutputTruncationTokens: z.number().int().positive().optional(),
1017
+ reasoningEffort: z.boolean().optional(), // legacy compatibility input/projection
1018
+ hostedWebSearch: z.boolean().optional(), // legacy compatibility input/projection
1019
+ capabilities: ModelCapabilitiesV1Schema.optional(),
1020
+ pricing: z.union([ModelPricingSchema, ModelPricingScheduleSchema]).optional(),
1021
+ // Reserved normalized contracts are derived by OpenGeni in V1. Generic
1022
+ // registry JSON must not opt itself into workspace BYOK or reattribute cost.
1023
+ credentialSource: z.never().optional(),
1024
+ billing: z.never().optional(),
1025
+ })
1026
+ .superRefine((model, ctx) => {
1027
+ if (
1028
+ model.capabilities &&
1029
+ model.reasoningEffort !== undefined &&
1030
+ model.reasoningEffort !== model.capabilities.reasoning.runnable
1031
+ ) {
1032
+ ctx.addIssue({
1033
+ code: "custom",
1034
+ path: ["reasoningEffort"],
1035
+ message: "legacy reasoningEffort must agree with capabilities.reasoning.runnable",
1036
+ });
1037
+ }
1038
+ if (
1039
+ model.capabilities &&
1040
+ model.hostedWebSearch !== undefined &&
1041
+ model.hostedWebSearch !== model.capabilities.hostedTools.webSearch.runnable
1042
+ ) {
1043
+ ctx.addIssue({
1044
+ code: "custom",
1045
+ path: ["hostedWebSearch"],
1046
+ message:
1047
+ "legacy hostedWebSearch must agree with capabilities.hostedTools.webSearch.runnable",
1048
+ });
1049
+ }
1050
+ });
757
1051
 
758
1052
  /** A non-built-in provider declared by the host via OPENGENI_MODEL_PROVIDERS_JSON. */
759
1053
  const RegistryProviderSchema = z.object({
@@ -766,6 +1060,12 @@ const RegistryProviderSchema = z.object({
766
1060
  apiKeyEnv: z.string().optional(), // ... OR name of the env var holding the key (preferred)
767
1061
  defaultQuery: z.record(z.string(), z.string()).optional(),
768
1062
  defaultHeaders: z.record(z.string(), z.string()).optional(),
1063
+ publicDefaultQueryNames: z.array(z.string().min(1)).optional(),
1064
+ publicDefaultHeaderNames: z.array(z.string().min(1)).optional(),
1065
+ // V1 derives these from provider kind. Workspace BYOK is deliberately not a
1066
+ // registry switch and requires a separately reviewed encrypted broker.
1067
+ credentialSource: z.never().optional(),
1068
+ billing: z.never().optional(),
769
1069
  models: z.array(RegistryModelSchema).min(1),
770
1070
  });
771
1071
  export type RegistryProvider = z.infer<typeof RegistryProviderSchema>;
@@ -796,18 +1096,33 @@ export interface ResolvedModelProvider {
796
1096
  apiKey?: string | undefined;
797
1097
  defaultQuery?: Record<string, string> | undefined;
798
1098
  defaultHeaders?: Record<string, string> | undefined;
1099
+ publicDefaultQueryNames?: string[] | undefined;
1100
+ publicDefaultHeaderNames?: string[] | undefined;
1101
+ credentialSource: CredentialSourceV1;
1102
+ billing: BillingAttributionV1;
799
1103
  }
800
1104
 
801
1105
  /** A single exposed model + the provider that serves it. */
802
1106
  export interface ConfiguredModel {
1107
+ schemaVersion: 1;
803
1108
  id: string;
1109
+ aliases: string[];
804
1110
  label: string;
805
1111
  providerId: string;
806
1112
  providerLabel: string;
807
1113
  api: ModelProviderApi;
1114
+ upstreamModelId: string;
1115
+ deployment: ModelDeploymentV1;
1116
+ executionLimits: ModelExecutionLimitsV1;
1117
+ credentialSource: CredentialSourceV1;
1118
+ billing: BillingAttributionV1;
1119
+ capabilities: ModelCapabilitiesV1;
1120
+ pricing?: ModelPricingScheduleV1 | undefined;
1121
+ definitionVersion: string;
808
1122
  contextWindowTokens?: number | undefined;
809
1123
  effectiveContextWindowTokens?: number | undefined;
810
1124
  autoCompactTokenLimit?: number | undefined;
1125
+ toolOutputTruncationTokens?: number | undefined;
811
1126
  reasoningEffort: boolean;
812
1127
  hostedWebSearch: boolean;
813
1128
  }
@@ -971,6 +1286,14 @@ export function getSettings(): Settings {
971
1286
  temporalHost: optional("OPENGENI_TEMPORAL_HOST"),
972
1287
  temporalNamespace: optional("OPENGENI_TEMPORAL_NAMESPACE"),
973
1288
  temporalTaskQueue: optional("OPENGENI_TEMPORAL_TASK_QUEUE"),
1289
+ temporalTlsEnabled: optional("OPENGENI_TEMPORAL_TLS_ENABLED"),
1290
+ temporalApiKey: optional("OPENGENI_TEMPORAL_API_KEY"),
1291
+ temporalTlsServerName: optional("OPENGENI_TEMPORAL_TLS_SERVER_NAME"),
1292
+ temporalTlsRootCaCertificateBase64: optional(
1293
+ "OPENGENI_TEMPORAL_TLS_ROOT_CA_CERTIFICATE_BASE64",
1294
+ ),
1295
+ temporalTlsClientCertificateBase64: optional("OPENGENI_TEMPORAL_TLS_CLIENT_CERTIFICATE_BASE64"),
1296
+ temporalTlsClientPrivateKeyBase64: optional("OPENGENI_TEMPORAL_TLS_CLIENT_PRIVATE_KEY_BASE64"),
974
1297
  startupDependencyRetryAttempts: optional("OPENGENI_STARTUP_DEPENDENCY_RETRY_ATTEMPTS"),
975
1298
  startupDependencyRetryInitialDelayMs: optional(
976
1299
  "OPENGENI_STARTUP_DEPENDENCY_RETRY_INITIAL_DELAY_MS",
@@ -993,8 +1316,10 @@ export function getSettings(): Settings {
993
1316
  delegationSecret: optional("OPENGENI_DELEGATION_SECRET"),
994
1317
  streamTokenSecret: optional("OPENGENI_STREAM_TOKEN_SECRET"),
995
1318
  streamControlEnabled: optional("OPENGENI_STREAM_CONTROL_ENABLED"),
1319
+ sessionTurnToolReplacementEnabled: optional("OPENGENI_SESSION_TURN_TOOL_REPLACEMENT_ENABLED"),
996
1320
  toolspaceEnabled: optional("OPENGENI_TOOLSPACE_ENABLED"),
997
1321
  toolspaceMaxCallsPerTurn: optional("OPENGENI_TOOLSPACE_MAX_CALLS_PER_TURN"),
1322
+ ogtoolPackageSpec: optional("OPENGENI_OGTOOL_PACKAGE_SPEC"),
998
1323
  environmentsEncryptionKey: optional("OPENGENI_ENVIRONMENTS_ENCRYPTION_KEY"),
999
1324
  integrationsEnabled: optional("OPENGENI_INTEGRATIONS_ENABLED"),
1000
1325
  integrationsStateSecret: optional("OPENGENI_INTEGRATIONS_STATE_SECRET"),
@@ -1010,6 +1335,7 @@ export function getSettings(): Settings {
1010
1335
  contextCompactionThresholdRatio: optional("OPENGENI_COMPACTION_THRESHOLD_RATIO"),
1011
1336
  contextReservedOutputTokens: optional("OPENGENI_CONTEXT_RESERVED_OUTPUT_TOKENS"),
1012
1337
  contextAutoCompactThresholdTokens: optional("OPENGENI_CONTEXT_AUTO_COMPACT_THRESHOLD_TOKENS"),
1338
+ modelToolOutputTruncationTokens: optional("OPENGENI_MODEL_TOOL_OUTPUT_TRUNCATION_TOKENS"),
1013
1339
  authRequired: optional("OPENGENI_AUTH_REQUIRED"),
1014
1340
  accessKey: optional("OPENGENI_ACCESS_KEY"),
1015
1341
  authAllowHealth: optional("OPENGENI_AUTH_ALLOW_HEALTH"),
@@ -1030,7 +1356,6 @@ export function getSettings(): Settings {
1030
1356
  codexToolSearchEnabled: optional("OPENGENI_CODEX_TOOL_SEARCH_ENABLED"),
1031
1357
  codexCredentialLeasingEnabled: optional("OPENGENI_CODEX_CREDENTIAL_LEASING_ENABLED"),
1032
1358
  codexProductSku: optional("OPENGENI_CODEX_PRODUCT_SKU"),
1033
- codexRotationNearExhaustionPct: optional("OPENGENI_CODEX_ROTATION_NEAR_EXHAUSTION_PCT"),
1034
1359
  openaiReasoningEffort: optional("OPENGENI_OPENAI_REASONING_EFFORT"),
1035
1360
  openaiAllowedReasoningEfforts: optional("OPENGENI_OPENAI_ALLOWED_REASONING_EFFORTS"),
1036
1361
  openaiResponsesTransport: optional("OPENGENI_OPENAI_RESPONSES_TRANSPORT"),
@@ -1108,6 +1433,9 @@ export function getSettings(): Settings {
1108
1433
  vercelTeamId: optional("OPENGENI_VERCEL_TEAM_ID"),
1109
1434
  vercelRuntime: optional("OPENGENI_VERCEL_RUNTIME"),
1110
1435
  sandboxOwnershipEnabled: optional("OPENGENI_SANDBOX_OWNERSHIP_ENABLED"),
1436
+ rigVerificationLeaseOwnershipEnabled: optional(
1437
+ "OPENGENI_RIG_VERIFICATION_LEASE_OWNERSHIP_ENABLED",
1438
+ ),
1111
1439
  sandboxLazyProvisionEnabled: optional("OPENGENI_SANDBOX_LAZY_PROVISION"),
1112
1440
  sandboxSelfhostedEnabled: optional("OPENGENI_SANDBOX_SELFHOSTED_ENABLED"),
1113
1441
  agentOpStreamEnabled: optional("OPENGENI_AGENT_OP_STREAM_ENABLED"),
@@ -1247,6 +1575,353 @@ export function resolveProviderApiKey(
1247
1575
  return undefined;
1248
1576
  }
1249
1577
 
1578
+ const HTTP_FIELD_NAME = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
1579
+ const CREDENTIAL_LIKE_NAME_PARTS = new Set([
1580
+ "apikey",
1581
+ "auth",
1582
+ "authorization",
1583
+ "bearer",
1584
+ "credential",
1585
+ "cookie",
1586
+ "key",
1587
+ "password",
1588
+ "secret",
1589
+ "session",
1590
+ "signature",
1591
+ "token",
1592
+ ]);
1593
+ const REASONING_EFFORT_ORDER = new Map(
1594
+ ReasoningEffort.options.map((effort, index) => [effort, index]),
1595
+ );
1596
+ const MODALITY_ORDER = new Map(["text", "image", "audio"].map((value, index) => [value, index]));
1597
+ const LATENCY_MODE_ORDER = new Map(
1598
+ ["standard", "priority", "fast"].map((value, index) => [value, index]),
1599
+ );
1600
+
1601
+ function normalizeRegistryBaseUrl(value: string, providerId: string): string {
1602
+ const url = new URL(value);
1603
+ if (url.username || url.password) {
1604
+ throw new Error(`provider ${providerId} baseUrl must not contain userinfo`);
1605
+ }
1606
+ if (url.search) {
1607
+ throw new Error(
1608
+ `provider ${providerId} baseUrl must not contain a query; move query entries to defaultQuery`,
1609
+ );
1610
+ }
1611
+ if (url.hash) {
1612
+ throw new Error(`provider ${providerId} baseUrl must not contain a fragment`);
1613
+ }
1614
+ return url.toString();
1615
+ }
1616
+
1617
+ function isCredentialLikeMetadataName(name: string): boolean {
1618
+ return name
1619
+ .toLowerCase()
1620
+ .split(/[-_.]/u)
1621
+ .some((part) => CREDENTIAL_LIKE_NAME_PARTS.has(part));
1622
+ }
1623
+
1624
+ function normalizeHeaderMap(
1625
+ providerId: string,
1626
+ headers: Record<string, string> | undefined,
1627
+ ): Record<string, string> | undefined {
1628
+ if (!headers) {
1629
+ return undefined;
1630
+ }
1631
+ const normalized: Record<string, string> = {};
1632
+ const rawByNormalized = new Map<string, string>();
1633
+ for (const [rawName, value] of Object.entries(headers)) {
1634
+ if (!HTTP_FIELD_NAME.test(rawName)) {
1635
+ throw new Error(
1636
+ `provider ${providerId} defaultHeaders contains invalid HTTP field name ${JSON.stringify(rawName)}`,
1637
+ );
1638
+ }
1639
+ const name = rawName.toLowerCase();
1640
+ const previous = rawByNormalized.get(name);
1641
+ if (previous !== undefined) {
1642
+ throw new Error(
1643
+ `provider ${providerId} defaultHeaders names ${JSON.stringify(previous)} and ${JSON.stringify(rawName)} collide after lowercase normalization`,
1644
+ );
1645
+ }
1646
+ if (name === "authorization") {
1647
+ throw new Error(
1648
+ `provider ${providerId} defaultHeaders must not override SDK-managed Authorization`,
1649
+ );
1650
+ }
1651
+ rawByNormalized.set(name, rawName);
1652
+ normalized[name] = value;
1653
+ }
1654
+ return normalized;
1655
+ }
1656
+
1657
+ function normalizePublicHeaderNames(
1658
+ providerId: string,
1659
+ names: string[] | undefined,
1660
+ headers: Record<string, string> | undefined,
1661
+ ): string[] | undefined {
1662
+ if (!names) {
1663
+ return undefined;
1664
+ }
1665
+ const normalized: string[] = [];
1666
+ const seen = new Set<string>();
1667
+ for (const rawName of names) {
1668
+ if (!HTTP_FIELD_NAME.test(rawName)) {
1669
+ throw new Error(
1670
+ `provider ${providerId} publicDefaultHeaderNames contains invalid HTTP field name ${JSON.stringify(rawName)}`,
1671
+ );
1672
+ }
1673
+ const name = rawName.toLowerCase();
1674
+ if (seen.has(name)) {
1675
+ throw new Error(
1676
+ `provider ${providerId} publicDefaultHeaderNames contains duplicate normalized name ${JSON.stringify(name)}`,
1677
+ );
1678
+ }
1679
+ if (!(name in (headers ?? {}))) {
1680
+ throw new Error(
1681
+ `provider ${providerId} publicDefaultHeaderNames declares absent defaultHeaders entry ${JSON.stringify(name)}`,
1682
+ );
1683
+ }
1684
+ if (isCredentialLikeMetadataName(name)) {
1685
+ throw new Error(
1686
+ `provider ${providerId} publicDefaultHeaderNames cannot classify credential-like name ${JSON.stringify(name)} as public`,
1687
+ );
1688
+ }
1689
+ seen.add(name);
1690
+ normalized.push(name);
1691
+ }
1692
+ return normalized;
1693
+ }
1694
+
1695
+ function normalizeQueryMap(
1696
+ providerId: string,
1697
+ query: Record<string, string> | undefined,
1698
+ ): Record<string, string> | undefined {
1699
+ if (!query) {
1700
+ return undefined;
1701
+ }
1702
+ for (const name of Object.keys(query)) {
1703
+ if (!name) {
1704
+ throw new Error(`provider ${providerId} defaultQuery contains an empty name`);
1705
+ }
1706
+ }
1707
+ return { ...query };
1708
+ }
1709
+
1710
+ function normalizePublicQueryNames(
1711
+ providerId: string,
1712
+ names: string[] | undefined,
1713
+ query: Record<string, string> | undefined,
1714
+ ): string[] | undefined {
1715
+ if (!names) {
1716
+ return undefined;
1717
+ }
1718
+ const seen = new Set<string>();
1719
+ for (const name of names) {
1720
+ if (seen.has(name)) {
1721
+ throw new Error(
1722
+ `provider ${providerId} publicDefaultQueryNames contains duplicate name ${JSON.stringify(name)}`,
1723
+ );
1724
+ }
1725
+ if (!(name in (query ?? {}))) {
1726
+ throw new Error(
1727
+ `provider ${providerId} publicDefaultQueryNames declares absent defaultQuery entry ${JSON.stringify(name)}`,
1728
+ );
1729
+ }
1730
+ if (isCredentialLikeMetadataName(name)) {
1731
+ throw new Error(
1732
+ `provider ${providerId} publicDefaultQueryNames cannot classify credential-like name ${JSON.stringify(name)} as public`,
1733
+ );
1734
+ }
1735
+ seen.add(name);
1736
+ }
1737
+ return [...names];
1738
+ }
1739
+
1740
+ function normalizeRegistryProvider(provider: RegistryProvider): RegistryProvider {
1741
+ const defaultHeaders = normalizeHeaderMap(provider.id, provider.defaultHeaders);
1742
+ const defaultQuery = normalizeQueryMap(provider.id, provider.defaultQuery);
1743
+ return {
1744
+ ...provider,
1745
+ baseUrl: normalizeRegistryBaseUrl(provider.baseUrl, provider.id),
1746
+ ...(defaultHeaders === undefined ? {} : { defaultHeaders }),
1747
+ ...(defaultQuery === undefined ? {} : { defaultQuery }),
1748
+ ...(provider.publicDefaultHeaderNames === undefined
1749
+ ? {}
1750
+ : {
1751
+ publicDefaultHeaderNames: normalizePublicHeaderNames(
1752
+ provider.id,
1753
+ provider.publicDefaultHeaderNames,
1754
+ defaultHeaders,
1755
+ ),
1756
+ }),
1757
+ ...(provider.publicDefaultQueryNames === undefined
1758
+ ? {}
1759
+ : {
1760
+ publicDefaultQueryNames: normalizePublicQueryNames(
1761
+ provider.id,
1762
+ provider.publicDefaultQueryNames,
1763
+ defaultQuery,
1764
+ ),
1765
+ }),
1766
+ };
1767
+ }
1768
+
1769
+ function normalizeModelPricingSchedule(
1770
+ pricing: ModelPricing | ModelPricingScheduleV1,
1771
+ ): ModelPricingScheduleV1 {
1772
+ return "default" in pricing ? pricing : { default: pricing };
1773
+ }
1774
+
1775
+ function normalizeCapabilities(capabilities: ModelCapabilitiesV1): ModelCapabilitiesV1 {
1776
+ const parsed = ModelCapabilitiesV1Schema.parse(capabilities);
1777
+ return {
1778
+ ...parsed,
1779
+ reasoning: {
1780
+ ...parsed.reasoning,
1781
+ efforts: [...parsed.reasoning.efforts].sort(
1782
+ (left, right) =>
1783
+ (REASONING_EFFORT_ORDER.get(left) ?? 0) - (REASONING_EFFORT_ORDER.get(right) ?? 0),
1784
+ ),
1785
+ },
1786
+ inputModalities: [...parsed.inputModalities].sort(
1787
+ (left, right) => (MODALITY_ORDER.get(left) ?? 0) - (MODALITY_ORDER.get(right) ?? 0),
1788
+ ),
1789
+ outputModalities: [...parsed.outputModalities].sort(
1790
+ (left, right) => (MODALITY_ORDER.get(left) ?? 0) - (MODALITY_ORDER.get(right) ?? 0),
1791
+ ),
1792
+ latencyModes: [...parsed.latencyModes].sort(
1793
+ (left, right) =>
1794
+ (LATENCY_MODE_ORDER.get(left.id) ?? 0) - (LATENCY_MODE_ORDER.get(right.id) ?? 0),
1795
+ ),
1796
+ };
1797
+ }
1798
+
1799
+ function legacyModelCapabilities(
1800
+ settings: Settings,
1801
+ input: { reasoningEffort: boolean; hostedWebSearch: boolean },
1802
+ ): ModelCapabilitiesV1 {
1803
+ const reasoningEfforts = input.reasoningEffort ? configuredAllowedReasoningEfforts(settings) : [];
1804
+ return normalizeCapabilities({
1805
+ reasoning: {
1806
+ upstream: input.reasoningEffort ? "supported" : "unknown",
1807
+ runnable: input.reasoningEffort,
1808
+ efforts: reasoningEfforts,
1809
+ defaultEffort: input.reasoningEffort ? settings.openaiReasoningEffort : null,
1810
+ required: false,
1811
+ },
1812
+ functionCalling: { upstream: "unknown", runnable: true },
1813
+ structuredOutput: { upstream: "unknown", runnable: false },
1814
+ hostedTools: {
1815
+ webSearch: {
1816
+ upstream: input.hostedWebSearch ? "supported" : "unknown",
1817
+ runnable: input.hostedWebSearch,
1818
+ },
1819
+ xSearch: { upstream: "unknown", runnable: false },
1820
+ codeExecution: { upstream: "unknown", runnable: false },
1821
+ },
1822
+ inputModalities: ["text"],
1823
+ outputModalities: ["text"],
1824
+ transports: {
1825
+ sse: { upstream: "unknown", runnable: true },
1826
+ responsesWebSocket: { upstream: "unknown", runnable: false },
1827
+ realtimeAudio: { upstream: "unknown", runnable: false },
1828
+ },
1829
+ latencyModes: [{ id: "standard", upstream: "unknown", runnable: true }],
1830
+ });
1831
+ }
1832
+
1833
+ function registryCredentialSource(provider: RegistryProvider): CredentialSourceV1 {
1834
+ return provider.kind === "codex-subscription"
1835
+ ? { kind: "connected_subscription", provider: "codex" }
1836
+ : { kind: "deployment", mechanism: "api_key" };
1837
+ }
1838
+
1839
+ function registryBilling(provider: RegistryProvider): BillingAttributionV1 {
1840
+ return provider.kind === "codex-subscription"
1841
+ ? { upstreamPayer: "connected_subscription", metering: "external" }
1842
+ : { upstreamPayer: "deployment", metering: "opengeni_credits" };
1843
+ }
1844
+
1845
+ function builtinCredentialSource(settings: Settings): CredentialSourceV1 {
1846
+ if (settings.openaiProvider === "azure" && !settings.azureOpenaiApiKey) {
1847
+ return { kind: "deployment", mechanism: "azure_ad_bearer" };
1848
+ }
1849
+ return { kind: "deployment", mechanism: "api_key" };
1850
+ }
1851
+
1852
+ function staticRequestMetadataForDigest(provider: ResolvedModelProvider): {
1853
+ headers: Array<{ name: string; classification: "public" | "secret"; value?: string }>;
1854
+ query: Array<{ name: string; classification: "public" | "secret"; value?: string }>;
1855
+ } {
1856
+ const publicHeaders = new Set(provider.publicDefaultHeaderNames ?? []);
1857
+ const publicQuery = new Set(provider.publicDefaultQueryNames ?? []);
1858
+ return {
1859
+ headers: Object.entries(provider.defaultHeaders ?? {})
1860
+ .sort(([left], [right]) => left.localeCompare(right))
1861
+ .map(([name, value]) =>
1862
+ publicHeaders.has(name)
1863
+ ? { name, classification: "public" as const, value }
1864
+ : { name, classification: "secret" as const },
1865
+ ),
1866
+ query: Object.entries(provider.defaultQuery ?? {})
1867
+ .sort(([left], [right]) => left.localeCompare(right))
1868
+ .map(([name, value]) =>
1869
+ publicQuery.has(name)
1870
+ ? { name, classification: "public" as const, value }
1871
+ : { name, classification: "secret" as const },
1872
+ ),
1873
+ };
1874
+ }
1875
+
1876
+ function canonicalJson(value: unknown): string {
1877
+ const normalize = (input: unknown): unknown => {
1878
+ if (Array.isArray(input)) {
1879
+ return input.map((entry) => normalize(entry));
1880
+ }
1881
+ if (input && typeof input === "object") {
1882
+ const out: Record<string, unknown> = {};
1883
+ for (const key of Object.keys(input).sort()) {
1884
+ const child = (input as Record<string, unknown>)[key];
1885
+ if (child !== undefined) {
1886
+ out[key] = normalize(child);
1887
+ }
1888
+ }
1889
+ return out;
1890
+ }
1891
+ return input;
1892
+ };
1893
+ return JSON.stringify(normalize(value));
1894
+ }
1895
+
1896
+ function definitionVersionFor(
1897
+ model: Omit<ConfiguredModel, "definitionVersion">,
1898
+ provider: ResolvedModelProvider,
1899
+ ): string {
1900
+ const requestMetadata = staticRequestMetadataForDigest(provider);
1901
+ const digestInput = canonicalJson({
1902
+ schemaVersion: model.schemaVersion,
1903
+ id: model.id,
1904
+ providerId: model.providerId,
1905
+ deployment: model.deployment,
1906
+ provider: {
1907
+ adapterKind: provider.kind,
1908
+ wireApi: provider.api,
1909
+ baseUrl: provider.baseUrl ?? null,
1910
+ defaultHeaders: requestMetadata.headers,
1911
+ defaultQuery: requestMetadata.query,
1912
+ },
1913
+ credentialSource: model.credentialSource,
1914
+ billing: model.billing,
1915
+ executionLimits: model.executionLimits,
1916
+ capabilities: model.capabilities,
1917
+ pricing: model.pricing ?? null,
1918
+ });
1919
+ return `sha256:${createHash("sha256")
1920
+ .update("opengeni:model-definition:v1\n", "utf8")
1921
+ .update(digestInput, "utf8")
1922
+ .digest("hex")}`;
1923
+ }
1924
+
1250
1925
  /**
1251
1926
  * The built-in provider's stable id: "openai" on the OpenAI platform, "azure"
1252
1927
  * on Azure. Exported because the workspace model-policy gate must attribute
@@ -1271,18 +1946,24 @@ function builtinProviderLabel(settings: Pick<Settings, "openaiProvider">): strin
1271
1946
  * id — validateSettings rejects that at boot.
1272
1947
  */
1273
1948
  export function configuredProviders(settings: Settings): ResolvedModelProvider[] {
1949
+ const credentialSource = builtinCredentialSource(settings);
1274
1950
  const builtin: ResolvedModelProvider = {
1275
1951
  id: builtinProviderId(settings),
1276
1952
  label: builtinProviderLabel(settings),
1277
1953
  kind: "api-key",
1278
1954
  api: "responses",
1279
1955
  builtin: true,
1956
+ credentialSource,
1957
+ billing: { upstreamPayer: "deployment", metering: "opengeni_credits" },
1280
1958
  };
1281
1959
  if (settings.openaiProvider === "azure") {
1282
- builtin.baseUrl = settings.azureOpenaiBaseUrl ?? settings.azureOpenaiEndpoint;
1960
+ const baseUrl = settings.azureOpenaiBaseUrl ?? settings.azureOpenaiEndpoint;
1961
+ builtin.baseUrl = baseUrl ? normalizeRegistryBaseUrl(baseUrl, builtin.id) : undefined;
1283
1962
  builtin.apiKey = settings.azureOpenaiApiKey ?? settings.azureOpenaiAdToken;
1284
1963
  } else {
1285
- builtin.baseUrl = settings.openaiBaseUrl;
1964
+ builtin.baseUrl = settings.openaiBaseUrl
1965
+ ? normalizeRegistryBaseUrl(settings.openaiBaseUrl, builtin.id)
1966
+ : undefined;
1286
1967
  builtin.apiKey = settings.openaiApiKey;
1287
1968
  }
1288
1969
  const registry = parseModelProvidersJson(settings.modelProvidersJson).map(
@@ -1296,11 +1977,46 @@ export function configuredProviders(settings: Settings): ResolvedModelProvider[]
1296
1977
  apiKey: resolveProviderApiKey(provider),
1297
1978
  defaultQuery: provider.defaultQuery,
1298
1979
  defaultHeaders: provider.defaultHeaders,
1980
+ publicDefaultQueryNames: provider.publicDefaultQueryNames,
1981
+ publicDefaultHeaderNames: provider.publicDefaultHeaderNames,
1982
+ credentialSource: registryCredentialSource(provider),
1983
+ billing: registryBilling(provider),
1299
1984
  }),
1300
1985
  );
1301
1986
  return [builtin, ...registry];
1302
1987
  }
1303
1988
 
1989
+ /**
1990
+ * Pure catalog overlay for a workspace whose existing Codex connection seam
1991
+ * reports ready. This describes product/provider identity only; it does not
1992
+ * select, lease, refresh, or expose a concrete credential; those runtime
1993
+ * operations remain owned by the credential allocator.
1994
+ */
1995
+ export function withCodexCatalogProvider(settings: Settings): Settings {
1996
+ const providers = parseModelProvidersJson(settings.modelProvidersJson);
1997
+ if (providers.some((provider) => provider.id === CODEX_PROVIDER_ID)) {
1998
+ return settings;
1999
+ }
2000
+ const provider: RegistryProvider = {
2001
+ kind: "codex-subscription",
2002
+ id: CODEX_PROVIDER_ID,
2003
+ label: "Codex (ChatGPT subscription)",
2004
+ api: "responses",
2005
+ baseUrl: CODEX_PROVIDER_BASE_URL,
2006
+ models: CODEX_FALLBACK_MODEL_SLUGS.map((slug) => ({
2007
+ id: `${CODEX_MODEL_ID_PREFIX}${slug}`,
2008
+ upstreamModelId: slug,
2009
+ label: slug,
2010
+ reasoningEffort: true,
2011
+ contextWindowTokens: CODEX_MODEL_CONTEXT_WINDOW_TOKENS,
2012
+ effectiveContextWindowTokens: CODEX_MODEL_EFFECTIVE_CONTEXT_WINDOW_TOKENS,
2013
+ autoCompactTokenLimit: CODEX_MODEL_AUTO_COMPACT_TOKEN_LIMIT,
2014
+ toolOutputTruncationTokens: CODEX_MODEL_TOOL_OUTPUT_TRUNCATION_TOKENS,
2015
+ })),
2016
+ };
2017
+ return { ...settings, modelProvidersJson: JSON.stringify([...providers, provider]) };
2018
+ }
2019
+
1304
2020
  /**
1305
2021
  * The provider identity a model id resolves to, for workspace model-policy
1306
2022
  * evaluation — MUST agree with the real router (resolveTurnModel /
@@ -1315,13 +2031,83 @@ export function configuredProviders(settings: Settings): ResolvedModelProvider[]
1315
2031
  * serves. A policy blocking the built-in must block this path too.
1316
2032
  */
1317
2033
  export function policyProviderIdForModel(settings: Settings, modelId: string): string {
1318
- if (modelId.startsWith(CODEX_MODEL_ID_PREFIX)) {
2034
+ const canonicalModelId = canonicalizeConfiguredModelId(settings, modelId);
2035
+ if (canonicalModelId.startsWith(CODEX_MODEL_ID_PREFIX)) {
1319
2036
  return CODEX_PROVIDER_ID;
1320
2037
  }
1321
- const configured = configuredModels(settings).find((model) => model.id === modelId);
2038
+ const configured = configuredModels(settings).find((model) => model.id === canonicalModelId);
1322
2039
  return configured?.providerId ?? builtinProviderId(settings);
1323
2040
  }
1324
2041
 
2042
+ function resolvedExecutionLimits(
2043
+ settings: Settings,
2044
+ model: {
2045
+ contextWindowTokens?: number | undefined;
2046
+ effectiveContextWindowTokens?: number | undefined;
2047
+ autoCompactTokenLimit?: number | undefined;
2048
+ toolOutputTruncationTokens?: number | undefined;
2049
+ },
2050
+ ): ModelExecutionLimitsV1 {
2051
+ return {
2052
+ contextWindowTokens: model.contextWindowTokens ?? settings.contextWindowTokens,
2053
+ effectiveContextWindowTokens:
2054
+ model.effectiveContextWindowTokens ?? settings.contextEffectiveWindowTokens ?? null,
2055
+ autoCompactTokenLimit:
2056
+ model.autoCompactTokenLimit ?? settings.contextAutoCompactThresholdTokens ?? null,
2057
+ toolOutputTruncationTokens:
2058
+ model.toolOutputTruncationTokens ?? settings.modelToolOutputTruncationTokens ?? null,
2059
+ };
2060
+ }
2061
+
2062
+ function finalizeConfiguredModel(
2063
+ settings: Settings,
2064
+ provider: ResolvedModelProvider,
2065
+ input: Omit<ConfiguredModel, "schemaVersion" | "definitionVersion" | "executionLimits">,
2066
+ ): ConfiguredModel {
2067
+ const modelWithoutVersion: Omit<ConfiguredModel, "definitionVersion"> = {
2068
+ schemaVersion: 1,
2069
+ ...input,
2070
+ executionLimits: resolvedExecutionLimits(settings, input),
2071
+ };
2072
+ return {
2073
+ ...modelWithoutVersion,
2074
+ definitionVersion: definitionVersionFor(modelWithoutVersion, provider),
2075
+ };
2076
+ }
2077
+
2078
+ function assertUniqueModelIdentities(models: ConfiguredModel[]): void {
2079
+ const canonicalOwners = new Map<string, string>();
2080
+ for (const model of models) {
2081
+ const previous = canonicalOwners.get(model.id);
2082
+ if (previous !== undefined) {
2083
+ throw new Error(
2084
+ `OPENGENI_MODEL_PROVIDERS_JSON model id ${JSON.stringify(model.id)} is declared by both ${previous} and ${model.providerId}`,
2085
+ );
2086
+ }
2087
+ canonicalOwners.set(model.id, model.providerId);
2088
+ }
2089
+
2090
+ const acceptedInputs = new Map(canonicalOwners);
2091
+ for (const model of models) {
2092
+ const ownAliases = new Set<string>();
2093
+ for (const alias of model.aliases) {
2094
+ if (ownAliases.has(alias)) {
2095
+ throw new Error(
2096
+ `OPENGENI_MODEL_PROVIDERS_JSON model ${JSON.stringify(model.id)} contains duplicate alias ${JSON.stringify(alias)}`,
2097
+ );
2098
+ }
2099
+ ownAliases.add(alias);
2100
+ const previous = acceptedInputs.get(alias);
2101
+ if (previous !== undefined) {
2102
+ throw new Error(
2103
+ `OPENGENI_MODEL_PROVIDERS_JSON alias ${JSON.stringify(alias)} for model ${JSON.stringify(model.id)} collides with model/provider ${previous}`,
2104
+ );
2105
+ }
2106
+ acceptedInputs.set(alias, model.id);
2107
+ }
2108
+ }
2109
+ }
2110
+
1325
2111
  /**
1326
2112
  * Every model a client may use, the built-in provider's models first
1327
2113
  * (configuredAllowedModels-from-openai, mapped to "responses" with
@@ -1333,6 +2119,9 @@ export function policyProviderIdForModel(settings: Settings, modelId: string): s
1333
2119
  export function configuredModels(settings: Settings): ConfiguredModel[] {
1334
2120
  const builtinId = builtinProviderId(settings);
1335
2121
  const builtinLabel = builtinProviderLabel(settings);
2122
+ const providers = configuredProviders(settings);
2123
+ const providerById = new Map(providers.map((provider) => [provider.id, provider]));
2124
+ const pricingSchedules = configuredModelPricingSchedules(settings);
1336
2125
  // The built-in (OpenAI/Azure) provider must NEVER claim a registry-namespaced
1337
2126
  // model id. The worker overwrites settings.openaiModel with the turn's model
1338
2127
  // (apps/worker agent-turn runSettings) — including a `codex/<slug>` id, or a
@@ -1349,59 +2138,110 @@ export function configuredModels(settings: Settings): ConfiguredModel[] {
1349
2138
  // a codex/ id has NO codex provider injected (no active subscription) it then
1350
2139
  // resolves to nothing and getModel fails loud with
1351
2140
  // CodexSubscriptionUnavailableError instead of mis-routing to Azure.
2141
+ const parsedRegistry = parseModelProvidersJson(settings.modelProvidersJson);
1352
2142
  const registryOwnedIds = new Set(
1353
- parseModelProvidersJson(settings.modelProvidersJson).flatMap((provider) =>
1354
- provider.models.map((model) => model.id),
1355
- ),
2143
+ parsedRegistry.flatMap((provider) => provider.models.map((model) => model.id)),
2144
+ );
2145
+ const registryAliases = new Set(
2146
+ parsedRegistry.flatMap((provider) => provider.models.flatMap((model) => model.aliases ?? [])),
1356
2147
  );
1357
2148
  const isRegistryNamespaced = (id: string): boolean =>
1358
- id.startsWith(CODEX_MODEL_ID_PREFIX) || (id.includes("/") && registryOwnedIds.has(id));
2149
+ id.startsWith(CODEX_MODEL_ID_PREFIX) ||
2150
+ registryAliases.has(id) ||
2151
+ (id.includes("/") && registryOwnedIds.has(id));
2152
+ const builtinProvider = providerById.get(builtinId);
2153
+ if (!builtinProvider) {
2154
+ throw new Error(`Built-in model provider ${builtinId} is not configured`);
2155
+ }
1359
2156
  const out: ConfiguredModel[] = uniqueValues([
1360
2157
  settings.openaiModel,
1361
2158
  ...splitCsv(settings.openaiAllowedModels),
1362
2159
  ])
1363
2160
  .filter((id) => !isRegistryNamespaced(id))
1364
- .map((id) => ({
1365
- id,
1366
- label: id,
1367
- providerId: builtinId,
1368
- providerLabel: builtinLabel,
1369
- api: "responses" as const,
1370
- contextWindowTokens: settings.contextWindowTokens,
1371
- reasoningEffort: true,
1372
- hostedWebSearch: settings.webSearchEnabled,
1373
- }));
1374
- for (const provider of parseModelProvidersJson(settings.modelProvidersJson)) {
2161
+ .map((id) => {
2162
+ const capabilities = legacyModelCapabilities(settings, {
2163
+ reasoningEffort: true,
2164
+ hostedWebSearch: settings.webSearchEnabled,
2165
+ });
2166
+ return finalizeConfiguredModel(settings, builtinProvider, {
2167
+ id,
2168
+ aliases: [],
2169
+ label: id,
2170
+ providerId: builtinId,
2171
+ providerLabel: builtinLabel,
2172
+ api: "responses" as const,
2173
+ upstreamModelId: id,
2174
+ deployment: { upstreamModelId: id, wireApi: "responses" },
2175
+ credentialSource: builtinProvider.credentialSource,
2176
+ billing: builtinProvider.billing,
2177
+ capabilities,
2178
+ ...(pricingSchedules[id] === undefined ? {} : { pricing: pricingSchedules[id] }),
2179
+ contextWindowTokens: settings.contextWindowTokens,
2180
+ toolOutputTruncationTokens: settings.modelToolOutputTruncationTokens,
2181
+ reasoningEffort: capabilities.reasoning.runnable,
2182
+ hostedWebSearch: capabilities.hostedTools.webSearch.runnable,
2183
+ });
2184
+ });
2185
+ for (const provider of parsedRegistry) {
1375
2186
  const providerLabel = provider.label ?? provider.id;
2187
+ const resolvedProvider = providerById.get(provider.id);
2188
+ if (!resolvedProvider) {
2189
+ throw new Error(`Registry model provider ${provider.id} is not configured`);
2190
+ }
1376
2191
  for (const model of provider.models) {
1377
- out.push({
1378
- id: model.id,
1379
- label: model.label ?? model.id,
1380
- providerId: provider.id,
1381
- providerLabel,
1382
- api: provider.api,
1383
- ...(model.contextWindowTokens === undefined
1384
- ? {}
1385
- : { contextWindowTokens: model.contextWindowTokens }),
1386
- ...(model.effectiveContextWindowTokens === undefined
1387
- ? {}
1388
- : { effectiveContextWindowTokens: model.effectiveContextWindowTokens }),
1389
- ...(model.autoCompactTokenLimit === undefined
1390
- ? {}
1391
- : { autoCompactTokenLimit: model.autoCompactTokenLimit }),
1392
- reasoningEffort: model.reasoningEffort ?? false,
1393
- hostedWebSearch: model.hostedWebSearch ?? false,
1394
- });
2192
+ const capabilities = model.capabilities
2193
+ ? normalizeCapabilities(model.capabilities)
2194
+ : legacyModelCapabilities(settings, {
2195
+ reasoningEffort: model.reasoningEffort ?? false,
2196
+ hostedWebSearch: model.hostedWebSearch ?? false,
2197
+ });
2198
+ const upstreamModelId = model.upstreamModelId ?? model.id;
2199
+ out.push(
2200
+ finalizeConfiguredModel(settings, resolvedProvider, {
2201
+ id: model.id,
2202
+ aliases: [...(model.aliases ?? [])],
2203
+ label: model.label ?? model.id,
2204
+ providerId: provider.id,
2205
+ providerLabel,
2206
+ api: provider.api,
2207
+ upstreamModelId,
2208
+ deployment: { upstreamModelId, wireApi: provider.api },
2209
+ credentialSource: resolvedProvider.credentialSource,
2210
+ billing: resolvedProvider.billing,
2211
+ capabilities,
2212
+ ...(pricingSchedules[model.id] === undefined
2213
+ ? {}
2214
+ : { pricing: pricingSchedules[model.id] }),
2215
+ ...(model.contextWindowTokens === undefined
2216
+ ? {}
2217
+ : { contextWindowTokens: model.contextWindowTokens }),
2218
+ ...(model.effectiveContextWindowTokens === undefined
2219
+ ? {}
2220
+ : { effectiveContextWindowTokens: model.effectiveContextWindowTokens }),
2221
+ ...(model.autoCompactTokenLimit === undefined
2222
+ ? {}
2223
+ : { autoCompactTokenLimit: model.autoCompactTokenLimit }),
2224
+ ...(model.toolOutputTruncationTokens === undefined
2225
+ ? {}
2226
+ : { toolOutputTruncationTokens: model.toolOutputTruncationTokens }),
2227
+ reasoningEffort: capabilities.reasoning.runnable,
2228
+ hostedWebSearch: capabilities.hostedTools.webSearch.runnable,
2229
+ }),
2230
+ );
1395
2231
  }
1396
2232
  }
1397
- const seen = new Set<string>();
1398
- return out.filter((model) => {
1399
- if (seen.has(model.id)) {
1400
- return false;
1401
- }
1402
- seen.add(model.id);
1403
- return true;
1404
- });
2233
+ assertUniqueModelIdentities(out);
2234
+ return out;
2235
+ }
2236
+
2237
+ /** Resolve a known canonical id or alias. Unknown strings are returned unchanged. */
2238
+ export function canonicalizeConfiguredModelId(settings: Settings, modelId: string): string {
2239
+ const models = configuredModels(settings);
2240
+ const canonical = models.find((model) => model.id === modelId);
2241
+ if (canonical) {
2242
+ return canonical.id;
2243
+ }
2244
+ return models.find((model) => model.aliases.includes(modelId))?.id ?? modelId;
1405
2245
  }
1406
2246
 
1407
2247
  /**
@@ -1425,7 +2265,8 @@ export function resolveModelProvider(
1425
2265
  settings: Settings,
1426
2266
  modelId: string,
1427
2267
  ): { provider: ResolvedModelProvider; model: ConfiguredModel } | undefined {
1428
- const model = configuredModels(settings).find((candidate) => candidate.id === modelId);
2268
+ const canonicalModelId = canonicalizeConfiguredModelId(settings, modelId);
2269
+ const model = configuredModels(settings).find((candidate) => candidate.id === canonicalModelId);
1429
2270
  if (!model) {
1430
2271
  return undefined;
1431
2272
  }
@@ -1438,28 +2279,169 @@ export function resolveModelProvider(
1438
2279
  return { provider, model };
1439
2280
  }
1440
2281
 
2282
+ export type ResolveTurnExecutionPolicyV1Input = {
2283
+ /** Effective persisted turn model. Aliases are accepted and canonicalized. */
2284
+ modelId: string;
2285
+ /** Exact caller-supplied input before canonicalization, only for explicit switches. */
2286
+ requestedModelId: string | null;
2287
+ modelSource: TurnExecutionModelSourceV1;
2288
+ reasoningEffort: Settings["openaiReasoningEffort"];
2289
+ reasoningSource: TurnExecutionReasoningSourceV1;
2290
+ };
2291
+
2292
+ function settingsForTurnExecutionPolicy(settings: Settings, modelId: string): Settings {
2293
+ return settings.codexSubscriptionEnabled && modelId.startsWith(CODEX_MODEL_ID_PREFIX)
2294
+ ? withCodexCatalogProvider(settings)
2295
+ : settings;
2296
+ }
2297
+
1441
2298
  /**
1442
- * Effective per-model pricing. Merge order (later wins):
1443
- * defaultModelPricing registry model `pricing` entries (keyed by model id)
1444
- * parseModelPricingJson(settings.modelPricingJson) (explicit JSON wins).
2299
+ * Build a trusted, secret-safe execution policy from the normalized catalog.
2300
+ * The Codex overlay here contains static product/provider identity only; it
2301
+ * neither proves readiness nor chooses, decrypts, leases, or exposes an account.
1445
2302
  */
1446
- export function configuredModelPricing(settings: Settings): Record<string, ModelPricing> {
1447
- const registry: Record<string, ModelPricing> = {};
2303
+ export function resolveTurnExecutionPolicyV1(
2304
+ settings: Settings,
2305
+ input: ResolveTurnExecutionPolicyV1Input,
2306
+ ): TurnExecutionPolicyV1 {
2307
+ const catalogSettings = settingsForTurnExecutionPolicy(settings, input.modelId);
2308
+ const productModelId = canonicalizeConfiguredModelId(catalogSettings, input.modelId);
2309
+ const resolved = resolveModelProvider(catalogSettings, productModelId);
2310
+ if (!resolved) {
2311
+ throw new Error("Turn execution policy model is not present in the configured catalog");
2312
+ }
2313
+ if (
2314
+ input.requestedModelId !== null &&
2315
+ canonicalizeConfiguredModelId(catalogSettings, input.requestedModelId) !== productModelId
2316
+ ) {
2317
+ throw new Error("Turn execution policy requested model does not canonicalize to its product");
2318
+ }
2319
+ return TurnExecutionPolicyV1.parse({
2320
+ schemaVersion: 1,
2321
+ productModelId,
2322
+ requestedModelId: input.requestedModelId,
2323
+ modelSource: input.modelSource,
2324
+ reasoningEffort: input.reasoningEffort,
2325
+ reasoningSource: input.reasoningSource,
2326
+ providerId: resolved.provider.id,
2327
+ upstreamModelId: resolved.model.upstreamModelId,
2328
+ wireApi: resolved.model.api,
2329
+ credentialSource: resolved.model.credentialSource,
2330
+ billing: resolved.model.billing,
2331
+ definitionVersion: resolved.model.definitionVersion,
2332
+ });
2333
+ }
2334
+
2335
+ /**
2336
+ * Parse-time validation lives in @opengeni/contracts; this verifier binds a
2337
+ * present snapshot to the current executable definition and exact turn row.
2338
+ * Any deployment/provider drift fails before a provider or compaction call.
2339
+ */
2340
+ export function assertTurnExecutionPolicyMatchesConfigV1(
2341
+ settings: Settings,
2342
+ policy: TurnExecutionPolicyV1,
2343
+ expected: {
2344
+ modelId: string;
2345
+ reasoningEffort: Settings["openaiReasoningEffort"];
2346
+ },
2347
+ ): {
2348
+ policy: TurnExecutionPolicyV1;
2349
+ provider: ResolvedModelProvider;
2350
+ model: ConfiguredModel;
2351
+ } {
2352
+ const parsed = TurnExecutionPolicyV1.parse(policy);
2353
+ const catalogSettings = settingsForTurnExecutionPolicy(settings, parsed.productModelId);
2354
+ const canonicalExpectedModel = canonicalizeConfiguredModelId(catalogSettings, expected.modelId);
2355
+ if (
2356
+ parsed.productModelId !== canonicalExpectedModel ||
2357
+ parsed.reasoningEffort !== expected.reasoningEffort
2358
+ ) {
2359
+ throw new Error("Turn execution policy does not match the accepted turn model/reasoning");
2360
+ }
2361
+ if (
2362
+ parsed.requestedModelId !== null &&
2363
+ canonicalizeConfiguredModelId(catalogSettings, parsed.requestedModelId) !==
2364
+ parsed.productModelId
2365
+ ) {
2366
+ throw new Error("Turn execution policy requested model does not match its product model");
2367
+ }
2368
+ const resolved = resolveModelProvider(catalogSettings, parsed.productModelId);
2369
+ if (!resolved) {
2370
+ throw new Error("Turn execution policy model is no longer configured");
2371
+ }
2372
+ const mismatched =
2373
+ parsed.providerId !== resolved.provider.id ||
2374
+ parsed.upstreamModelId !== resolved.model.upstreamModelId ||
2375
+ parsed.wireApi !== resolved.model.api ||
2376
+ parsed.definitionVersion !== resolved.model.definitionVersion ||
2377
+ canonicalJson(parsed.credentialSource) !== canonicalJson(resolved.model.credentialSource) ||
2378
+ canonicalJson(parsed.billing) !== canonicalJson(resolved.model.billing);
2379
+ if (mismatched) {
2380
+ throw new Error("Turn execution policy does not match the current provider definition");
2381
+ }
2382
+ return { policy: parsed, provider: resolved.provider, model: resolved.model };
2383
+ }
2384
+
2385
+ /**
2386
+ * Effective per-model pricing schedules. Merge order (later wins): built-in
2387
+ * flat defaults → registry model flat/scheduled pricing → explicit legacy flat
2388
+ * OPENGENI_MODEL_PRICING_JSON. The explicit legacy map intentionally replaces
2389
+ * a registry schedule with one flat default so its historical precedence stays
2390
+ * exact.
2391
+ */
2392
+ export function configuredModelPricingSchedules(
2393
+ settings: Settings,
2394
+ ): Record<string, ModelPricingScheduleV1> {
2395
+ const defaults = Object.fromEntries(
2396
+ Object.entries(defaultModelPricing).map(([model, pricing]) => [model, { default: pricing }]),
2397
+ );
2398
+ const registry: Record<string, ModelPricingScheduleV1> = {};
1448
2399
  for (const provider of parseModelProvidersJson(settings.modelProvidersJson)) {
1449
2400
  for (const model of provider.models) {
1450
2401
  if (model.pricing) {
1451
- registry[model.id] = model.pricing;
2402
+ registry[model.id] = normalizeModelPricingSchedule(model.pricing);
1452
2403
  }
1453
2404
  }
1454
2405
  }
1455
- const configured = parseModelPricingJson(settings.modelPricingJson);
2406
+ const configured = Object.fromEntries(
2407
+ Object.entries(parseModelPricingJson(settings.modelPricingJson)).map(([model, pricing]) => [
2408
+ model,
2409
+ { default: pricing },
2410
+ ]),
2411
+ );
1456
2412
  return {
1457
- ...defaultModelPricing,
2413
+ ...defaults,
1458
2414
  ...registry,
1459
2415
  ...configured,
1460
2416
  };
1461
2417
  }
1462
2418
 
2419
+ /** Legacy flat projection: returns the default/below-threshold price. */
2420
+ export function configuredModelPricing(settings: Settings): Record<string, ModelPricing> {
2421
+ return Object.fromEntries(
2422
+ Object.entries(configuredModelPricingSchedules(settings)).map(([model, schedule]) => [
2423
+ model,
2424
+ schedule.default,
2425
+ ]),
2426
+ );
2427
+ }
2428
+
2429
+ /** Select the per-provider-request price at an exact input-token threshold. */
2430
+ export function selectModelPricing(
2431
+ schedule: ModelPricingScheduleV1,
2432
+ inputTokens: number,
2433
+ ): ModelPricing {
2434
+ const normalizedInputTokens = Math.max(0, Math.floor(inputTokens));
2435
+ let selected = schedule.default;
2436
+ for (const tier of schedule.inputTokenTiers ?? []) {
2437
+ if (normalizedInputTokens < tier.minimumInputTokens) {
2438
+ break;
2439
+ }
2440
+ selected = tier.pricing;
2441
+ }
2442
+ return selected;
2443
+ }
2444
+
1463
2445
  /**
1464
2446
  * Usable input-token budget: an explicit model-catalog effective window when
1465
2447
  * available, otherwise the deployment window minus its output reserve.
@@ -1485,7 +2467,10 @@ export function settingsWithResolvedModelContext(
1485
2467
  settings: Settings,
1486
2468
  model: Pick<
1487
2469
  ConfiguredModel,
1488
- "contextWindowTokens" | "effectiveContextWindowTokens" | "autoCompactTokenLimit"
2470
+ | "contextWindowTokens"
2471
+ | "effectiveContextWindowTokens"
2472
+ | "autoCompactTokenLimit"
2473
+ | "toolOutputTruncationTokens"
1489
2474
  >,
1490
2475
  ): Settings {
1491
2476
  const contextWindowTokens = model.contextWindowTokens ?? settings.contextWindowTokens;
@@ -1503,6 +2488,9 @@ export function settingsWithResolvedModelContext(
1503
2488
  ...(model.autoCompactTokenLimit === undefined
1504
2489
  ? {}
1505
2490
  : { contextAutoCompactThresholdTokens: model.autoCompactTokenLimit }),
2491
+ ...(model.toolOutputTruncationTokens === undefined
2492
+ ? {}
2493
+ : { modelToolOutputTruncationTokens: model.toolOutputTruncationTokens }),
1506
2494
  };
1507
2495
  }
1508
2496
 
@@ -1533,17 +2521,28 @@ export function calculateModelUsageCostMicros(
1533
2521
  model: string,
1534
2522
  usage: ModelUsageInput,
1535
2523
  ): number {
1536
- const pricing = configuredModelPricing(settings)[model];
1537
- if (!pricing) {
2524
+ const schedule = configuredModelPricingSchedules(settings)[model];
2525
+ if (!schedule) {
1538
2526
  throw new Error(`Missing model pricing for ${model}`);
1539
2527
  }
1540
2528
  const entries =
1541
2529
  usage.requestUsageEntries && usage.requestUsageEntries.length > 0
1542
2530
  ? usage.requestUsageEntries
1543
2531
  : [usage];
1544
- const rawCost = entries.reduce((sum, entry) => sum + calculateEntryCostMicros(pricing, entry), 0);
1545
- const marginBps = pricing.marginBps ?? 0;
1546
- return Math.ceil((rawCost * (10_000 + marginBps)) / 10_000);
2532
+ const rawCostByPricing = new Map<ModelPricing, number>();
2533
+ for (const entry of entries) {
2534
+ const pricing = selectModelPricing(schedule, positiveInt(entry.inputTokens));
2535
+ rawCostByPricing.set(
2536
+ pricing,
2537
+ (rawCostByPricing.get(pricing) ?? 0) + calculateEntryCostMicros(pricing, entry),
2538
+ );
2539
+ }
2540
+ let total = 0;
2541
+ for (const [pricing, rawCost] of rawCostByPricing) {
2542
+ const marginBps = pricing.marginBps ?? 0;
2543
+ total += Math.ceil((rawCost * (10_000 + marginBps)) / 10_000);
2544
+ }
2545
+ return total;
1547
2546
  }
1548
2547
 
1549
2548
  export function configuredAllowedReasoningEfforts(
@@ -1573,6 +2572,77 @@ export function environmentsEncryptionKeyBytes(settings: Settings): Uint8Array |
1573
2572
  return new Uint8Array(decoded);
1574
2573
  }
1575
2574
 
2575
+ /**
2576
+ * Build one structurally compatible connection policy for both
2577
+ * `@temporalio/client` and `@temporalio/worker`. An API key or any custom TLS
2578
+ * material enables TLS automatically; the explicit flag covers server-auth TLS
2579
+ * without credentials. Secret values are never included in validation errors.
2580
+ */
2581
+ export function temporalConnectionOptions(settings: Settings): TemporalConnectionOptions {
2582
+ const apiKey = settings.temporalApiKey?.trim() || undefined;
2583
+ const serverNameOverride = settings.temporalTlsServerName?.trim() || undefined;
2584
+ const rootCa = decodeTemporalTlsMaterial(
2585
+ settings.temporalTlsRootCaCertificateBase64,
2586
+ "OPENGENI_TEMPORAL_TLS_ROOT_CA_CERTIFICATE_BASE64",
2587
+ );
2588
+ const clientCertificate = decodeTemporalTlsMaterial(
2589
+ settings.temporalTlsClientCertificateBase64,
2590
+ "OPENGENI_TEMPORAL_TLS_CLIENT_CERTIFICATE_BASE64",
2591
+ );
2592
+ const clientPrivateKey = decodeTemporalTlsMaterial(
2593
+ settings.temporalTlsClientPrivateKeyBase64,
2594
+ "OPENGENI_TEMPORAL_TLS_CLIENT_PRIVATE_KEY_BASE64",
2595
+ );
2596
+
2597
+ if (Boolean(clientCertificate) !== Boolean(clientPrivateKey)) {
2598
+ throw new Error(
2599
+ "OPENGENI_TEMPORAL_TLS_CLIENT_CERTIFICATE_BASE64 and " +
2600
+ "OPENGENI_TEMPORAL_TLS_CLIENT_PRIVATE_KEY_BASE64 must both be set or both omitted",
2601
+ );
2602
+ }
2603
+
2604
+ const tls: TemporalTlsConnectionConfig = {};
2605
+ if (serverNameOverride) {
2606
+ tls.serverNameOverride = serverNameOverride;
2607
+ }
2608
+ if (rootCa) {
2609
+ tls.serverRootCACertificate = rootCa;
2610
+ }
2611
+ if (clientCertificate && clientPrivateKey) {
2612
+ tls.clientCertPair = { crt: clientCertificate, key: clientPrivateKey };
2613
+ }
2614
+ const hasCustomTls = Object.keys(tls).length > 0;
2615
+ const tlsEnabled = settings.temporalTlsEnabled || Boolean(apiKey) || hasCustomTls;
2616
+
2617
+ return {
2618
+ address: settings.temporalHost,
2619
+ ...(tlsEnabled ? { tls: hasCustomTls ? tls : true } : {}),
2620
+ ...(apiKey ? { apiKey } : {}),
2621
+ };
2622
+ }
2623
+
2624
+ function decodeTemporalTlsMaterial(
2625
+ value: string | undefined,
2626
+ settingName: string,
2627
+ ): Uint8Array | undefined {
2628
+ // RFC 2045 base64 commonly arrives wrapped at 76 columns. Kubernetes
2629
+ // stringData and external secret stores preserve those line breaks, so
2630
+ // normalize whitespace before applying the strict alphabet/canonical check.
2631
+ const encoded = value?.replace(/\s/g, "");
2632
+ if (!encoded) {
2633
+ return undefined;
2634
+ }
2635
+ if (!/^[A-Za-z0-9+/]+={0,2}$/.test(encoded) || encoded.length % 4 === 1) {
2636
+ throw new Error(`${settingName} must contain valid base64`);
2637
+ }
2638
+ const decoded = Buffer.from(encoded, "base64");
2639
+ const canonical = decoded.toString("base64").replace(/=+$/, "");
2640
+ if (decoded.length === 0 || canonical !== encoded.replace(/=+$/, "")) {
2641
+ throw new Error(`${settingName} must contain valid base64`);
2642
+ }
2643
+ return new Uint8Array(decoded);
2644
+ }
2645
+
1576
2646
  /**
1577
2647
  * The connection `search_path` for OpenGeni's db handles + the managed-auth pool
1578
2648
  * (Step I, §7.8 runtime half). Returns `undefined` when `dbSchema` is unset
@@ -1580,7 +2650,7 @@ export function environmentsEncryptionKeyBytes(settings: Settings): Uint8Array |
1580
2650
  * default (`public`) applies — byte-for-byte today's behavior. When `dbSchema`
1581
2651
  * is set (embedded), returns `"<schema>,opengeni_private,public"` — `public`
1582
2652
  * stays LAST so `gen_random_uuid()` (pgcrypto) and the `vector` type still
1583
- * resolve (the SPIKE-1 live footgun). `opengeni_private` is on the path so the
2653
+ * resolve (the schema-isolation contract live footgun). `opengeni_private` is on the path so the
1584
2654
  * RLS GUC-reader helpers resolve when referenced unqualified.
1585
2655
  */
1586
2656
  export function dbSearchPath(settings: Pick<Settings, "dbSchema">): string | undefined {
@@ -1683,6 +2753,9 @@ export function stableSandboxEnvironmentForRun(
1683
2753
  }
1684
2754
  if (settings.toolspaceEnabled) {
1685
2755
  environment.OPENGENI_TOOLSPACE_TOKEN_FILE ??= `${environment.HOME ?? descriptor.workspaceRoot}/.opengeni/toolspace-token`;
2756
+ if (settings.ogtoolPackageSpec) {
2757
+ environment.OPENGENI_OGTOOL_PACKAGE_SPEC ??= settings.ogtoolPackageSpec;
2758
+ }
1686
2759
  if (options.workspaceId) {
1687
2760
  environment.OPENGENI_TOOLSPACE_URL ??= firstPartyMcpWorkspaceUrl(
1688
2761
  settings,
@@ -1983,7 +3056,14 @@ export function parseModelProvidersJson(raw: string): RegistryProvider[] {
1983
3056
  `OPENGENI_MODEL_PROVIDERS_JSON provider[${index}] is invalid: ${result.error.message}`,
1984
3057
  );
1985
3058
  }
1986
- return result.data;
3059
+ try {
3060
+ return normalizeRegistryProvider(result.data);
3061
+ } catch (error) {
3062
+ const message = error instanceof Error ? error.message : String(error);
3063
+ throw new Error(`OPENGENI_MODEL_PROVIDERS_JSON provider[${index}] is invalid: ${message}`, {
3064
+ cause: error,
3065
+ });
3066
+ }
1987
3067
  });
1988
3068
  }
1989
3069
 
@@ -2196,6 +3276,7 @@ function firstPartyDocumentsMcpServerUrl(mcpUrl: string): string {
2196
3276
  }
2197
3277
 
2198
3278
  function validateSettings(settings: Settings): void {
3279
+ temporalConnectionOptions(settings);
2199
3280
  if (settings.toolspaceEnabled && !settings.delegationSecret) {
2200
3281
  throw new Error("OPENGENI_DELEGATION_SECRET is required when OPENGENI_TOOLSPACE_ENABLED=true");
2201
3282
  }
@@ -2507,7 +3588,7 @@ function validateSettings(settings: Settings): void {
2507
3588
  );
2508
3589
  }
2509
3590
  }
2510
- // --- stream-token secret: required-when-desktop, but GRACEFULLY DEGRADE (I8) ---
3591
+ // --- stream-token secret: required-when-desktop, but GRACEFULLY DEGRADE (stream-token availability contract) ---
2511
3592
  // The desktop pixel plane needs an HMAC secret to mint scoped stream tokens.
2512
3593
  // It is REQUIRED when desktop is enabled — but per OD-8 a missing secret is NOT
2513
3594
  // a hard boot-fail: we emit a LOUD warning and the deployment ships with
@@ -2551,10 +3632,14 @@ function validateSettings(settings: Settings): void {
2551
3632
  );
2552
3633
  }
2553
3634
  }
3635
+ // Materialize the normalized catalog at boot so canonical product ids,
3636
+ // aliases, definition digests, and capability/pricing normalization are
3637
+ // validated even when managed billing is disabled.
3638
+ configuredModels(settings);
2554
3639
  }
2555
3640
 
2556
3641
  /**
2557
- * Resolve the secret used to sign/verify scoped stream tokens (master-spine
3642
+ * Resolve the secret used to sign/verify scoped stream tokens (sandbox contract
2558
3643
  * §C.3). Falls back to `delegationSecret` (the same HMAC envelope family —
2559
3644
  * `ogs_` vs `ogd_` prefix) so a deployment that already carries a delegation
2560
3645
  * secret does not need a second one. Returns undefined when neither is set,
@@ -2571,7 +3656,7 @@ export function resolveStreamTokenSecret(settings: Settings): string | undefined
2571
3656
 
2572
3657
  /**
2573
3658
  * True iff the desktop pixel plane must GRACEFULLY DEGRADE because desktop is
2574
- * enabled but no stream-token secret is resolvable (I8/OD-8). When true,
3659
+ * enabled but no stream-token secret is resolvable (stream-token availability contract). When true,
2575
3660
  * negotiateCapabilities forces DesktopStream.transport:null.
2576
3661
  */
2577
3662
  export function streamTokenDegraded(settings: Settings): boolean {
@@ -2580,7 +3665,7 @@ export function streamTokenDegraded(settings: Settings): boolean {
2580
3665
 
2581
3666
  /**
2582
3667
  * Resolve the secret the control plane signs the enrollment bearer credential
2583
- * with (the `oge_` envelope the agent presents back — M5/dossier §10.2). Falls
3668
+ * with (the `oge_` envelope the agent presents back — M5). Falls
2584
3669
  * back to `delegationSecret` (the same HMAC envelope family) so a deployment that
2585
3670
  * already carries a delegation secret needs no second one. Returns undefined when
2586
3671
  * neither is set; when selfhosted is enabled but this is undefined, the poll route
@@ -2598,7 +3683,7 @@ export function resolveEnrollmentSigningSecret(settings: Settings): string | und
2598
3683
 
2599
3684
  /**
2600
3685
  * Resolve the HMAC secret the control plane signs the agent's relay PRODUCER token
2601
- * with (the `ogr_` envelope; M8b/dossier §10.5). The RELAY verifies the producer
3686
+ * with (the `ogr_` envelope; M8b). The RELAY verifies the producer
2602
3687
  * token with the SAME secret (injected into the relay via env). Prefers an explicit
2603
3688
  * `selfhostedRelayTokenSecret`, then the `streamTokenSecret` (the relay already
2604
3689
  * needs that one to verify the viewer's `ogs_` token, so a single secret can back