@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/dist/index.d.ts +422 -15
- package/dist/index.js +877 -80
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/index.ts +1187 -102
package/src/index.ts
CHANGED
|
@@ -6,10 +6,24 @@ import {
|
|
|
6
6
|
ProductAccessMode,
|
|
7
7
|
ReasoningEffort,
|
|
8
8
|
SandboxBackend,
|
|
9
|
+
SessionMcpApprovalPolicy,
|
|
9
10
|
StaticUsageLimits,
|
|
11
|
+
TurnExecutionPolicyV1,
|
|
10
12
|
UsageLimitsMode,
|
|
13
|
+
type TurnExecutionModelSourceV1,
|
|
14
|
+
type TurnExecutionReasoningSourceV1,
|
|
11
15
|
} from "@opengeni/contracts";
|
|
12
|
-
import {
|
|
16
|
+
import { CODEX_MODEL_TOOL_OUTPUT_TRUNCATION_TOKENS } from "@opengeni/codex";
|
|
17
|
+
import {
|
|
18
|
+
CODEX_FALLBACK_MODEL_SLUGS,
|
|
19
|
+
CODEX_MODEL_AUTO_COMPACT_TOKEN_LIMIT,
|
|
20
|
+
CODEX_MODEL_CONTEXT_WINDOW_TOKENS,
|
|
21
|
+
CODEX_MODEL_EFFECTIVE_CONTEXT_WINDOW_TOKENS,
|
|
22
|
+
CODEX_MODEL_ID_PREFIX,
|
|
23
|
+
CODEX_PROVIDER_BASE_URL,
|
|
24
|
+
CODEX_PROVIDER_ID,
|
|
25
|
+
} from "@opengeni/codex/constants";
|
|
26
|
+
import { createHash } from "node:crypto";
|
|
13
27
|
import { z } from "zod";
|
|
14
28
|
|
|
15
29
|
const envName = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
@@ -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
|
-
|
|
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 (
|
|
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
|
-
//
|
|
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 (
|
|
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
|
-
//
|
|
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
|
|
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
|
|
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/
|
|
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;
|
|
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
|
-
|
|
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
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 ===
|
|
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
|
-
|
|
1354
|
-
|
|
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) ||
|
|
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
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
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
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
:
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
:
|
|
1392
|
-
|
|
1393
|
-
|
|
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
|
-
|
|
1398
|
-
return out
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
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
|
|
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
|
-
*
|
|
1443
|
-
*
|
|
1444
|
-
*
|
|
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
|
|
1447
|
-
|
|
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 =
|
|
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
|
-
...
|
|
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
|
-
|
|
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
|
|
1537
|
-
if (!
|
|
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
|
|
1545
|
-
const
|
|
1546
|
-
|
|
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
|
|
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
|
-
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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
|
|
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
|
|
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
|