@opengeni/config 0.7.11 → 0.7.22
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 +270 -175
- package/dist/index.js +399 -122
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
- package/src/index.ts +575 -121
package/src/index.ts
CHANGED
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
CAPABILITY_DESCRIPTORS,
|
|
4
4
|
Entitlements,
|
|
5
5
|
EntitlementsMode,
|
|
6
|
+
LatencyMode,
|
|
6
7
|
MAX_NESTED_AGENT_DEPTH,
|
|
7
8
|
ProductAccessMode,
|
|
8
9
|
ReasoningEffort,
|
|
@@ -11,6 +12,7 @@ import {
|
|
|
11
12
|
StaticUsageLimits,
|
|
12
13
|
TurnExecutionPolicyV1,
|
|
13
14
|
UsageLimitsMode,
|
|
15
|
+
type TurnExecutionLatencyModeSourceV1,
|
|
14
16
|
type TurnExecutionModelSourceV1,
|
|
15
17
|
type TurnExecutionReasoningSourceV1,
|
|
16
18
|
} from "@opengeni/contracts";
|
|
@@ -217,6 +219,9 @@ const SettingsSchema = z.object({
|
|
|
217
219
|
observabilityOtlpEndpoint: z.string().url().optional(),
|
|
218
220
|
observabilityOtlpHeaders: z.string().default(""),
|
|
219
221
|
publicBaseUrl: z.string().url().optional(),
|
|
222
|
+
// Browser origin when the web app and API use separate origins in local
|
|
223
|
+
// development. Production normally leaves this unset and uses publicBaseUrl.
|
|
224
|
+
webBaseUrl: z.string().url().optional(),
|
|
220
225
|
// Base URL for the bring-your-own-compute agent release assets the get.<domain>
|
|
221
226
|
// install routes redirect to. Defaults to this repo's GitHub Releases. The route
|
|
222
227
|
// appends `/download/agent-v<ver>/<asset>`.
|
|
@@ -248,9 +253,6 @@ const SettingsSchema = z.object({
|
|
|
248
253
|
// holder of stream:control gets 403 until this flips. Keeps stream:control a
|
|
249
254
|
// declared-but-inert permission so later hardening is a flag flip.
|
|
250
255
|
streamControlEnabled: EnvBoolean.default(false),
|
|
251
|
-
// Existing-session explicit tool replacement is gated until every API and
|
|
252
|
-
// worker instance understands durable tools_provided provenance.
|
|
253
|
-
sessionTurnToolReplacementEnabled: EnvBoolean.default(false),
|
|
254
256
|
toolspaceEnabled: EnvBoolean.default(false),
|
|
255
257
|
toolspaceMaxCallsPerTurn: z.coerce.number().int().positive().default(200),
|
|
256
258
|
// Optional release-coherent bootstrap hint for custom rigs/connected machines
|
|
@@ -267,6 +269,8 @@ const SettingsSchema = z.object({
|
|
|
267
269
|
integrationsOauthClientsJson: z.string().default("{}"),
|
|
268
270
|
slackClientId: z.string().optional(),
|
|
269
271
|
slackClientSecret: z.string().optional(),
|
|
272
|
+
googleDriveClientId: z.string().optional(),
|
|
273
|
+
googleDriveClientSecret: z.string().optional(),
|
|
270
274
|
// Undefined is meaningful: the migration boundary persists the product
|
|
271
275
|
// default of 3 when no deployment override is supplied.
|
|
272
276
|
maxNestedAgentDepth: z.coerce.number().int().nonnegative().max(MAX_NESTED_AGENT_DEPTH).optional(),
|
|
@@ -330,6 +334,41 @@ const SettingsSchema = z.object({
|
|
|
330
334
|
openaiBaseUrl: z.string().optional(),
|
|
331
335
|
openaiModel: z.string().default("gpt-5.6-sol"),
|
|
332
336
|
openaiAllowedModels: z.string().default("gpt-5.6-sol,gpt-5.6-terra,gpt-5.6-luna"),
|
|
337
|
+
// Native composer voice input (browser MediaRecorder → API transcription).
|
|
338
|
+
// Provider credentials stay server-side; ClientConfig only projects availability
|
|
339
|
+
// and hard ceilings. Selection happens once before audio is sent — never retry
|
|
340
|
+
// the same clip across vendors after an upstream request may have started.
|
|
341
|
+
voiceInputMaxDurationSeconds: z.coerce.number().int().positive().max(600).default(60),
|
|
342
|
+
voiceInputMaxSizeBytes: z.coerce
|
|
343
|
+
.number()
|
|
344
|
+
.int()
|
|
345
|
+
.positive()
|
|
346
|
+
.max(100 * 1024 * 1024)
|
|
347
|
+
.default(25 * 1024 * 1024),
|
|
348
|
+
// Preferred provider order (comma-separated ids). First configured+ready wins.
|
|
349
|
+
// Codex subscription STT is preferred by default when subscription routing is
|
|
350
|
+
// enabled; operators can put openai/azure-openai first explicitly.
|
|
351
|
+
// Supported: openai, azure-openai, codex-subscription.
|
|
352
|
+
voiceInputProviderOrder: z.string().default("codex-subscription,openai,azure-openai"),
|
|
353
|
+
// OpenAI public /v1/audio/transcriptions path. Reuses OPENGENI_OPENAI_API_KEY
|
|
354
|
+
// when voiceInputOpenaiApiKey is unset. Default model is gpt-transcribe.
|
|
355
|
+
voiceInputOpenaiEnabled: EnvBoolean.default(true),
|
|
356
|
+
voiceInputOpenaiApiKey: z.string().optional(),
|
|
357
|
+
voiceInputOpenaiBaseUrl: z.string().optional(),
|
|
358
|
+
voiceInputOpenaiModel: z.string().default("gpt-transcribe"),
|
|
359
|
+
// Azure OpenAI deployment-scoped audio transcriptions. Reuses the turn-model
|
|
360
|
+
// Azure endpoint/key/AD token when voice-specific overrides are unset.
|
|
361
|
+
voiceInputAzureEnabled: EnvBoolean.default(true),
|
|
362
|
+
voiceInputAzureEndpoint: z.string().optional(),
|
|
363
|
+
voiceInputAzureDeployment: z.string().optional(),
|
|
364
|
+
voiceInputAzureApiVersion: z.string().optional(),
|
|
365
|
+
voiceInputAzureApiKey: z.string().optional(),
|
|
366
|
+
voiceInputAzureAdToken: z.string().optional(),
|
|
367
|
+
// Legacy opt-in for undocumented ChatGPT /backend-api/transcribe. When
|
|
368
|
+
// OPENGENI_CODEX_SUBSCRIPTION_ENABLED is true, Codex STT is included without
|
|
369
|
+
// this flag. Set false and omit codex-subscription from PROVIDER_ORDER to
|
|
370
|
+
// keep subscription model routing while disabling Codex voice input.
|
|
371
|
+
voiceInputCodexExperimentalEnabled: EnvBoolean.default(false),
|
|
333
372
|
modelPricingJson: z.string().default("{}"),
|
|
334
373
|
// Extra (non-built-in) model providers, declared by the host as a JSON
|
|
335
374
|
// provider registry. Each entry carries its own base URL, API key, wire API
|
|
@@ -413,6 +452,11 @@ const SettingsSchema = z.object({
|
|
|
413
452
|
dockerImage: z.string().default("opengeni-sandbox:local"),
|
|
414
453
|
dockerExposedPorts: z.string().default(""),
|
|
415
454
|
dockerNetwork: z.string().optional(),
|
|
455
|
+
// When the worker itself runs in a container and talks to a host Docker daemon,
|
|
456
|
+
// this directory must be bind-mounted at the exact same absolute path on both
|
|
457
|
+
// sides. The Agents SDK materializes the workspace here before bind-mounting it
|
|
458
|
+
// into the sandbox container.
|
|
459
|
+
dockerWorkspaceBaseDir: z.string().min(1).optional(),
|
|
416
460
|
modalAppName: z.string().default("opengeni-sandbox"),
|
|
417
461
|
modalImageRef: z.string().optional(),
|
|
418
462
|
// Name of a Modal Secret (containing REGISTRY_USERNAME + REGISTRY_PASSWORD) used
|
|
@@ -424,17 +468,17 @@ const SettingsSchema = z.object({
|
|
|
424
468
|
// the named Secret and builds the image via `fromRegistry(tag, secret)` before the
|
|
425
469
|
// first sandbox is created. Knob: OPENGENI_MODAL_IMAGE_REGISTRY_SECRET.
|
|
426
470
|
modalImageRegistrySecret: z.string().optional(),
|
|
427
|
-
// Modal's hard sandbox lifetime (timeoutMs = this * 1000), counted from
|
|
428
|
-
//
|
|
429
|
-
// down, NOT the warm-window
|
|
430
|
-
//
|
|
431
|
-
//
|
|
432
|
-
//
|
|
433
|
-
//
|
|
434
|
-
//
|
|
435
|
-
//
|
|
471
|
+
// Modal's hard sandbox lifetime (timeoutMs = this * 1000), counted from box
|
|
472
|
+
// creation. A resume-by-id does NOT reset that provider clock. It is the
|
|
473
|
+
// BACKSTOP that reclaims a box if the reaper/worker is down, NOT the warm-window
|
|
474
|
+
// controller (that's sandboxIdleGraceMs). It must comfortably exceed
|
|
475
|
+
// reaperPeriod + idleGrace so the reaper terminates a genuinely-idle box FIRST;
|
|
476
|
+
// the boot invariant below enforces that. Default 24h, Modal's documented
|
|
477
|
+
// maximum, to reduce premature active-box loss and leave headroom for the
|
|
478
|
+
// deadline-aware snapshot/rematerialization transition. The transition—not a
|
|
479
|
+
// larger timeout—is what lets a session outlive one finite provider box.
|
|
436
480
|
// Knob: OPENGENI_MODAL_TIMEOUT_SECONDS.
|
|
437
|
-
modalTimeoutSeconds: z.coerce.number().int().positive().default(
|
|
481
|
+
modalTimeoutSeconds: z.coerce.number().int().positive().max(86_400).default(86_400),
|
|
438
482
|
modalTokenId: z.string().optional(),
|
|
439
483
|
modalTokenSecret: z.string().optional(),
|
|
440
484
|
modalEnvironment: z.string().optional(),
|
|
@@ -467,13 +511,6 @@ const SettingsSchema = z.object({
|
|
|
467
511
|
modalWorkspacePersistence: z
|
|
468
512
|
.enum(["tar", "snapshot_filesystem", "snapshot_directory"])
|
|
469
513
|
.default("snapshot_filesystem"),
|
|
470
|
-
// Snapshot GC backstop (sandbox-file-persistence): the reaper keeps ONE latest
|
|
471
|
-
// filesystem snapshot per lease (delete-prior-on-supersede + delete-on-teardown).
|
|
472
|
-
// This is the TTL retention floor for the periodic orphan sweep — a snapshot
|
|
473
|
-
// whose lease is cold and older than this is best-effort deleted so a crashed
|
|
474
|
-
// persist-then-no-restore never leaks a Modal image. 0 disables the TTL sweep
|
|
475
|
-
// (delete-on-supersede/teardown still run). Default 7 days.
|
|
476
|
-
modalSnapshotRetentionSeconds: z.coerce.number().int().nonnegative().default(604_800),
|
|
477
514
|
// Shared desktop toggle: this module reads it for the 6080 port-merge; the
|
|
478
515
|
// owner module (P4.x) acts on it to launch the display stack.
|
|
479
516
|
sandboxDesktopEnabled: EnvBoolean.default(false),
|
|
@@ -692,8 +729,9 @@ const SettingsSchema = z.object({
|
|
|
692
729
|
// this whole window so a "glanced away then came back" re-arms the SAME warm box
|
|
693
730
|
// (acquireLease re-arms draining->warm; the reaper's BEFORE-terminate re-read
|
|
694
731
|
// skips a re-armed box). Default 15min so a brief detour never cold-creates a
|
|
695
|
-
// fresh EMPTY box; lower it to trade warm cost for a snappier reclaim.
|
|
696
|
-
//
|
|
732
|
+
// fresh EMPTY box; lower it to trade warm cost for a snappier reclaim.
|
|
733
|
+
// getSettings caps the default at half a shorter configured Modal lifetime so
|
|
734
|
+
// the entire reaper window always fits. Knob: OPENGENI_SANDBOX_IDLE_GRACE_MS.
|
|
697
735
|
sandboxIdleGraceMs: z.coerce.number().int().positive().default(900_000),
|
|
698
736
|
// MID-SESSION /workspace snapshot cadence (sandbox-file-persistence). The
|
|
699
737
|
// reaper's drain-persist only protects boxes the reaper itself kills; a box
|
|
@@ -711,6 +749,22 @@ const SettingsSchema = z.object({
|
|
|
711
749
|
// treated exactly like a failed best-effort snapshot. Knob:
|
|
712
750
|
// OPENGENI_SANDBOX_SNAPSHOT_TIMEOUT_MS. Default 60s.
|
|
713
751
|
sandboxSnapshotTimeoutMs: z.coerce.number().int().positive().default(60_000),
|
|
752
|
+
// Begin a controlled snapshot/quiesce/drain/rematerialize transition this far
|
|
753
|
+
// ahead of a finite provider deadline. Modal's 24h creation clock cannot be
|
|
754
|
+
// extended; the logical sandbox outlives it by moving to one successor box.
|
|
755
|
+
// getSettings derives the actual default as min(1h, half the configured
|
|
756
|
+
// provider lifetime) so short-lived test/canary boxes remain bootable without
|
|
757
|
+
// an extra coupled environment override. An explicit value may be larger when
|
|
758
|
+
// an operator deliberately wants more rotation headroom; the boot invariant
|
|
759
|
+
// still requires it to remain below the provider lifetime.
|
|
760
|
+
sandboxRotationLeadMs: z.coerce.number().int().positive().default(3_600_000),
|
|
761
|
+
// Bound each global reaper pass so a rollout that discovers many legacy boxes
|
|
762
|
+
// with unknown creation clocks cannot create a provider/API thundering herd.
|
|
763
|
+
// One is the safe admission default: the reaper services provider transitions
|
|
764
|
+
// sequentially, so claiming a wider batch would fence boxes before the same
|
|
765
|
+
// sweep can service them. Larger fleets may raise this only as an explicit,
|
|
766
|
+
// observed deployment choice.
|
|
767
|
+
sandboxRotationBatchSize: z.coerce.number().int().positive().max(500).default(1),
|
|
714
768
|
// expires_at refresh window for a held lease (>> the turn 10s heartbeat so a
|
|
715
769
|
// single missed heartbeat never TTL-reaps a live turn). The warming TTL is the
|
|
716
770
|
// window a cold->warming spawner has to commit warm before a reaper resets it.
|
|
@@ -739,6 +793,7 @@ const SettingsSchema = z.object({
|
|
|
739
793
|
sandboxPreparationProfiles: z.string().default("none"),
|
|
740
794
|
sandboxEnvAllowlist: z.string().default(""),
|
|
741
795
|
objectStorageEndpoint: z.string().url().optional(),
|
|
796
|
+
objectStorageInternalEndpoint: z.string().url().optional(),
|
|
742
797
|
objectStorageSandboxEndpoint: z.string().url().optional(),
|
|
743
798
|
objectStorageBackend: z
|
|
744
799
|
.enum(["s3-compatible", "aws-s3", "azure-blob", "gcs"])
|
|
@@ -817,6 +872,163 @@ const SettingsSchema = z.object({
|
|
|
817
872
|
|
|
818
873
|
export type Settings = z.infer<typeof SettingsSchema>;
|
|
819
874
|
export type McpServerConfig = Settings["mcpServers"][number];
|
|
875
|
+
|
|
876
|
+
/** Declarative voice-input transcription provider ids. */
|
|
877
|
+
export type VoiceInputProviderId = "openai" | "azure-openai" | "codex-subscription";
|
|
878
|
+
|
|
879
|
+
export type VoiceInputProviderConfig =
|
|
880
|
+
| {
|
|
881
|
+
id: "openai";
|
|
882
|
+
kind: "openai";
|
|
883
|
+
apiKey: string;
|
|
884
|
+
baseUrl: string;
|
|
885
|
+
model: string;
|
|
886
|
+
}
|
|
887
|
+
| {
|
|
888
|
+
id: "azure-openai";
|
|
889
|
+
kind: "azure-openai";
|
|
890
|
+
endpoint: string;
|
|
891
|
+
deployment: string;
|
|
892
|
+
apiVersion: string;
|
|
893
|
+
apiKey: string | null;
|
|
894
|
+
adToken: string | null;
|
|
895
|
+
}
|
|
896
|
+
| {
|
|
897
|
+
id: "codex-subscription";
|
|
898
|
+
kind: "codex-subscription";
|
|
899
|
+
experimental: true;
|
|
900
|
+
};
|
|
901
|
+
|
|
902
|
+
/**
|
|
903
|
+
* Reject empty / template secrets so `.env.example` placeholders like
|
|
904
|
+
* `your-key` cannot advertise voice input as available and then 401 upstream.
|
|
905
|
+
*/
|
|
906
|
+
export function isUsableVoiceInputSecret(value: string | null | undefined): value is string {
|
|
907
|
+
if (value == null) return false;
|
|
908
|
+
const trimmed = value.trim();
|
|
909
|
+
if (!trimmed) return false;
|
|
910
|
+
const normalized = trimmed.toLowerCase();
|
|
911
|
+
if (
|
|
912
|
+
normalized === "your-key" ||
|
|
913
|
+
normalized === "your_key" ||
|
|
914
|
+
normalized === "changeme" ||
|
|
915
|
+
normalized === "replace-me" ||
|
|
916
|
+
normalized === "xxx" ||
|
|
917
|
+
normalized.startsWith("your-") ||
|
|
918
|
+
normalized.startsWith("your_")
|
|
919
|
+
) {
|
|
920
|
+
return false;
|
|
921
|
+
}
|
|
922
|
+
return true;
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
/**
|
|
926
|
+
* Resolve the configured voice-input provider registry in selection order.
|
|
927
|
+
* Credentials stay in this server-side structure; ClientConfig only projects
|
|
928
|
+
* whether at least one non-experimental (or probed experimental) provider exists.
|
|
929
|
+
*/
|
|
930
|
+
export function resolveVoiceInputProviderRegistry(settings: Settings): VoiceInputProviderConfig[] {
|
|
931
|
+
const order = settings.voiceInputProviderOrder
|
|
932
|
+
.split(",")
|
|
933
|
+
.map((part) => part.trim())
|
|
934
|
+
.filter(
|
|
935
|
+
(part): part is VoiceInputProviderId =>
|
|
936
|
+
part === "openai" || part === "azure-openai" || part === "codex-subscription",
|
|
937
|
+
);
|
|
938
|
+
const seen = new Set<VoiceInputProviderId>();
|
|
939
|
+
const providers: VoiceInputProviderConfig[] = [];
|
|
940
|
+
for (const id of order) {
|
|
941
|
+
if (seen.has(id)) continue;
|
|
942
|
+
seen.add(id);
|
|
943
|
+
if (id === "openai") {
|
|
944
|
+
if (!settings.voiceInputOpenaiEnabled) continue;
|
|
945
|
+
const apiKey = settings.voiceInputOpenaiApiKey ?? settings.openaiApiKey;
|
|
946
|
+
if (!isUsableVoiceInputSecret(apiKey)) continue;
|
|
947
|
+
// When the turn provider is Azure-only and no voice-specific OpenAI key/URL
|
|
948
|
+
// was set, do not silently reuse a leftover OPENAI_API_KEY for voice.
|
|
949
|
+
if (
|
|
950
|
+
settings.openaiProvider === "azure" &&
|
|
951
|
+
!settings.voiceInputOpenaiApiKey &&
|
|
952
|
+
!settings.voiceInputOpenaiBaseUrl
|
|
953
|
+
) {
|
|
954
|
+
continue;
|
|
955
|
+
}
|
|
956
|
+
providers.push({
|
|
957
|
+
id: "openai",
|
|
958
|
+
kind: "openai",
|
|
959
|
+
apiKey,
|
|
960
|
+
baseUrl: (
|
|
961
|
+
settings.voiceInputOpenaiBaseUrl ??
|
|
962
|
+
settings.openaiBaseUrl ??
|
|
963
|
+
"https://api.openai.com/v1"
|
|
964
|
+
).replace(/\/+$/, ""),
|
|
965
|
+
model: settings.voiceInputOpenaiModel,
|
|
966
|
+
});
|
|
967
|
+
continue;
|
|
968
|
+
}
|
|
969
|
+
if (id === "azure-openai") {
|
|
970
|
+
if (!settings.voiceInputAzureEnabled) continue;
|
|
971
|
+
const endpoint = (
|
|
972
|
+
settings.voiceInputAzureEndpoint ??
|
|
973
|
+
settings.azureOpenaiEndpoint ??
|
|
974
|
+
""
|
|
975
|
+
).replace(/\/+$/, "");
|
|
976
|
+
const deployment = settings.voiceInputAzureDeployment ?? settings.azureOpenaiDeployment ?? "";
|
|
977
|
+
const apiVersion =
|
|
978
|
+
settings.voiceInputAzureApiVersion ??
|
|
979
|
+
settings.azureOpenaiApiVersion ??
|
|
980
|
+
"2025-04-01-preview";
|
|
981
|
+
const apiKey = settings.voiceInputAzureApiKey ?? settings.azureOpenaiApiKey ?? null;
|
|
982
|
+
const adToken = settings.voiceInputAzureAdToken ?? settings.azureOpenaiAdToken ?? null;
|
|
983
|
+
if (
|
|
984
|
+
!endpoint ||
|
|
985
|
+
!deployment ||
|
|
986
|
+
(!isUsableVoiceInputSecret(apiKey) && !isUsableVoiceInputSecret(adToken))
|
|
987
|
+
) {
|
|
988
|
+
continue;
|
|
989
|
+
}
|
|
990
|
+
// When turn provider is OpenAI-only and no voice-specific Azure settings
|
|
991
|
+
// were provided, skip ambient Azure leftovers.
|
|
992
|
+
if (
|
|
993
|
+
settings.openaiProvider !== "azure" &&
|
|
994
|
+
!settings.voiceInputAzureEndpoint &&
|
|
995
|
+
!settings.voiceInputAzureDeployment &&
|
|
996
|
+
!settings.voiceInputAzureApiKey &&
|
|
997
|
+
!settings.voiceInputAzureAdToken
|
|
998
|
+
) {
|
|
999
|
+
continue;
|
|
1000
|
+
}
|
|
1001
|
+
providers.push({
|
|
1002
|
+
id: "azure-openai",
|
|
1003
|
+
kind: "azure-openai",
|
|
1004
|
+
endpoint,
|
|
1005
|
+
deployment,
|
|
1006
|
+
apiVersion,
|
|
1007
|
+
apiKey: isUsableVoiceInputSecret(apiKey) ? apiKey : null,
|
|
1008
|
+
adToken: isUsableVoiceInputSecret(adToken) ? adToken : null,
|
|
1009
|
+
});
|
|
1010
|
+
continue;
|
|
1011
|
+
}
|
|
1012
|
+
if (id === "codex-subscription") {
|
|
1013
|
+
// Prefer Codex STT whenever subscription model routing is enabled.
|
|
1014
|
+
// Operators who want OpenAI/Azure first should set PROVIDER_ORDER; omit
|
|
1015
|
+
// `codex-subscription` from the order to disable Codex voice while keeping
|
|
1016
|
+
// subscription turns. VOICE_INPUT_CODEX_EXPERIMENTAL is retained for
|
|
1017
|
+
// back-compat docs/env but no longer gates inclusion.
|
|
1018
|
+
if (!settings.codexSubscriptionEnabled) continue;
|
|
1019
|
+
providers.push({ id: "codex-subscription", kind: "codex-subscription", experimental: true });
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
return providers;
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
/** True when the deployment has at least one supported (non-experimental) provider. */
|
|
1026
|
+
export function voiceInputDeploymentConfigured(settings: Settings): boolean {
|
|
1027
|
+
return resolveVoiceInputProviderRegistry(settings).some(
|
|
1028
|
+
(provider) => provider.kind !== "codex-subscription",
|
|
1029
|
+
);
|
|
1030
|
+
}
|
|
1031
|
+
|
|
820
1032
|
export type TemporalTlsConnectionConfig = {
|
|
821
1033
|
serverNameOverride?: string;
|
|
822
1034
|
serverRootCACertificate?: Uint8Array;
|
|
@@ -1165,88 +1377,91 @@ export interface ConfiguredModel {
|
|
|
1165
1377
|
hostedWebSearch: boolean;
|
|
1166
1378
|
}
|
|
1167
1379
|
|
|
1168
|
-
|
|
1380
|
+
/**
|
|
1381
|
+
* Built-in OpenGeni credit pricing schedules.
|
|
1382
|
+
*
|
|
1383
|
+
* Rates are provider list prices in USD micros per 1M tokens. Debit applies
|
|
1384
|
+
* `marginBps` (2_500 = +25%) on top. Long-context tiers follow OpenAI's
|
|
1385
|
+
* ">272K input tokens" rule (threshold exclusive of 272_000).
|
|
1386
|
+
*
|
|
1387
|
+
* GPT-5.4 and older families are intentionally omitted — they are no longer
|
|
1388
|
+
* offered. Codex / connected-subscription turns use `metering: external` and
|
|
1389
|
+
* never consult this map.
|
|
1390
|
+
*
|
|
1391
|
+
* When adding or changing a billed model, run `bun run check:model-pricing`
|
|
1392
|
+
* (see docs/model-providers.md § Price audit). That compares this map to
|
|
1393
|
+
* llm-prices.com as a ground-truth canary; it does not generate this table.
|
|
1394
|
+
*/
|
|
1395
|
+
export const defaultModelPricing: Record<string, ModelPricingScheduleV1> = {
|
|
1169
1396
|
"gpt-5.6-sol": {
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1397
|
+
default: {
|
|
1398
|
+
inputMicrosPerMillionTokens: 5_000_000,
|
|
1399
|
+
cachedInputMicrosPerMillionTokens: 500_000,
|
|
1400
|
+
outputMicrosPerMillionTokens: 30_000_000,
|
|
1401
|
+
marginBps: 2_500,
|
|
1402
|
+
},
|
|
1403
|
+
inputTokenTiers: [
|
|
1404
|
+
{
|
|
1405
|
+
// OpenAI: prompts with >272K input tokens use the long-context rate.
|
|
1406
|
+
minimumInputTokens: 272_001,
|
|
1407
|
+
pricing: {
|
|
1408
|
+
inputMicrosPerMillionTokens: 10_000_000,
|
|
1409
|
+
cachedInputMicrosPerMillionTokens: 1_000_000,
|
|
1410
|
+
outputMicrosPerMillionTokens: 45_000_000,
|
|
1411
|
+
marginBps: 2_500,
|
|
1412
|
+
},
|
|
1413
|
+
},
|
|
1414
|
+
],
|
|
1174
1415
|
},
|
|
1175
1416
|
"gpt-5.6-terra": {
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1417
|
+
default: {
|
|
1418
|
+
inputMicrosPerMillionTokens: 2_000_000,
|
|
1419
|
+
cachedInputMicrosPerMillionTokens: 200_000,
|
|
1420
|
+
outputMicrosPerMillionTokens: 12_000_000,
|
|
1421
|
+
marginBps: 2_500,
|
|
1422
|
+
},
|
|
1423
|
+
inputTokenTiers: [
|
|
1424
|
+
{
|
|
1425
|
+
minimumInputTokens: 272_001,
|
|
1426
|
+
pricing: {
|
|
1427
|
+
inputMicrosPerMillionTokens: 4_000_000,
|
|
1428
|
+
cachedInputMicrosPerMillionTokens: 400_000,
|
|
1429
|
+
outputMicrosPerMillionTokens: 18_000_000,
|
|
1430
|
+
marginBps: 2_500,
|
|
1431
|
+
},
|
|
1432
|
+
},
|
|
1433
|
+
],
|
|
1180
1434
|
},
|
|
1181
1435
|
"gpt-5.6-luna": {
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
"gpt-5.2": {
|
|
1200
|
-
inputMicrosPerMillionTokens: 1_750_000,
|
|
1201
|
-
cachedInputMicrosPerMillionTokens: 175_000,
|
|
1202
|
-
outputMicrosPerMillionTokens: 14_000_000,
|
|
1203
|
-
marginBps: 2_500,
|
|
1204
|
-
},
|
|
1205
|
-
"gpt-5.2-chat-latest": {
|
|
1206
|
-
inputMicrosPerMillionTokens: 1_750_000,
|
|
1207
|
-
cachedInputMicrosPerMillionTokens: 175_000,
|
|
1208
|
-
outputMicrosPerMillionTokens: 14_000_000,
|
|
1209
|
-
marginBps: 2_500,
|
|
1210
|
-
},
|
|
1211
|
-
"gpt-5.2-codex": {
|
|
1212
|
-
inputMicrosPerMillionTokens: 1_750_000,
|
|
1213
|
-
cachedInputMicrosPerMillionTokens: 175_000,
|
|
1214
|
-
outputMicrosPerMillionTokens: 14_000_000,
|
|
1215
|
-
marginBps: 2_500,
|
|
1216
|
-
},
|
|
1217
|
-
"gpt-5.1": {
|
|
1218
|
-
inputMicrosPerMillionTokens: 1_250_000,
|
|
1219
|
-
cachedInputMicrosPerMillionTokens: 125_000,
|
|
1220
|
-
outputMicrosPerMillionTokens: 10_000_000,
|
|
1221
|
-
marginBps: 2_500,
|
|
1222
|
-
},
|
|
1223
|
-
"gpt-5": {
|
|
1224
|
-
inputMicrosPerMillionTokens: 1_250_000,
|
|
1225
|
-
cachedInputMicrosPerMillionTokens: 125_000,
|
|
1226
|
-
outputMicrosPerMillionTokens: 10_000_000,
|
|
1227
|
-
marginBps: 2_500,
|
|
1228
|
-
},
|
|
1229
|
-
"gpt-5-mini": {
|
|
1230
|
-
inputMicrosPerMillionTokens: 250_000,
|
|
1231
|
-
cachedInputMicrosPerMillionTokens: 25_000,
|
|
1232
|
-
outputMicrosPerMillionTokens: 2_000_000,
|
|
1233
|
-
marginBps: 2_500,
|
|
1234
|
-
},
|
|
1235
|
-
"gpt-5-nano": {
|
|
1236
|
-
inputMicrosPerMillionTokens: 50_000,
|
|
1237
|
-
cachedInputMicrosPerMillionTokens: 5_000,
|
|
1238
|
-
outputMicrosPerMillionTokens: 400_000,
|
|
1239
|
-
marginBps: 2_500,
|
|
1436
|
+
default: {
|
|
1437
|
+
inputMicrosPerMillionTokens: 200_000,
|
|
1438
|
+
cachedInputMicrosPerMillionTokens: 20_000,
|
|
1439
|
+
outputMicrosPerMillionTokens: 1_200_000,
|
|
1440
|
+
marginBps: 2_500,
|
|
1441
|
+
},
|
|
1442
|
+
inputTokenTiers: [
|
|
1443
|
+
{
|
|
1444
|
+
minimumInputTokens: 272_001,
|
|
1445
|
+
pricing: {
|
|
1446
|
+
inputMicrosPerMillionTokens: 400_000,
|
|
1447
|
+
cachedInputMicrosPerMillionTokens: 40_000,
|
|
1448
|
+
outputMicrosPerMillionTokens: 1_800_000,
|
|
1449
|
+
marginBps: 2_500,
|
|
1450
|
+
},
|
|
1451
|
+
},
|
|
1452
|
+
],
|
|
1240
1453
|
},
|
|
1241
1454
|
// Fireworks AI / GLM 5.2 — the first shipped non-OpenAI registry model. A
|
|
1242
1455
|
// built-in default pricing entry makes managed billing work out of the box
|
|
1243
1456
|
// for hosts that expose this model via OPENGENI_MODEL_PROVIDERS_JSON without
|
|
1244
1457
|
// also setting OPENGENI_MODEL_PRICING_JSON.
|
|
1245
1458
|
"accounts/fireworks/models/glm-5p2": {
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1459
|
+
default: {
|
|
1460
|
+
inputMicrosPerMillionTokens: 1_400_000,
|
|
1461
|
+
cachedInputMicrosPerMillionTokens: 140_000,
|
|
1462
|
+
outputMicrosPerMillionTokens: 4_400_000,
|
|
1463
|
+
marginBps: 2_500,
|
|
1464
|
+
},
|
|
1250
1465
|
},
|
|
1251
1466
|
};
|
|
1252
1467
|
|
|
@@ -1349,6 +1564,7 @@ export function getSettings(): Settings {
|
|
|
1349
1564
|
observabilityOtlpHeaders:
|
|
1350
1565
|
optional("OPENGENI_OTEL_EXPORTER_OTLP_HEADERS") ?? optional("OTEL_EXPORTER_OTLP_HEADERS"),
|
|
1351
1566
|
publicBaseUrl: optional("OPENGENI_PUBLIC_BASE_URL"),
|
|
1567
|
+
webBaseUrl: optional("OPENGENI_WEB_BASE_URL"),
|
|
1352
1568
|
agentReleasesBaseUrl: optional("OPENGENI_AGENT_RELEASES_BASE_URL"),
|
|
1353
1569
|
agentStableVersion: optional("OPENGENI_AGENT_STABLE_VERSION"),
|
|
1354
1570
|
productAccessMode: optional("OPENGENI_PRODUCT_ACCESS_MODE"),
|
|
@@ -1360,7 +1576,6 @@ export function getSettings(): Settings {
|
|
|
1360
1576
|
delegationSecret: optional("OPENGENI_DELEGATION_SECRET"),
|
|
1361
1577
|
streamTokenSecret: optional("OPENGENI_STREAM_TOKEN_SECRET"),
|
|
1362
1578
|
streamControlEnabled: optional("OPENGENI_STREAM_CONTROL_ENABLED"),
|
|
1363
|
-
sessionTurnToolReplacementEnabled: optional("OPENGENI_SESSION_TURN_TOOL_REPLACEMENT_ENABLED"),
|
|
1364
1579
|
toolspaceEnabled: optional("OPENGENI_TOOLSPACE_ENABLED"),
|
|
1365
1580
|
toolspaceMaxCallsPerTurn: optional("OPENGENI_TOOLSPACE_MAX_CALLS_PER_TURN"),
|
|
1366
1581
|
ogtoolPackageSpec: optional("OPENGENI_OGTOOL_PACKAGE_SPEC"),
|
|
@@ -1373,6 +1588,8 @@ export function getSettings(): Settings {
|
|
|
1373
1588
|
integrationsOauthClientsJson: optional("OPENGENI_INTEGRATIONS_OAUTH_CLIENTS_JSON"),
|
|
1374
1589
|
slackClientId: optional("OPENGENI_SLACK_CLIENT_ID"),
|
|
1375
1590
|
slackClientSecret: optional("OPENGENI_SLACK_CLIENT_SECRET"),
|
|
1591
|
+
googleDriveClientId: optional("OPENGENI_GOOGLE_DRIVE_CLIENT_ID"),
|
|
1592
|
+
googleDriveClientSecret: optional("OPENGENI_GOOGLE_DRIVE_CLIENT_SECRET"),
|
|
1376
1593
|
maxNestedAgentDepth: optional("OPENGENI_MAX_NESTED_AGENT_DEPTH"),
|
|
1377
1594
|
goalMaxAutoContinuations: optional("OPENGENI_GOAL_MAX_AUTO_CONTINUATIONS"),
|
|
1378
1595
|
goalNoProgressLimit: optional("OPENGENI_GOAL_NO_PROGRESS_LIMIT"),
|
|
@@ -1397,6 +1614,20 @@ export function getSettings(): Settings {
|
|
|
1397
1614
|
openaiBaseUrl: optional("OPENGENI_OPENAI_BASE_URL") ?? optional("OPENAI_BASE_URL"),
|
|
1398
1615
|
openaiModel: optional("OPENGENI_OPENAI_MODEL"),
|
|
1399
1616
|
openaiAllowedModels: optional("OPENGENI_OPENAI_ALLOWED_MODELS"),
|
|
1617
|
+
voiceInputMaxDurationSeconds: optional("OPENGENI_VOICE_INPUT_MAX_DURATION_SECONDS"),
|
|
1618
|
+
voiceInputMaxSizeBytes: optional("OPENGENI_VOICE_INPUT_MAX_SIZE_BYTES"),
|
|
1619
|
+
voiceInputProviderOrder: optional("OPENGENI_VOICE_INPUT_PROVIDER_ORDER"),
|
|
1620
|
+
voiceInputOpenaiEnabled: optional("OPENGENI_VOICE_INPUT_OPENAI_ENABLED"),
|
|
1621
|
+
voiceInputOpenaiApiKey: optional("OPENGENI_VOICE_INPUT_OPENAI_API_KEY"),
|
|
1622
|
+
voiceInputOpenaiBaseUrl: optional("OPENGENI_VOICE_INPUT_OPENAI_BASE_URL"),
|
|
1623
|
+
voiceInputOpenaiModel: optional("OPENGENI_VOICE_INPUT_OPENAI_MODEL"),
|
|
1624
|
+
voiceInputAzureEnabled: optional("OPENGENI_VOICE_INPUT_AZURE_ENABLED"),
|
|
1625
|
+
voiceInputAzureEndpoint: optional("OPENGENI_VOICE_INPUT_AZURE_ENDPOINT"),
|
|
1626
|
+
voiceInputAzureDeployment: optional("OPENGENI_VOICE_INPUT_AZURE_DEPLOYMENT"),
|
|
1627
|
+
voiceInputAzureApiVersion: optional("OPENGENI_VOICE_INPUT_AZURE_API_VERSION"),
|
|
1628
|
+
voiceInputAzureApiKey: optional("OPENGENI_VOICE_INPUT_AZURE_API_KEY"),
|
|
1629
|
+
voiceInputAzureAdToken: optional("OPENGENI_VOICE_INPUT_AZURE_AD_TOKEN"),
|
|
1630
|
+
voiceInputCodexExperimentalEnabled: optional("OPENGENI_VOICE_INPUT_CODEX_EXPERIMENTAL"),
|
|
1400
1631
|
modelPricingJson: optional("OPENGENI_MODEL_PRICING_JSON"),
|
|
1401
1632
|
modelProvidersJson: optional("OPENGENI_MODEL_PROVIDERS_JSON"),
|
|
1402
1633
|
codexSubscriptionEnabled: optional("OPENGENI_CODEX_SUBSCRIPTION_ENABLED"),
|
|
@@ -1424,6 +1655,7 @@ export function getSettings(): Settings {
|
|
|
1424
1655
|
dockerImage: optional("OPENGENI_DOCKER_IMAGE"),
|
|
1425
1656
|
dockerExposedPorts: optional("OPENGENI_DOCKER_EXPOSED_PORTS"),
|
|
1426
1657
|
dockerNetwork: optional("OPENGENI_DOCKER_NETWORK"),
|
|
1658
|
+
dockerWorkspaceBaseDir: optional("OPENGENI_DOCKER_WORKSPACE_BASE_DIR"),
|
|
1427
1659
|
modalAppName: optional("OPENGENI_MODAL_APP_NAME"),
|
|
1428
1660
|
modalImageRef: optional("OPENGENI_MODAL_IMAGE_REF"),
|
|
1429
1661
|
modalImageRegistrySecret: optional("OPENGENI_MODAL_IMAGE_REGISTRY_SECRET"),
|
|
@@ -1433,7 +1665,6 @@ export function getSettings(): Settings {
|
|
|
1433
1665
|
modalEnvironment: optional("OPENGENI_MODAL_ENVIRONMENT"),
|
|
1434
1666
|
modalIdleTimeoutSeconds: optional("OPENGENI_MODAL_IDLE_TIMEOUT_SECONDS"),
|
|
1435
1667
|
modalWorkspacePersistence: optional("OPENGENI_MODAL_WORKSPACE_PERSISTENCE"),
|
|
1436
|
-
modalSnapshotRetentionSeconds: optional("OPENGENI_MODAL_SNAPSHOT_RETENTION_SECONDS"),
|
|
1437
1668
|
sandboxDesktopEnabled: optional("OPENGENI_SANDBOX_DESKTOP_ENABLED"),
|
|
1438
1669
|
sandboxDesktopInteractive: optional("OPENGENI_SANDBOX_DESKTOP_INTERACTIVE"),
|
|
1439
1670
|
sandboxTerminalEnabled: optional("OPENGENI_SANDBOX_TERMINAL_ENABLED"),
|
|
@@ -1506,6 +1737,8 @@ export function getSettings(): Settings {
|
|
|
1506
1737
|
sandboxIdleGraceMs: optional("OPENGENI_SANDBOX_IDLE_GRACE_MS"),
|
|
1507
1738
|
sandboxSnapshotIntervalMs: optional("OPENGENI_SANDBOX_SNAPSHOT_INTERVAL_MS"),
|
|
1508
1739
|
sandboxSnapshotTimeoutMs: optional("OPENGENI_SANDBOX_SNAPSHOT_TIMEOUT_MS"),
|
|
1740
|
+
sandboxRotationLeadMs: optional("OPENGENI_SANDBOX_ROTATION_LEAD_MS"),
|
|
1741
|
+
sandboxRotationBatchSize: optional("OPENGENI_SANDBOX_ROTATION_BATCH_SIZE"),
|
|
1509
1742
|
sandboxLeaseTtlMs: optional("OPENGENI_SANDBOX_LEASE_TTL_MS"),
|
|
1510
1743
|
sandboxLeaseWarmingTtlMs: optional("OPENGENI_SANDBOX_LEASE_WARMING_TTL_MS"),
|
|
1511
1744
|
sandboxWarmingTimeoutMs: optional("OPENGENI_SANDBOX_WARMING_TIMEOUT_MS"),
|
|
@@ -1517,6 +1750,7 @@ export function getSettings(): Settings {
|
|
|
1517
1750
|
sandboxPreparationProfiles: optional("OPENGENI_SANDBOX_PREPARATION_PROFILES"),
|
|
1518
1751
|
sandboxEnvAllowlist: optional("OPENGENI_SANDBOX_ENV_ALLOWLIST"),
|
|
1519
1752
|
objectStorageEndpoint: optional("OPENGENI_OBJECT_STORAGE_ENDPOINT"),
|
|
1753
|
+
objectStorageInternalEndpoint: optional("OPENGENI_OBJECT_STORAGE_INTERNAL_ENDPOINT"),
|
|
1520
1754
|
objectStorageSandboxEndpoint: optional("OPENGENI_OBJECT_STORAGE_SANDBOX_ENDPOINT"),
|
|
1521
1755
|
objectStorageBackend: optional("OPENGENI_OBJECT_STORAGE_BACKEND"),
|
|
1522
1756
|
objectStorageBucket: optional("OPENGENI_OBJECT_STORAGE_BUCKET"),
|
|
@@ -1572,6 +1806,14 @@ export function getSettings(): Settings {
|
|
|
1572
1806
|
const parsed = SettingsSchema.parse(raw);
|
|
1573
1807
|
const settings = {
|
|
1574
1808
|
...parsed,
|
|
1809
|
+
sandboxIdleGraceMs:
|
|
1810
|
+
raw.sandboxIdleGraceMs === undefined
|
|
1811
|
+
? Math.min(900_000, Math.floor((parsed.modalTimeoutSeconds * 1000) / 2))
|
|
1812
|
+
: parsed.sandboxIdleGraceMs,
|
|
1813
|
+
sandboxRotationLeadMs:
|
|
1814
|
+
raw.sandboxRotationLeadMs === undefined
|
|
1815
|
+
? Math.min(3_600_000, Math.floor((parsed.modalTimeoutSeconds * 1000) / 2))
|
|
1816
|
+
: parsed.sandboxRotationLeadMs,
|
|
1575
1817
|
mcpServers: ensureBuiltInMcpServers(parsed),
|
|
1576
1818
|
};
|
|
1577
1819
|
validateSettings(settings);
|
|
@@ -1593,6 +1835,23 @@ export function effectiveModalIdleTimeoutSeconds(settings: Settings): number {
|
|
|
1593
1835
|
return settings.modalIdleTimeoutSeconds ?? settings.modalTimeoutSeconds;
|
|
1594
1836
|
}
|
|
1595
1837
|
|
|
1838
|
+
/**
|
|
1839
|
+
* One shared upper bound for the durable provider-capture claim and for command
|
|
1840
|
+
* admission waiting behind it. The SDK request itself is bounded by
|
|
1841
|
+
* sandboxSnapshotTimeoutMs; the extra window lets a non-cancellable provider
|
|
1842
|
+
* response settle and release its exact claim without turning a normal
|
|
1843
|
+
* checkpoint into a visible command failure. Database validation caps both
|
|
1844
|
+
* consumers at one hour.
|
|
1845
|
+
*/
|
|
1846
|
+
export function sandboxArchiveCaptureTimeoutMs(
|
|
1847
|
+
settings: Pick<Settings, "sandboxSnapshotTimeoutMs">,
|
|
1848
|
+
): number {
|
|
1849
|
+
return Math.min(
|
|
1850
|
+
60 * 60_000,
|
|
1851
|
+
Math.max(settings.sandboxSnapshotTimeoutMs + 30_000, settings.sandboxSnapshotTimeoutMs * 2),
|
|
1852
|
+
);
|
|
1853
|
+
}
|
|
1854
|
+
|
|
1596
1855
|
export function collectSandboxEnvironment(
|
|
1597
1856
|
settings: Settings,
|
|
1598
1857
|
source: NodeJS.ProcessEnv = process.env,
|
|
@@ -1883,6 +2142,115 @@ function legacyModelCapabilities(
|
|
|
1883
2142
|
});
|
|
1884
2143
|
}
|
|
1885
2144
|
|
|
2145
|
+
/** OpenAI GPT-5.6 Fast mode is 2× Standard list rates (service_tier fast/priority). */
|
|
2146
|
+
const GPT56_FAST_BILLING_MULTIPLIER_BPS = 20_000;
|
|
2147
|
+
|
|
2148
|
+
/**
|
|
2149
|
+
* Product display label for catalog/picker UI.
|
|
2150
|
+
* Same string for OpenAI and Codex copies of a slug (`gpt-5.6-luna` and
|
|
2151
|
+
* `codex/gpt-5.6-luna` → `GPT-5.6 Luna`). Non-gpt ids pass through unchanged.
|
|
2152
|
+
*/
|
|
2153
|
+
export function productLabelForModelId(modelId: string): string {
|
|
2154
|
+
const slug = modelId.startsWith(CODEX_MODEL_ID_PREFIX)
|
|
2155
|
+
? modelId.slice(CODEX_MODEL_ID_PREFIX.length)
|
|
2156
|
+
: modelId;
|
|
2157
|
+
const match = /^(gpt-\d+(?:\.\d+)?)(?:-(.+))?$/i.exec(slug);
|
|
2158
|
+
if (!match) {
|
|
2159
|
+
return slug;
|
|
2160
|
+
}
|
|
2161
|
+
const family = match[1]!.replace(/^gpt/i, "GPT");
|
|
2162
|
+
const rest = match[2];
|
|
2163
|
+
if (!rest) {
|
|
2164
|
+
return family;
|
|
2165
|
+
}
|
|
2166
|
+
const suffix = rest
|
|
2167
|
+
.split("-")
|
|
2168
|
+
.filter((part) => part.length > 0)
|
|
2169
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase())
|
|
2170
|
+
.join(" ");
|
|
2171
|
+
return suffix.length > 0 ? `${family} ${suffix}` : family;
|
|
2172
|
+
}
|
|
2173
|
+
|
|
2174
|
+
function builtinLatencyModesForModel(modelId: string): Array<{
|
|
2175
|
+
id: z.infer<typeof ModelLatencyModeV1>;
|
|
2176
|
+
upstream: "supported" | "unsupported" | "unknown";
|
|
2177
|
+
runnable: boolean;
|
|
2178
|
+
billingMultiplierBps?: number;
|
|
2179
|
+
}> {
|
|
2180
|
+
if (
|
|
2181
|
+
modelId === "gpt-5.6-sol" ||
|
|
2182
|
+
modelId === "gpt-5.6-terra" ||
|
|
2183
|
+
modelId === "gpt-5.6-luna" ||
|
|
2184
|
+
modelId.startsWith("codex/gpt-5.6-")
|
|
2185
|
+
) {
|
|
2186
|
+
return [
|
|
2187
|
+
{ id: "standard", upstream: "supported", runnable: true },
|
|
2188
|
+
{
|
|
2189
|
+
id: "fast",
|
|
2190
|
+
upstream: "supported",
|
|
2191
|
+
runnable: true,
|
|
2192
|
+
billingMultiplierBps: GPT56_FAST_BILLING_MULTIPLIER_BPS,
|
|
2193
|
+
},
|
|
2194
|
+
];
|
|
2195
|
+
}
|
|
2196
|
+
return [{ id: "standard", upstream: "unknown", runnable: true }];
|
|
2197
|
+
}
|
|
2198
|
+
|
|
2199
|
+
/**
|
|
2200
|
+
* Map OpenGeni latency mode to the provider `service_tier` wire value.
|
|
2201
|
+
* Azure and Codex ChatGPT accept `priority`; OpenAI API accepts `fast` (alias of priority).
|
|
2202
|
+
* Standard omits the field.
|
|
2203
|
+
*/
|
|
2204
|
+
export function serviceTierForLatencyMode(
|
|
2205
|
+
providerId: string,
|
|
2206
|
+
latencyMode: LatencyMode,
|
|
2207
|
+
): "fast" | "priority" | undefined {
|
|
2208
|
+
if (latencyMode === "standard") {
|
|
2209
|
+
return undefined;
|
|
2210
|
+
}
|
|
2211
|
+
if (providerId === "azure" || providerId === CODEX_PROVIDER_ID) {
|
|
2212
|
+
return "priority";
|
|
2213
|
+
}
|
|
2214
|
+
return "fast";
|
|
2215
|
+
}
|
|
2216
|
+
|
|
2217
|
+
/** True when the response tier fulfills a non-standard Fast/priority request. */
|
|
2218
|
+
export function responseSatisfiesLatencyMode(
|
|
2219
|
+
requested: LatencyMode,
|
|
2220
|
+
responseServiceTier: string | null | undefined,
|
|
2221
|
+
): boolean {
|
|
2222
|
+
if (requested === "standard") {
|
|
2223
|
+
return true;
|
|
2224
|
+
}
|
|
2225
|
+
return responseServiceTier === "priority" || responseServiceTier === "fast";
|
|
2226
|
+
}
|
|
2227
|
+
|
|
2228
|
+
export function runnableLatencyModesForModel(settings: Settings, modelId: string): LatencyMode[] {
|
|
2229
|
+
const resolved = resolveModelProvider(
|
|
2230
|
+
settingsForTurnExecutionPolicy(settings, modelId),
|
|
2231
|
+
canonicalizeConfiguredModelId(settings, modelId),
|
|
2232
|
+
);
|
|
2233
|
+
if (!resolved) {
|
|
2234
|
+
return ["standard"];
|
|
2235
|
+
}
|
|
2236
|
+
return resolved.model.capabilities.latencyModes
|
|
2237
|
+
.filter((mode) => mode.runnable)
|
|
2238
|
+
.map((mode) => LatencyMode.parse(mode.id));
|
|
2239
|
+
}
|
|
2240
|
+
|
|
2241
|
+
function assertLatencyModeRunnable(
|
|
2242
|
+
settings: Settings,
|
|
2243
|
+
modelId: string,
|
|
2244
|
+
latencyMode: LatencyMode,
|
|
2245
|
+
): void {
|
|
2246
|
+
const runnable = runnableLatencyModesForModel(settings, modelId);
|
|
2247
|
+
if (!runnable.includes(latencyMode)) {
|
|
2248
|
+
throw new Error(
|
|
2249
|
+
`latency mode ${latencyMode} is not runnable for model ${modelId} (allowed: ${runnable.join(", ")})`,
|
|
2250
|
+
);
|
|
2251
|
+
}
|
|
2252
|
+
}
|
|
2253
|
+
|
|
1886
2254
|
function registryCredentialSource(provider: RegistryProvider): CredentialSourceV1 {
|
|
1887
2255
|
return provider.kind === "codex-subscription"
|
|
1888
2256
|
? { kind: "connected_subscription", provider: "codex" }
|
|
@@ -2056,21 +2424,31 @@ export function withCodexCatalogProvider(settings: Settings): Settings {
|
|
|
2056
2424
|
label: "Codex (ChatGPT subscription)",
|
|
2057
2425
|
api: "responses",
|
|
2058
2426
|
baseUrl: CODEX_PROVIDER_BASE_URL,
|
|
2059
|
-
models: CODEX_FALLBACK_MODEL_SLUGS.map((slug) =>
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2427
|
+
models: CODEX_FALLBACK_MODEL_SLUGS.map((slug) => {
|
|
2428
|
+
const capabilities = {
|
|
2429
|
+
...legacyModelCapabilities(settings, {
|
|
2430
|
+
reasoningEffort: true,
|
|
2431
|
+
hostedWebSearch: true,
|
|
2432
|
+
}),
|
|
2433
|
+
latencyModes: builtinLatencyModesForModel(`${CODEX_MODEL_ID_PREFIX}${slug}`),
|
|
2434
|
+
};
|
|
2435
|
+
return {
|
|
2436
|
+
id: `${CODEX_MODEL_ID_PREFIX}${slug}`,
|
|
2437
|
+
upstreamModelId: slug,
|
|
2438
|
+
label: productLabelForModelId(slug),
|
|
2439
|
+
reasoningEffort: true,
|
|
2440
|
+
// The ChatGPT/Codex Responses backend accepts the native web_search
|
|
2441
|
+
// hosted tool (unlike hosted apply_patch/computer transports). Declaring
|
|
2442
|
+
// this here makes provider resolution truthful; the worker still applies
|
|
2443
|
+
// the durable session/turn policy gate before attaching it.
|
|
2444
|
+
hostedWebSearch: true,
|
|
2445
|
+
capabilities,
|
|
2446
|
+
contextWindowTokens: CODEX_MODEL_CONTEXT_WINDOW_TOKENS,
|
|
2447
|
+
effectiveContextWindowTokens: CODEX_MODEL_EFFECTIVE_CONTEXT_WINDOW_TOKENS,
|
|
2448
|
+
autoCompactTokenLimit: CODEX_MODEL_AUTO_COMPACT_TOKEN_LIMIT,
|
|
2449
|
+
toolOutputTruncationTokens: CODEX_MODEL_TOOL_OUTPUT_TRUNCATION_TOKENS,
|
|
2450
|
+
};
|
|
2451
|
+
}),
|
|
2074
2452
|
};
|
|
2075
2453
|
return { ...settings, modelProvidersJson: JSON.stringify([...providers, provider]) };
|
|
2076
2454
|
}
|
|
@@ -2217,14 +2595,17 @@ export function configuredModels(settings: Settings): ConfiguredModel[] {
|
|
|
2217
2595
|
])
|
|
2218
2596
|
.filter((id) => !isRegistryNamespaced(id))
|
|
2219
2597
|
.map((id) => {
|
|
2220
|
-
const capabilities =
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2598
|
+
const capabilities = {
|
|
2599
|
+
...legacyModelCapabilities(settings, {
|
|
2600
|
+
reasoningEffort: true,
|
|
2601
|
+
hostedWebSearch: settings.webSearchEnabled,
|
|
2602
|
+
}),
|
|
2603
|
+
latencyModes: builtinLatencyModesForModel(id),
|
|
2604
|
+
};
|
|
2224
2605
|
return finalizeConfiguredModel(settings, builtinProvider, {
|
|
2225
2606
|
id,
|
|
2226
2607
|
aliases: [],
|
|
2227
|
-
label: id,
|
|
2608
|
+
label: productLabelForModelId(id),
|
|
2228
2609
|
providerId: builtinId,
|
|
2229
2610
|
providerLabel: builtinLabel,
|
|
2230
2611
|
api: "responses" as const,
|
|
@@ -2258,7 +2639,7 @@ export function configuredModels(settings: Settings): ConfiguredModel[] {
|
|
|
2258
2639
|
finalizeConfiguredModel(settings, resolvedProvider, {
|
|
2259
2640
|
id: model.id,
|
|
2260
2641
|
aliases: [...(model.aliases ?? [])],
|
|
2261
|
-
label: model.label ?? model.id,
|
|
2642
|
+
label: model.label ?? productLabelForModelId(model.id),
|
|
2262
2643
|
providerId: provider.id,
|
|
2263
2644
|
providerLabel,
|
|
2264
2645
|
api: provider.api,
|
|
@@ -2345,6 +2726,8 @@ export type ResolveTurnExecutionPolicyV1Input = {
|
|
|
2345
2726
|
modelSource: TurnExecutionModelSourceV1;
|
|
2346
2727
|
reasoningEffort: Settings["openaiReasoningEffort"];
|
|
2347
2728
|
reasoningSource: TurnExecutionReasoningSourceV1;
|
|
2729
|
+
latencyMode?: LatencyMode;
|
|
2730
|
+
latencyModeSource?: TurnExecutionLatencyModeSourceV1;
|
|
2348
2731
|
};
|
|
2349
2732
|
|
|
2350
2733
|
function settingsForTurnExecutionPolicy(settings: Settings, modelId: string): Settings {
|
|
@@ -2374,6 +2757,9 @@ export function resolveTurnExecutionPolicyV1(
|
|
|
2374
2757
|
) {
|
|
2375
2758
|
throw new Error("Turn execution policy requested model does not canonicalize to its product");
|
|
2376
2759
|
}
|
|
2760
|
+
const latencyMode = LatencyMode.parse(input.latencyMode ?? "standard");
|
|
2761
|
+
const latencyModeSource = input.latencyModeSource ?? "deployment";
|
|
2762
|
+
assertLatencyModeRunnable(catalogSettings, productModelId, latencyMode);
|
|
2377
2763
|
return TurnExecutionPolicyV1.parse({
|
|
2378
2764
|
schemaVersion: 1,
|
|
2379
2765
|
productModelId,
|
|
@@ -2381,6 +2767,8 @@ export function resolveTurnExecutionPolicyV1(
|
|
|
2381
2767
|
modelSource: input.modelSource,
|
|
2382
2768
|
reasoningEffort: input.reasoningEffort,
|
|
2383
2769
|
reasoningSource: input.reasoningSource,
|
|
2770
|
+
latencyMode,
|
|
2771
|
+
latencyModeSource,
|
|
2384
2772
|
providerId: resolved.provider.id,
|
|
2385
2773
|
upstreamModelId: resolved.model.upstreamModelId,
|
|
2386
2774
|
wireApi: resolved.model.api,
|
|
@@ -2401,6 +2789,7 @@ export function assertTurnExecutionPolicyMatchesConfigV1(
|
|
|
2401
2789
|
expected: {
|
|
2402
2790
|
modelId: string;
|
|
2403
2791
|
reasoningEffort: Settings["openaiReasoningEffort"];
|
|
2792
|
+
latencyMode?: LatencyMode;
|
|
2404
2793
|
},
|
|
2405
2794
|
): {
|
|
2406
2795
|
policy: TurnExecutionPolicyV1;
|
|
@@ -2410,12 +2799,17 @@ export function assertTurnExecutionPolicyMatchesConfigV1(
|
|
|
2410
2799
|
const parsed = TurnExecutionPolicyV1.parse(policy);
|
|
2411
2800
|
const catalogSettings = settingsForTurnExecutionPolicy(settings, parsed.productModelId);
|
|
2412
2801
|
const canonicalExpectedModel = canonicalizeConfiguredModelId(catalogSettings, expected.modelId);
|
|
2802
|
+
const expectedLatencyMode = expected.latencyMode ?? parsed.latencyMode;
|
|
2413
2803
|
if (
|
|
2414
2804
|
parsed.productModelId !== canonicalExpectedModel ||
|
|
2415
|
-
parsed.reasoningEffort !== expected.reasoningEffort
|
|
2805
|
+
parsed.reasoningEffort !== expected.reasoningEffort ||
|
|
2806
|
+
parsed.latencyMode !== expectedLatencyMode
|
|
2416
2807
|
) {
|
|
2417
|
-
throw new Error(
|
|
2808
|
+
throw new Error(
|
|
2809
|
+
"Turn execution policy does not match the accepted turn model/reasoning/latency",
|
|
2810
|
+
);
|
|
2418
2811
|
}
|
|
2812
|
+
assertLatencyModeRunnable(catalogSettings, parsed.productModelId, parsed.latencyMode);
|
|
2419
2813
|
if (
|
|
2420
2814
|
parsed.requestedModelId !== null &&
|
|
2421
2815
|
canonicalizeConfiguredModelId(catalogSettings, parsed.requestedModelId) !==
|
|
@@ -2451,7 +2845,10 @@ export function configuredModelPricingSchedules(
|
|
|
2451
2845
|
settings: Settings,
|
|
2452
2846
|
): Record<string, ModelPricingScheduleV1> {
|
|
2453
2847
|
const defaults = Object.fromEntries(
|
|
2454
|
-
Object.entries(defaultModelPricing).map(([model, pricing]) => [
|
|
2848
|
+
Object.entries(defaultModelPricing).map(([model, pricing]) => [
|
|
2849
|
+
model,
|
|
2850
|
+
normalizeModelPricingSchedule(pricing),
|
|
2851
|
+
]),
|
|
2455
2852
|
);
|
|
2456
2853
|
const registry: Record<string, ModelPricingScheduleV1> = {};
|
|
2457
2854
|
for (const provider of parseModelProvidersJson(settings.modelProvidersJson)) {
|
|
@@ -2578,6 +2975,7 @@ export function calculateModelUsageCostMicros(
|
|
|
2578
2975
|
settings: Settings,
|
|
2579
2976
|
model: string,
|
|
2580
2977
|
usage: ModelUsageInput,
|
|
2978
|
+
options?: { latencyMode?: LatencyMode },
|
|
2581
2979
|
): number {
|
|
2582
2980
|
const schedule = configuredModelPricingSchedules(settings)[model];
|
|
2583
2981
|
if (!schedule) {
|
|
@@ -2600,6 +2998,20 @@ export function calculateModelUsageCostMicros(
|
|
|
2600
2998
|
const marginBps = pricing.marginBps ?? 0;
|
|
2601
2999
|
total += Math.ceil((rawCost * (10_000 + marginBps)) / 10_000);
|
|
2602
3000
|
}
|
|
3001
|
+
const latencyMode = options?.latencyMode ?? "standard";
|
|
3002
|
+
if (latencyMode !== "standard") {
|
|
3003
|
+
const catalogSettings = settingsForTurnExecutionPolicy(settings, model);
|
|
3004
|
+
const resolved = resolveModelProvider(
|
|
3005
|
+
catalogSettings,
|
|
3006
|
+
canonicalizeConfiguredModelId(catalogSettings, model),
|
|
3007
|
+
);
|
|
3008
|
+
const multiplierBps = resolved?.model.capabilities.latencyModes.find(
|
|
3009
|
+
(mode) => mode.id === latencyMode && mode.runnable,
|
|
3010
|
+
)?.billingMultiplierBps;
|
|
3011
|
+
if (multiplierBps && multiplierBps > 0) {
|
|
3012
|
+
total = Math.ceil((total * multiplierBps) / 10_000);
|
|
3013
|
+
}
|
|
3014
|
+
}
|
|
2603
3015
|
return total;
|
|
2604
3016
|
}
|
|
2605
3017
|
|
|
@@ -3428,6 +3840,31 @@ function validateSettings(settings: Settings): void {
|
|
|
3428
3840
|
);
|
|
3429
3841
|
}
|
|
3430
3842
|
}
|
|
3843
|
+
if (Boolean(settings.googleDriveClientId) !== Boolean(settings.googleDriveClientSecret)) {
|
|
3844
|
+
throw new Error(
|
|
3845
|
+
"OPENGENI_GOOGLE_DRIVE_CLIENT_ID and OPENGENI_GOOGLE_DRIVE_CLIENT_SECRET must be configured together",
|
|
3846
|
+
);
|
|
3847
|
+
}
|
|
3848
|
+
if (settings.googleDriveClientId) {
|
|
3849
|
+
if (!settings.publicBaseUrl) {
|
|
3850
|
+
throw new Error(
|
|
3851
|
+
"OPENGENI_PUBLIC_BASE_URL is required when the Google Drive integration is configured",
|
|
3852
|
+
);
|
|
3853
|
+
}
|
|
3854
|
+
if (
|
|
3855
|
+
!settings.publicBaseUrl.startsWith("https://") &&
|
|
3856
|
+
!["local", "test"].includes(settings.environment)
|
|
3857
|
+
) {
|
|
3858
|
+
throw new Error(
|
|
3859
|
+
"OPENGENI_PUBLIC_BASE_URL must use https when the Google Drive integration is configured outside local/test",
|
|
3860
|
+
);
|
|
3861
|
+
}
|
|
3862
|
+
if (!settings.integrationsStateSecret) {
|
|
3863
|
+
throw new Error(
|
|
3864
|
+
"OPENGENI_INTEGRATIONS_STATE_SECRET is required when the Google Drive integration is configured",
|
|
3865
|
+
);
|
|
3866
|
+
}
|
|
3867
|
+
}
|
|
3431
3868
|
parseIntegrationsOauthClientsJson(settings.integrationsOauthClientsJson);
|
|
3432
3869
|
if (
|
|
3433
3870
|
settings.productAccessMode === "configured" &&
|
|
@@ -3534,7 +3971,9 @@ function validateSettings(settings: Settings): void {
|
|
|
3534
3971
|
}
|
|
3535
3972
|
if (
|
|
3536
3973
|
settings.objectStorageBackend === "s3-compatible" &&
|
|
3537
|
-
(settings.objectStorageEndpoint ||
|
|
3974
|
+
(settings.objectStorageEndpoint ||
|
|
3975
|
+
settings.objectStorageInternalEndpoint ||
|
|
3976
|
+
settings.objectStorageSandboxEndpoint) &&
|
|
3538
3977
|
(!settings.objectStorageAccessKeyId || !settings.objectStorageSecretAccessKey)
|
|
3539
3978
|
) {
|
|
3540
3979
|
throw new Error(
|
|
@@ -3564,6 +4003,7 @@ function validateSettings(settings: Settings): void {
|
|
|
3564
4003
|
} else if (settings.objectStorageBackend === "azure-blob") {
|
|
3565
4004
|
if (
|
|
3566
4005
|
settings.objectStorageEndpoint ||
|
|
4006
|
+
settings.objectStorageInternalEndpoint ||
|
|
3567
4007
|
settings.objectStorageSandboxEndpoint ||
|
|
3568
4008
|
settings.objectStorageAccessKeyId ||
|
|
3569
4009
|
settings.objectStorageSecretAccessKey
|
|
@@ -3594,6 +4034,7 @@ function validateSettings(settings: Settings): void {
|
|
|
3594
4034
|
} else {
|
|
3595
4035
|
if (
|
|
3596
4036
|
settings.objectStorageEndpoint ||
|
|
4037
|
+
settings.objectStorageInternalEndpoint ||
|
|
3597
4038
|
settings.objectStorageSandboxEndpoint ||
|
|
3598
4039
|
settings.objectStorageAccessKeyId ||
|
|
3599
4040
|
settings.objectStorageSecretAccessKey
|
|
@@ -3642,12 +4083,13 @@ function validateSettings(settings: Settings): void {
|
|
|
3642
4083
|
// out from under us — the provider lifetime is the backstop, not the
|
|
3643
4084
|
// warm-window controller. idleGrace counts from the user's last release;
|
|
3644
4085
|
// the provider clock counts from the preceding resume, so we leave the
|
|
3645
|
-
// active-turn headroom in modalTimeoutSeconds (default
|
|
4086
|
+
// active-turn headroom in modalTimeoutSeconds (default 86400s).
|
|
3646
4087
|
{
|
|
3647
4088
|
const reaperPeriod = settings.sandboxLeaseReaperPeriodMs;
|
|
3648
4089
|
const viewerTtl = settings.sandboxViewerHolderTtlMs;
|
|
3649
4090
|
const idleGraceMs = settings.sandboxIdleGraceMs;
|
|
3650
4091
|
const providerLifetimeMs = settings.modalTimeoutSeconds * 1000;
|
|
4092
|
+
const rotationLeadMs = settings.sandboxRotationLeadMs;
|
|
3651
4093
|
// The EFFECTIVE box lifetime when it sits idle between turns is the Modal IDLE
|
|
3652
4094
|
// timeout, NOT the hard lifetime (sandbox-file-persistence): a box with no
|
|
3653
4095
|
// active connection is idle-reaped at idleTimeout. effectiveModalIdleTimeout
|
|
@@ -3670,6 +4112,18 @@ function validateSettings(settings: Settings): void {
|
|
|
3670
4112
|
`floor under the hard lifetime, not above it.`,
|
|
3671
4113
|
);
|
|
3672
4114
|
}
|
|
4115
|
+
if (!(rotationLeadMs < providerLifetimeMs)) {
|
|
4116
|
+
throw new Error(
|
|
4117
|
+
`OPENGENI_SANDBOX_ROTATION_LEAD_MS (${rotationLeadMs}) must be strictly less than ` +
|
|
4118
|
+
`OPENGENI_MODAL_TIMEOUT_SECONDS*1000 (${providerLifetimeMs}).`,
|
|
4119
|
+
);
|
|
4120
|
+
}
|
|
4121
|
+
if (!(rotationLeadMs > settings.sandboxSnapshotTimeoutMs + 2 * reaperPeriod)) {
|
|
4122
|
+
throw new Error(
|
|
4123
|
+
`OPENGENI_SANDBOX_ROTATION_LEAD_MS (${rotationLeadMs}) must exceed the snapshot timeout ` +
|
|
4124
|
+
`plus two reaper periods (${settings.sandboxSnapshotTimeoutMs + 2 * reaperPeriod}).`,
|
|
4125
|
+
);
|
|
4126
|
+
}
|
|
3673
4127
|
if (!(viewerTtl < idleTimeoutMs)) {
|
|
3674
4128
|
throw new Error(
|
|
3675
4129
|
`OPENGENI_SANDBOX_VIEWER_HOLDER_TTL_MS (${viewerTtl}) must be strictly less than the effective box ` +
|