@opengeni/config 0.12.1 → 0.13.2
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 +69 -3
- package/dist/index.js +219 -50
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/index.ts +330 -50
package/src/index.ts
CHANGED
|
@@ -9,12 +9,14 @@ import {
|
|
|
9
9
|
ReasoningEffort,
|
|
10
10
|
SandboxBackend,
|
|
11
11
|
SessionMcpApprovalPolicy,
|
|
12
|
+
SEEDANCE_2_5_MODEL_ID,
|
|
12
13
|
StaticUsageLimits,
|
|
13
14
|
TurnExecutionPolicyV1,
|
|
14
15
|
UsageLimitsMode,
|
|
15
16
|
type TurnExecutionLatencyModeSourceV1,
|
|
16
17
|
type TurnExecutionModelSourceV1,
|
|
17
18
|
type TurnExecutionReasoningSourceV1,
|
|
19
|
+
type VideoGenerationResolution,
|
|
18
20
|
} from "@opengeni/contracts";
|
|
19
21
|
import { CODEX_MODEL_TOOL_OUTPUT_TRUNCATION_TOKENS } from "@opengeni/codex";
|
|
20
22
|
import {
|
|
@@ -31,6 +33,24 @@ import { z } from "zod";
|
|
|
31
33
|
|
|
32
34
|
const envName = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
33
35
|
const registryId = /^[A-Za-z0-9_-]+$/;
|
|
36
|
+
|
|
37
|
+
// Archive capture claims are also the admission/teardown fence around a
|
|
38
|
+
// provider snapshot. Keep a real settlement window after the provider request;
|
|
39
|
+
// a configured request timeout may never consume the entire durable claim.
|
|
40
|
+
export const SANDBOX_ARCHIVE_CAPTURE_MAX_TIMEOUT_MS = 60 * 60_000;
|
|
41
|
+
export const SANDBOX_ARCHIVE_CAPTURE_SETTLEMENT_GRACE_MS = 10_000;
|
|
42
|
+
export const SANDBOX_SNAPSHOT_MAX_TIMEOUT_MS =
|
|
43
|
+
SANDBOX_ARCHIVE_CAPTURE_MAX_TIMEOUT_MS - SANDBOX_ARCHIVE_CAPTURE_SETTLEMENT_GRACE_MS;
|
|
44
|
+
// Admission waits are observational: successful capture/teardown returns as
|
|
45
|
+
// soon as the DB fence clears. This ceiling only covers the unhealthy path. It
|
|
46
|
+
// allows one scheduled inventory and one complete successor claim without
|
|
47
|
+
// turning a recoverable lifecycle transition into a visible caller error. A
|
|
48
|
+
// dead holder's TTL is deliberately NOT included: admission cannot accelerate
|
|
49
|
+
// that proof, and the turn moves to durable recovery if holder quiescence takes
|
|
50
|
+
// longer than this observational wait. The outer cap remains an explicit
|
|
51
|
+
// request-resource boundary; successful transitions return immediately.
|
|
52
|
+
export const SANDBOX_LIFECYCLE_TRANSITION_MAX_WAIT_MS = 60 * 60_000;
|
|
53
|
+
export const SANDBOX_LIFECYCLE_RETRY_HANDOFF_GRACE_MS = 10_000;
|
|
34
54
|
const EnvBoolean = z.preprocess((value) => {
|
|
35
55
|
if (typeof value !== "string") {
|
|
36
56
|
return value;
|
|
@@ -214,6 +234,12 @@ const SettingsSchema = z.object({
|
|
|
214
234
|
turnWorkerMaxConcurrentTurns: z.coerce.number().int().positive().max(2_000).default(16),
|
|
215
235
|
turnWorkerTargetCpuUsage: z.coerce.number().positive().max(1).default(0.8),
|
|
216
236
|
turnWorkerTargetMemoryUsage: z.coerce.number().positive().max(0.8).default(0.75),
|
|
237
|
+
// Admission and emergency recovery are deliberately separate control loops.
|
|
238
|
+
// The Temporal tuner stops polling at the lower target; only genuine danger
|
|
239
|
+
// may invoke the disruptive graceful-drain fallback.
|
|
240
|
+
turnWorkerEmergencyMemoryUsage: z.coerce.number().min(0.85).max(0.95).default(0.9),
|
|
241
|
+
turnWorkerMemoryGuardIntervalMs: z.coerce.number().int().min(1_000).max(60_000).default(5_000),
|
|
242
|
+
turnWorkerMemoryGuardSustainMs: z.coerce.number().int().min(5_000).max(300_000).default(30_000),
|
|
217
243
|
observabilityStructuredLogs: EnvBoolean.default(false),
|
|
218
244
|
observabilityMetricsEnabled: EnvBoolean.default(true),
|
|
219
245
|
observabilityOtlpEndpoint: z.string().url().optional(),
|
|
@@ -249,7 +275,7 @@ const SettingsSchema = z.object({
|
|
|
249
275
|
agentStableVersion: z
|
|
250
276
|
.string()
|
|
251
277
|
.regex(/^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)$/u)
|
|
252
|
-
.default("0.1.
|
|
278
|
+
.default("0.1.14"),
|
|
253
279
|
// Optional independent beta-channel pointer. When unset, the beta update
|
|
254
280
|
// manifest route is unavailable rather than silently serving stable.
|
|
255
281
|
agentBetaVersion: z
|
|
@@ -273,8 +299,7 @@ const SettingsSchema = z.object({
|
|
|
273
299
|
// holder of stream:control gets 403 until this flips. Keeps stream:control a
|
|
274
300
|
// declared-but-inert permission so later hardening is a flag flip.
|
|
275
301
|
streamControlEnabled: EnvBoolean.default(false),
|
|
276
|
-
|
|
277
|
-
toolspaceMaxCallsPerTurn: z.coerce.number().int().positive().default(200),
|
|
302
|
+
codemodeMaxCallsPerTurn: z.coerce.number().int().positive().default(200),
|
|
278
303
|
// Optional release-coherent bootstrap hint for custom rigs/connected machines
|
|
279
304
|
// that do not carry the stock-image ogtool binary. Exact stable versions only:
|
|
280
305
|
// the agent must never guess a tag or silently install `latest`.
|
|
@@ -287,11 +312,15 @@ const SettingsSchema = z.object({
|
|
|
287
312
|
integrationsStateSecret: z.string().optional(),
|
|
288
313
|
integrationsAllowPrivateNetworkTargets: EnvBoolean.default(false),
|
|
289
314
|
integrationsOauthClientsJson: z.string().default("{}"),
|
|
315
|
+
gmailRestAdapterEnabled: EnvBoolean.default(false),
|
|
290
316
|
slackClientId: z.string().optional(),
|
|
291
317
|
slackClientSecret: z.string().optional(),
|
|
292
318
|
slackSigningSecret: z.string().optional(),
|
|
293
319
|
googleDriveClientId: z.string().optional(),
|
|
294
320
|
googleDriveClientSecret: z.string().optional(),
|
|
321
|
+
googleDriveWorkspaceEventsEnabled: EnvBoolean.optional(),
|
|
322
|
+
atlassianClientId: z.string().optional(),
|
|
323
|
+
atlassianClientSecret: z.string().optional(),
|
|
295
324
|
// Undefined is meaningful: the migration boundary persists the product
|
|
296
325
|
// default of 3 when no deployment override is supplied.
|
|
297
326
|
maxNestedAgentDepth: z.coerce.number().int().nonnegative().max(MAX_NESTED_AGENT_DEPTH).optional(),
|
|
@@ -365,6 +394,45 @@ const SettingsSchema = z.object({
|
|
|
365
394
|
// Gateway models below are added to the managed-credit catalog. Workspace
|
|
366
395
|
// Gateway keys use the encrypted connection broker and never this secret.
|
|
367
396
|
vercelAiGatewayApiKey: z.string().optional(),
|
|
397
|
+
/** Image adapter route; native hosted providers ignore this model. */
|
|
398
|
+
imageGenerationModel: z.string().trim().min(1).max(256).default("openai/gpt-image-2"),
|
|
399
|
+
/** Durable video generation uses the workspace-owned Gateway credential. */
|
|
400
|
+
videoGenerationPollIntervalMs: z.coerce.number().int().min(1_000).max(60_000).default(5_000),
|
|
401
|
+
videoGenerationRecoveryDeadlineMs: z.coerce
|
|
402
|
+
.number()
|
|
403
|
+
.int()
|
|
404
|
+
.min(60_000)
|
|
405
|
+
.max(24 * 60 * 60_000)
|
|
406
|
+
.default(2 * 60 * 60_000),
|
|
407
|
+
videoGenerationReferenceUrlTtlSeconds: z.coerce
|
|
408
|
+
.number()
|
|
409
|
+
.int()
|
|
410
|
+
.min(300)
|
|
411
|
+
.max(6 * 60 * 60)
|
|
412
|
+
.default(60 * 60),
|
|
413
|
+
videoGenerationMaxConcurrentPerWorkspace: z.coerce.number().int().min(1).max(16).default(2),
|
|
414
|
+
videoGenerationWorkspaceQuotaBytes: z.coerce
|
|
415
|
+
.number()
|
|
416
|
+
.int()
|
|
417
|
+
.positive()
|
|
418
|
+
.max(Number.MAX_SAFE_INTEGER)
|
|
419
|
+
.default(20 * 1024 * 1024 * 1024),
|
|
420
|
+
videoGenerationTempDirectory: z.string().trim().min(1).max(1_024).default("/tmp/opengeni-video"),
|
|
421
|
+
videoGenerationFfprobePath: z.string().trim().min(1).max(1_024).default("ffprobe"),
|
|
422
|
+
// OpenGeni's customer price, not a claim about the provider's delayed cost report.
|
|
423
|
+
// The durable operation freezes the exact resulting price before provider submit.
|
|
424
|
+
videoGenerationCredit480pMicrosPerSecond: z.coerce
|
|
425
|
+
.number()
|
|
426
|
+
.int()
|
|
427
|
+
.positive()
|
|
428
|
+
.max(10_000_000)
|
|
429
|
+
.default(155_000),
|
|
430
|
+
videoGenerationCredit720pMicrosPerSecond: z.coerce
|
|
431
|
+
.number()
|
|
432
|
+
.int()
|
|
433
|
+
.positive()
|
|
434
|
+
.max(10_000_000)
|
|
435
|
+
.default(350_000),
|
|
368
436
|
// Native composer voice input (browser MediaRecorder → API transcription).
|
|
369
437
|
// Provider credentials stay server-side; ClientConfig only projects availability
|
|
370
438
|
// and hard ceilings. Selection happens once before audio is sent — never retry
|
|
@@ -455,6 +523,11 @@ const SettingsSchema = z.object({
|
|
|
455
523
|
// compatibility diagnosis.
|
|
456
524
|
// OPENGENI_CODEX_TOOL_SEARCH_ENABLED
|
|
457
525
|
codexToolSearchEnabled: EnvBoolean.default(true),
|
|
526
|
+
// Provider-neutral progressive disclosure for direct OpenAI/Azure native
|
|
527
|
+
// client search and ordinary-function generic dispatch. Kept separate from
|
|
528
|
+
// the Codex rollout so an emergency Codex opt-out cannot disable every model.
|
|
529
|
+
// OPENGENI_LAZY_TOOL_SEARCH_ENABLED
|
|
530
|
+
lazyToolSearchEnabled: EnvBoolean.default(true),
|
|
458
531
|
// credential allocator atomic, workspace-local credential allocation. Default OFF is a
|
|
459
532
|
// deliberate rolling-deploy fence: migrate + roll every worker first, then
|
|
460
533
|
// enable. Turning it off restores the legacy sticky selector without a schema
|
|
@@ -512,6 +585,13 @@ const SettingsSchema = z.object({
|
|
|
512
585
|
disableOpenaiTracing: EnvBoolean.default(false),
|
|
513
586
|
sandboxBackend: SandboxBackend.default("docker"),
|
|
514
587
|
dockerImage: z.string().default("opengeni-sandbox:local"),
|
|
588
|
+
// Explicit deployment contract: the configured base sandbox image contains
|
|
589
|
+
// the verified, self-contained native artifact runtime at its fixed image
|
|
590
|
+
// paths. Disabled by default so arbitrary/custom provider images never make
|
|
591
|
+
// document/spreadsheet/presentation skills appear when their runtime is
|
|
592
|
+
// absent. Per-pack/per-rig image overrides fail closed in the worker even
|
|
593
|
+
// when this base-image contract is enabled.
|
|
594
|
+
sandboxArtifactRuntimeEnabled: EnvBoolean.default(false),
|
|
515
595
|
dockerExposedPorts: z.string().default(""),
|
|
516
596
|
dockerNetwork: z.string().optional(),
|
|
517
597
|
// When the worker itself runs in a container and talks to a host Docker daemon,
|
|
@@ -571,17 +651,16 @@ const SettingsSchema = z.object({
|
|
|
571
651
|
// SOONER than the hard lifetime; the boot invariant forbids a value that would
|
|
572
652
|
// reap before reaperPeriod + idleGrace elapses.
|
|
573
653
|
modalIdleTimeoutSeconds: z.coerce.number().int().positive().optional(),
|
|
574
|
-
// /workspace FILE PERSISTENCE across warm/cold cycles.
|
|
575
|
-
//
|
|
576
|
-
//
|
|
577
|
-
//
|
|
578
|
-
//
|
|
579
|
-
//
|
|
580
|
-
//
|
|
581
|
-
// the reaper persists a tar archive — same store+hydrate plumbing, slower).
|
|
654
|
+
// /workspace FILE PERSISTENCE across warm/cold cycles. Directory snapshots
|
|
655
|
+
// preserve only the durable user workspace, so provider recovery does not
|
|
656
|
+
// restore an entire machine image or replace the selected rig/base image.
|
|
657
|
+
// Existing serialized sessions retain their original persistence mode and
|
|
658
|
+
// remain recoverable; this default governs newly created Modal sandboxes.
|
|
659
|
+
// `snapshot_filesystem` remains available for explicit compatibility and
|
|
660
|
+
// immutable rig-image materialization. `tar` is the portable fallback.
|
|
582
661
|
modalWorkspacePersistence: z
|
|
583
662
|
.enum(["tar", "snapshot_filesystem", "snapshot_directory"])
|
|
584
|
-
.default("
|
|
663
|
+
.default("snapshot_directory"),
|
|
585
664
|
// Shared desktop toggle: this module reads it for the 6080 port-merge; the
|
|
586
665
|
// owner module (P4.x) acts on it to launch the display stack.
|
|
587
666
|
sandboxDesktopEnabled: EnvBoolean.default(false),
|
|
@@ -789,6 +868,10 @@ const SettingsSchema = z.object({
|
|
|
789
868
|
// snapshotted before the box dies (sandbox-file-persistence).
|
|
790
869
|
sandboxLeaseReaperPeriodMs: z.coerce.number().int().positive().default(30_000),
|
|
791
870
|
sandboxViewerHolderTtlMs: z.coerce.number().int().positive().default(90_000),
|
|
871
|
+
// A BrowserSession controller refreshes its durable resource and exact
|
|
872
|
+
// interaction lease holder together. This longer crash horizon tolerates API
|
|
873
|
+
// replacement while still releasing a placement whose controller died.
|
|
874
|
+
sandboxInteractionHolderTtlMs: z.coerce.number().int().positive().default(180_000),
|
|
792
875
|
// The DRAIN grace: how long a refcount-0 (draining) lease stays WARM before the
|
|
793
876
|
// reaper resume-by-ids the box and terminates it. This is the cost-vs-snappiness
|
|
794
877
|
// dial — when the user navigates away the box keeps refcount 0, but it survives
|
|
@@ -814,7 +897,12 @@ const SettingsSchema = z.object({
|
|
|
814
897
|
// graceful shutdown, or become permission to GC an older archive. Timeout is
|
|
815
898
|
// treated exactly like a failed best-effort snapshot. Knob:
|
|
816
899
|
// OPENGENI_SANDBOX_SNAPSHOT_TIMEOUT_MS. Default 60s.
|
|
817
|
-
sandboxSnapshotTimeoutMs: z.coerce
|
|
900
|
+
sandboxSnapshotTimeoutMs: z.coerce
|
|
901
|
+
.number()
|
|
902
|
+
.int()
|
|
903
|
+
.positive()
|
|
904
|
+
.max(SANDBOX_SNAPSHOT_MAX_TIMEOUT_MS)
|
|
905
|
+
.default(60_000),
|
|
818
906
|
// Begin a controlled snapshot/quiesce/drain/rematerialize transition this far
|
|
819
907
|
// ahead of a finite provider deadline. Modal's 24h creation clock cannot be
|
|
820
908
|
// extended; the logical sandbox outlives it by moving to one successor box.
|
|
@@ -824,13 +912,12 @@ const SettingsSchema = z.object({
|
|
|
824
912
|
// an operator deliberately wants more rotation headroom; the boot invariant
|
|
825
913
|
// still requires it to remain below the provider lifetime.
|
|
826
914
|
sandboxRotationLeadMs: z.coerce.number().int().positive().default(3_600_000),
|
|
827
|
-
// Bound
|
|
828
|
-
//
|
|
829
|
-
//
|
|
830
|
-
//
|
|
831
|
-
//
|
|
832
|
-
|
|
833
|
-
sandboxRotationBatchSize: z.coerce.number().int().positive().max(500).default(1),
|
|
915
|
+
// Bound provider-deadline rotation admission independently of execution.
|
|
916
|
+
// Every admitted box receives its own durable drain child; the control worker
|
|
917
|
+
// limits provider I/O to 32 concurrent activities. Matching that bound avoids
|
|
918
|
+
// both the old one-box-per-tick deadline backlog and an unbounded provider/API
|
|
919
|
+
// burst. Operators may tune this for a differently-sized worker pool.
|
|
920
|
+
sandboxRotationBatchSize: z.coerce.number().int().positive().max(500).default(32),
|
|
834
921
|
// expires_at refresh window for a held lease (>> the turn 10s heartbeat so a
|
|
835
922
|
// single missed heartbeat never TTL-reaps a live turn). The warming TTL is the
|
|
836
923
|
// window a cold->warming spawner has to commit warm before a reaper resets it.
|
|
@@ -1135,6 +1222,13 @@ export type ModelUsageInput = {
|
|
|
1135
1222
|
requestUsageEntries?: ModelUsageInput[] | undefined;
|
|
1136
1223
|
};
|
|
1137
1224
|
|
|
1225
|
+
export type ModelUsageCostBreakdown = {
|
|
1226
|
+
/** Provider-rate cost basis for the exact usage, before OpenGeni margin. */
|
|
1227
|
+
providerCostMicros: number;
|
|
1228
|
+
/** OpenGeni credit price after configured margin and latency-mode multiplier. */
|
|
1229
|
+
creditCostMicros: number;
|
|
1230
|
+
};
|
|
1231
|
+
|
|
1138
1232
|
export type StaticUsageLimitsConfig = StaticUsageLimits;
|
|
1139
1233
|
export type EntitlementsConfig = Entitlements;
|
|
1140
1234
|
|
|
@@ -1206,6 +1300,10 @@ export const ModelCapabilitiesV1Schema = z
|
|
|
1206
1300
|
webSearch: CapabilityStateV1Schema,
|
|
1207
1301
|
xSearch: CapabilityStateV1Schema,
|
|
1208
1302
|
codeExecution: CapabilityStateV1Schema,
|
|
1303
|
+
imageGeneration: CapabilityStateV1Schema.default({
|
|
1304
|
+
upstream: "unknown",
|
|
1305
|
+
runnable: false,
|
|
1306
|
+
}),
|
|
1209
1307
|
}),
|
|
1210
1308
|
inputModalities: z.array(ModelModalityV1).min(1),
|
|
1211
1309
|
/** Exact MIME types accepted as typed `input_file`; `text/*` is allowed. */
|
|
@@ -1742,6 +1840,9 @@ export function getSettings(): Settings {
|
|
|
1742
1840
|
turnWorkerMaxConcurrentTurns: optional("OPENGENI_TURN_WORKER_MAX_CONCURRENT_TURNS"),
|
|
1743
1841
|
turnWorkerTargetCpuUsage: optional("OPENGENI_TURN_WORKER_TARGET_CPU_USAGE"),
|
|
1744
1842
|
turnWorkerTargetMemoryUsage: optional("OPENGENI_TURN_WORKER_TARGET_MEMORY_USAGE"),
|
|
1843
|
+
turnWorkerEmergencyMemoryUsage: optional("OPENGENI_TURN_WORKER_EMERGENCY_MEMORY_USAGE"),
|
|
1844
|
+
turnWorkerMemoryGuardIntervalMs: optional("OPENGENI_TURN_WORKER_MEMORY_GUARD_INTERVAL_MS"),
|
|
1845
|
+
turnWorkerMemoryGuardSustainMs: optional("OPENGENI_TURN_WORKER_MEMORY_GUARD_SUSTAIN_MS"),
|
|
1745
1846
|
observabilityStructuredLogs: optional("OPENGENI_OBSERVABILITY_STRUCTURED_LOGS"),
|
|
1746
1847
|
observabilityMetricsEnabled: optional("OPENGENI_OBSERVABILITY_METRICS_ENABLED"),
|
|
1747
1848
|
observabilityOtlpEndpoint:
|
|
@@ -1768,8 +1869,7 @@ export function getSettings(): Settings {
|
|
|
1768
1869
|
delegationSecret: optional("OPENGENI_DELEGATION_SECRET"),
|
|
1769
1870
|
streamTokenSecret: optional("OPENGENI_STREAM_TOKEN_SECRET"),
|
|
1770
1871
|
streamControlEnabled: optional("OPENGENI_STREAM_CONTROL_ENABLED"),
|
|
1771
|
-
|
|
1772
|
-
toolspaceMaxCallsPerTurn: optional("OPENGENI_TOOLSPACE_MAX_CALLS_PER_TURN"),
|
|
1872
|
+
codemodeMaxCallsPerTurn: optional("OPENGENI_CODEMODE_MAX_CALLS_PER_TURN"),
|
|
1773
1873
|
ogtoolPackageSpec: optional("OPENGENI_OGTOOL_PACKAGE_SPEC"),
|
|
1774
1874
|
environmentsEncryptionKey: optional("OPENGENI_ENVIRONMENTS_ENCRYPTION_KEY"),
|
|
1775
1875
|
integrationsEnabled: optional("OPENGENI_INTEGRATIONS_ENABLED"),
|
|
@@ -1778,11 +1878,15 @@ export function getSettings(): Settings {
|
|
|
1778
1878
|
"OPENGENI_INTEGRATIONS_ALLOW_PRIVATE_NETWORK_TARGETS",
|
|
1779
1879
|
),
|
|
1780
1880
|
integrationsOauthClientsJson: optional("OPENGENI_INTEGRATIONS_OAUTH_CLIENTS_JSON"),
|
|
1881
|
+
gmailRestAdapterEnabled: optional("OPENGENI_GMAIL_REST_ADAPTER_ENABLED"),
|
|
1781
1882
|
slackClientId: optional("OPENGENI_SLACK_CLIENT_ID"),
|
|
1782
1883
|
slackClientSecret: optional("OPENGENI_SLACK_CLIENT_SECRET"),
|
|
1783
1884
|
slackSigningSecret: optional("OPENGENI_SLACK_SIGNING_SECRET"),
|
|
1784
1885
|
googleDriveClientId: optional("OPENGENI_GOOGLE_DRIVE_CLIENT_ID"),
|
|
1785
1886
|
googleDriveClientSecret: optional("OPENGENI_GOOGLE_DRIVE_CLIENT_SECRET"),
|
|
1887
|
+
googleDriveWorkspaceEventsEnabled: optional("OPENGENI_GOOGLE_DRIVE_WORKSPACE_EVENTS_ENABLED"),
|
|
1888
|
+
atlassianClientId: optional("OPENGENI_ATLASSIAN_CLIENT_ID"),
|
|
1889
|
+
atlassianClientSecret: optional("OPENGENI_ATLASSIAN_CLIENT_SECRET"),
|
|
1786
1890
|
maxNestedAgentDepth: optional("OPENGENI_MAX_NESTED_AGENT_DEPTH"),
|
|
1787
1891
|
socialOauthClientsJson: optional("OPENGENI_SOCIAL_OAUTH_CLIENTS_JSON"),
|
|
1788
1892
|
goalMaxAutoContinuations: optional("OPENGENI_GOAL_MAX_AUTO_CONTINUATIONS"),
|
|
@@ -1809,6 +1913,24 @@ export function getSettings(): Settings {
|
|
|
1809
1913
|
openaiModel: optional("OPENGENI_OPENAI_MODEL"),
|
|
1810
1914
|
openaiAllowedModels: optional("OPENGENI_OPENAI_ALLOWED_MODELS"),
|
|
1811
1915
|
vercelAiGatewayApiKey: optional("OPENGENI_VERCEL_AI_GATEWAY_API_KEY"),
|
|
1916
|
+
imageGenerationModel: optional("OPENGENI_IMAGE_GENERATION_MODEL"),
|
|
1917
|
+
videoGenerationPollIntervalMs: optional("OPENGENI_VIDEO_GENERATION_POLL_INTERVAL_MS"),
|
|
1918
|
+
videoGenerationRecoveryDeadlineMs: optional("OPENGENI_VIDEO_GENERATION_RECOVERY_DEADLINE_MS"),
|
|
1919
|
+
videoGenerationReferenceUrlTtlSeconds: optional(
|
|
1920
|
+
"OPENGENI_VIDEO_GENERATION_REFERENCE_URL_TTL_SECONDS",
|
|
1921
|
+
),
|
|
1922
|
+
videoGenerationMaxConcurrentPerWorkspace: optional(
|
|
1923
|
+
"OPENGENI_VIDEO_GENERATION_MAX_CONCURRENT_PER_WORKSPACE",
|
|
1924
|
+
),
|
|
1925
|
+
videoGenerationWorkspaceQuotaBytes: optional("OPENGENI_VIDEO_GENERATION_WORKSPACE_QUOTA_BYTES"),
|
|
1926
|
+
videoGenerationTempDirectory: optional("OPENGENI_VIDEO_GENERATION_TEMP_DIRECTORY"),
|
|
1927
|
+
videoGenerationFfprobePath: optional("OPENGENI_VIDEO_GENERATION_FFPROBE_PATH"),
|
|
1928
|
+
videoGenerationCredit480pMicrosPerSecond: optional(
|
|
1929
|
+
"OPENGENI_VIDEO_GENERATION_CREDIT_480P_MICROS_PER_SECOND",
|
|
1930
|
+
),
|
|
1931
|
+
videoGenerationCredit720pMicrosPerSecond: optional(
|
|
1932
|
+
"OPENGENI_VIDEO_GENERATION_CREDIT_720P_MICROS_PER_SECOND",
|
|
1933
|
+
),
|
|
1812
1934
|
voiceInputMaxDurationSeconds: optional("OPENGENI_VOICE_INPUT_MAX_DURATION_SECONDS"),
|
|
1813
1935
|
voiceInputMaxSizeBytes: optional("OPENGENI_VOICE_INPUT_MAX_SIZE_BYTES"),
|
|
1814
1936
|
voiceInputResumableEnabled: optional("OPENGENI_VOICE_INPUT_RESUMABLE_ENABLED"),
|
|
@@ -1840,6 +1962,7 @@ export function getSettings(): Settings {
|
|
|
1840
1962
|
codexSubscriptionEnabled: optional("OPENGENI_CODEX_SUBSCRIPTION_ENABLED"),
|
|
1841
1963
|
codexConnectedAppsEnabled: optional("OPENGENI_CODEX_CONNECTED_APPS_ENABLED"),
|
|
1842
1964
|
codexToolSearchEnabled: optional("OPENGENI_CODEX_TOOL_SEARCH_ENABLED"),
|
|
1965
|
+
lazyToolSearchEnabled: optional("OPENGENI_LAZY_TOOL_SEARCH_ENABLED"),
|
|
1843
1966
|
codexCredentialLeasingEnabled: optional("OPENGENI_CODEX_CREDENTIAL_LEASING_ENABLED"),
|
|
1844
1967
|
codexFleetPolicyShadowEnabled: optional("OPENGENI_CODEX_FLEET_POLICY_SHADOW_ENABLED"),
|
|
1845
1968
|
codexProductSku: optional("OPENGENI_CODEX_PRODUCT_SKU"),
|
|
@@ -1860,6 +1983,7 @@ export function getSettings(): Settings {
|
|
|
1860
1983
|
disableOpenaiTracing: optional("OPENGENI_DISABLE_OPENAI_TRACING"),
|
|
1861
1984
|
sandboxBackend: optional("OPENGENI_SANDBOX_BACKEND"),
|
|
1862
1985
|
dockerImage: optional("OPENGENI_DOCKER_IMAGE"),
|
|
1986
|
+
sandboxArtifactRuntimeEnabled: optional("OPENGENI_SANDBOX_ARTIFACT_RUNTIME_ENABLED"),
|
|
1863
1987
|
dockerExposedPorts: optional("OPENGENI_DOCKER_EXPOSED_PORTS"),
|
|
1864
1988
|
dockerNetwork: optional("OPENGENI_DOCKER_NETWORK"),
|
|
1865
1989
|
dockerWorkspaceBaseDir: optional("OPENGENI_DOCKER_WORKSPACE_BASE_DIR"),
|
|
@@ -1942,6 +2066,7 @@ export function getSettings(): Settings {
|
|
|
1942
2066
|
sandboxSelfhostedControlTimeoutMs: optional("OPENGENI_SANDBOX_SELFHOSTED_CONTROL_TIMEOUT_MS"),
|
|
1943
2067
|
sandboxLeaseReaperPeriodMs: optional("OPENGENI_SANDBOX_LEASE_REAPER_PERIOD_MS"),
|
|
1944
2068
|
sandboxViewerHolderTtlMs: optional("OPENGENI_SANDBOX_VIEWER_HOLDER_TTL_MS"),
|
|
2069
|
+
sandboxInteractionHolderTtlMs: optional("OPENGENI_SANDBOX_INTERACTION_HOLDER_TTL_MS"),
|
|
1945
2070
|
sandboxIdleGraceMs: optional("OPENGENI_SANDBOX_IDLE_GRACE_MS"),
|
|
1946
2071
|
sandboxSnapshotIntervalMs: optional("OPENGENI_SANDBOX_SNAPSHOT_INTERVAL_MS"),
|
|
1947
2072
|
sandboxSnapshotTimeoutMs: optional("OPENGENI_SANDBOX_SNAPSHOT_TIMEOUT_MS"),
|
|
@@ -2072,8 +2197,20 @@ export function sandboxArchiveCaptureTimeoutMs(
|
|
|
2072
2197
|
settings: Pick<Settings, "sandboxSnapshotTimeoutMs">,
|
|
2073
2198
|
): number {
|
|
2074
2199
|
return Math.min(
|
|
2075
|
-
|
|
2076
|
-
|
|
2200
|
+
SANDBOX_ARCHIVE_CAPTURE_MAX_TIMEOUT_MS,
|
|
2201
|
+
settings.sandboxSnapshotTimeoutMs + SANDBOX_ARCHIVE_CAPTURE_SETTLEMENT_GRACE_MS,
|
|
2202
|
+
);
|
|
2203
|
+
}
|
|
2204
|
+
|
|
2205
|
+
export function sandboxLifecycleTransitionWaitMs(
|
|
2206
|
+
settings: Pick<Settings, "sandboxSnapshotTimeoutMs" | "sandboxLeaseReaperPeriodMs">,
|
|
2207
|
+
): number {
|
|
2208
|
+
const captureTimeoutMs = sandboxArchiveCaptureTimeoutMs(settings);
|
|
2209
|
+
return Math.min(
|
|
2210
|
+
SANDBOX_LIFECYCLE_TRANSITION_MAX_WAIT_MS,
|
|
2211
|
+
settings.sandboxLeaseReaperPeriodMs +
|
|
2212
|
+
captureTimeoutMs +
|
|
2213
|
+
SANDBOX_LIFECYCLE_RETRY_HANDOFF_GRACE_MS,
|
|
2077
2214
|
);
|
|
2078
2215
|
}
|
|
2079
2216
|
|
|
@@ -2336,7 +2473,12 @@ function normalizeCapabilities(capabilities: ModelCapabilitiesV1): ModelCapabili
|
|
|
2336
2473
|
|
|
2337
2474
|
function legacyModelCapabilities(
|
|
2338
2475
|
settings: Settings,
|
|
2339
|
-
input: {
|
|
2476
|
+
input: {
|
|
2477
|
+
reasoningEffort: boolean;
|
|
2478
|
+
hostedWebSearch: boolean;
|
|
2479
|
+
hostedImageGeneration?: boolean;
|
|
2480
|
+
vision?: boolean;
|
|
2481
|
+
},
|
|
2340
2482
|
): ModelCapabilitiesV1 {
|
|
2341
2483
|
const reasoningEfforts = input.reasoningEffort ? configuredAllowedReasoningEfforts(settings) : [];
|
|
2342
2484
|
return normalizeCapabilities({
|
|
@@ -2356,6 +2498,10 @@ function legacyModelCapabilities(
|
|
|
2356
2498
|
},
|
|
2357
2499
|
xSearch: { upstream: "unknown", runnable: false },
|
|
2358
2500
|
codeExecution: { upstream: "unknown", runnable: false },
|
|
2501
|
+
imageGeneration: {
|
|
2502
|
+
upstream: input.hostedImageGeneration ? "supported" : "unknown",
|
|
2503
|
+
runnable: input.hostedImageGeneration ?? false,
|
|
2504
|
+
},
|
|
2359
2505
|
},
|
|
2360
2506
|
inputModalities: input.vision ? ["text", "image"] : ["text"],
|
|
2361
2507
|
inputFileMediaTypes: [
|
|
@@ -2588,6 +2734,35 @@ function builtinPromptCachingForModel(
|
|
|
2588
2734
|
: undefined;
|
|
2589
2735
|
}
|
|
2590
2736
|
|
|
2737
|
+
/** Reviewed direct-OpenAI text models that accept the hosted image tool. */
|
|
2738
|
+
function builtinHostedImageGenerationForModel(settings: Settings, modelId: string): boolean {
|
|
2739
|
+
return (
|
|
2740
|
+
settings.openaiProvider === "openai" &&
|
|
2741
|
+
isDirectOpenAiApiBaseUrl(settings.openaiBaseUrl) &&
|
|
2742
|
+
["gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna"].includes(modelId)
|
|
2743
|
+
);
|
|
2744
|
+
}
|
|
2745
|
+
|
|
2746
|
+
/** Undefined and the exact public OpenAI v1 endpoint are the same direct route. */
|
|
2747
|
+
export function isDirectOpenAiApiBaseUrl(baseUrl: string | undefined): boolean {
|
|
2748
|
+
if (baseUrl === undefined) return true;
|
|
2749
|
+
try {
|
|
2750
|
+
const parsed = new URL(baseUrl);
|
|
2751
|
+
return (
|
|
2752
|
+
parsed.protocol === "https:" &&
|
|
2753
|
+
parsed.hostname === "api.openai.com" &&
|
|
2754
|
+
parsed.port === "" &&
|
|
2755
|
+
parsed.username === "" &&
|
|
2756
|
+
parsed.password === "" &&
|
|
2757
|
+
parsed.search === "" &&
|
|
2758
|
+
parsed.hash === "" &&
|
|
2759
|
+
parsed.pathname.replace(/\/+$/, "") === "/v1"
|
|
2760
|
+
);
|
|
2761
|
+
} catch {
|
|
2762
|
+
return false;
|
|
2763
|
+
}
|
|
2764
|
+
}
|
|
2765
|
+
|
|
2591
2766
|
/**
|
|
2592
2767
|
* Map OpenGeni latency mode to the provider `service_tier` wire value.
|
|
2593
2768
|
* Azure and Codex ChatGPT accept `priority`; OpenAI API accepts `fast` (alias of priority).
|
|
@@ -3028,6 +3203,7 @@ export function configuredModels(settings: Settings): ConfiguredModel[] {
|
|
|
3028
3203
|
...legacyModelCapabilities(settings, {
|
|
3029
3204
|
reasoningEffort: true,
|
|
3030
3205
|
hostedWebSearch: settings.webSearchEnabled,
|
|
3206
|
+
hostedImageGeneration: builtinHostedImageGenerationForModel(settings, id),
|
|
3031
3207
|
vision: id.startsWith("gpt-5.6-"),
|
|
3032
3208
|
}),
|
|
3033
3209
|
...(builtinPromptCachingForModel(id)
|
|
@@ -3424,6 +3600,15 @@ export function calculateModelUsageCostMicros(
|
|
|
3424
3600
|
usage: ModelUsageInput,
|
|
3425
3601
|
options?: { latencyMode?: LatencyMode },
|
|
3426
3602
|
): number {
|
|
3603
|
+
return calculateModelUsageCostBreakdown(settings, model, usage, options).creditCostMicros;
|
|
3604
|
+
}
|
|
3605
|
+
|
|
3606
|
+
export function calculateModelUsageCostBreakdown(
|
|
3607
|
+
settings: Settings,
|
|
3608
|
+
model: string,
|
|
3609
|
+
usage: ModelUsageInput,
|
|
3610
|
+
options?: { latencyMode?: LatencyMode },
|
|
3611
|
+
): ModelUsageCostBreakdown {
|
|
3427
3612
|
const schedule = configuredModelPricingSchedules(settings)[model];
|
|
3428
3613
|
if (!schedule) {
|
|
3429
3614
|
throw new Error(`Missing model pricing for ${model}`);
|
|
@@ -3440,10 +3625,12 @@ export function calculateModelUsageCostMicros(
|
|
|
3440
3625
|
(rawCostByPricing.get(pricing) ?? 0) + calculateEntryCostMicros(pricing, entry),
|
|
3441
3626
|
);
|
|
3442
3627
|
}
|
|
3443
|
-
let
|
|
3628
|
+
let providerCostMicros = 0;
|
|
3629
|
+
let creditCostMicros = 0;
|
|
3444
3630
|
for (const [pricing, rawCost] of rawCostByPricing) {
|
|
3445
3631
|
const marginBps = pricing.marginBps ?? 0;
|
|
3446
|
-
|
|
3632
|
+
providerCostMicros += rawCost;
|
|
3633
|
+
creditCostMicros += Math.ceil((rawCost * (10_000 + marginBps)) / 10_000);
|
|
3447
3634
|
}
|
|
3448
3635
|
const latencyMode = options?.latencyMode ?? "standard";
|
|
3449
3636
|
if (latencyMode !== "standard") {
|
|
@@ -3456,10 +3643,11 @@ export function calculateModelUsageCostMicros(
|
|
|
3456
3643
|
(mode) => mode.id === latencyMode && mode.runnable,
|
|
3457
3644
|
)?.billingMultiplierBps;
|
|
3458
3645
|
if (multiplierBps && multiplierBps > 0) {
|
|
3459
|
-
|
|
3646
|
+
providerCostMicros = Math.ceil((providerCostMicros * multiplierBps) / 10_000);
|
|
3647
|
+
creditCostMicros = Math.ceil((creditCostMicros * multiplierBps) / 10_000);
|
|
3460
3648
|
}
|
|
3461
3649
|
}
|
|
3462
|
-
return
|
|
3650
|
+
return { providerCostMicros, creditCostMicros };
|
|
3463
3651
|
}
|
|
3464
3652
|
|
|
3465
3653
|
/**
|
|
@@ -3473,6 +3661,16 @@ export function calculateGatewayReportedCostMicros(
|
|
|
3473
3661
|
inferenceCostUsd: string,
|
|
3474
3662
|
options?: { inputTokens?: number },
|
|
3475
3663
|
): number {
|
|
3664
|
+
return calculateGatewayReportedCostBreakdown(settings, model, inferenceCostUsd, options)
|
|
3665
|
+
.creditCostMicros;
|
|
3666
|
+
}
|
|
3667
|
+
|
|
3668
|
+
export function calculateGatewayReportedCostBreakdown(
|
|
3669
|
+
settings: Settings,
|
|
3670
|
+
model: string,
|
|
3671
|
+
inferenceCostUsd: string,
|
|
3672
|
+
options?: { inputTokens?: number },
|
|
3673
|
+
): ModelUsageCostBreakdown {
|
|
3476
3674
|
const schedule = configuredModelPricingSchedules(settings)[model];
|
|
3477
3675
|
if (!schedule) {
|
|
3478
3676
|
throw new Error(`Missing model pricing for ${model}`);
|
|
@@ -3485,14 +3683,52 @@ export function calculateGatewayReportedCostMicros(
|
|
|
3485
3683
|
const fraction = match[2] ?? "";
|
|
3486
3684
|
const decimalDigits = BigInt(`${match[1]}${fraction}`);
|
|
3487
3685
|
const decimalScale = 10n ** BigInt(fraction.length);
|
|
3686
|
+
const providerNumerator = decimalDigits * 1_000_000n;
|
|
3687
|
+
const providerMicros = (providerNumerator + decimalScale - 1n) / decimalScale;
|
|
3488
3688
|
const marginBps = BigInt(10_000 + (pricing.marginBps ?? 0));
|
|
3489
|
-
const numerator =
|
|
3689
|
+
const numerator = providerNumerator * marginBps;
|
|
3490
3690
|
const denominator = decimalScale * 10_000n;
|
|
3491
|
-
const
|
|
3492
|
-
if (
|
|
3691
|
+
const creditMicros = (numerator + denominator - 1n) / denominator;
|
|
3692
|
+
if (
|
|
3693
|
+
providerMicros > BigInt(Number.MAX_SAFE_INTEGER) ||
|
|
3694
|
+
creditMicros > BigInt(Number.MAX_SAFE_INTEGER)
|
|
3695
|
+
) {
|
|
3493
3696
|
throw new Error("AI Gateway inference cost exceeds the supported billing range");
|
|
3494
3697
|
}
|
|
3495
|
-
return
|
|
3698
|
+
return {
|
|
3699
|
+
providerCostMicros: Number(providerMicros),
|
|
3700
|
+
creditCostMicros: Number(creditMicros),
|
|
3701
|
+
};
|
|
3702
|
+
}
|
|
3703
|
+
|
|
3704
|
+
/**
|
|
3705
|
+
* Exact OpenGeni product price frozen before a managed video request starts.
|
|
3706
|
+
* Gateway reporting is delayed for asynchronous video, so this deliberately
|
|
3707
|
+
* does not masquerade as provider-reported cost.
|
|
3708
|
+
*/
|
|
3709
|
+
export function calculateVideoGenerationCreditCostMicros(
|
|
3710
|
+
settings: Settings,
|
|
3711
|
+
input: {
|
|
3712
|
+
modelId: string;
|
|
3713
|
+
resolution: VideoGenerationResolution;
|
|
3714
|
+
durationSeconds: number;
|
|
3715
|
+
},
|
|
3716
|
+
): number {
|
|
3717
|
+
if (input.modelId !== SEEDANCE_2_5_MODEL_ID) {
|
|
3718
|
+
throw new Error(`Missing video generation credit pricing for ${input.modelId}`);
|
|
3719
|
+
}
|
|
3720
|
+
if (!Number.isSafeInteger(input.durationSeconds) || input.durationSeconds < 1) {
|
|
3721
|
+
throw new Error("Video generation duration is invalid for credit pricing");
|
|
3722
|
+
}
|
|
3723
|
+
const rate =
|
|
3724
|
+
input.resolution === "480p"
|
|
3725
|
+
? settings.videoGenerationCredit480pMicrosPerSecond
|
|
3726
|
+
: settings.videoGenerationCredit720pMicrosPerSecond;
|
|
3727
|
+
const cost = rate * input.durationSeconds;
|
|
3728
|
+
if (!Number.isSafeInteger(cost) || cost <= 0 || cost > 1_000_000_000) {
|
|
3729
|
+
throw new Error("Video generation credit price exceeds the supported range");
|
|
3730
|
+
}
|
|
3731
|
+
return cost;
|
|
3496
3732
|
}
|
|
3497
3733
|
|
|
3498
3734
|
export function configuredAllowedReasoningEfforts(
|
|
@@ -3706,16 +3942,13 @@ export function stableSandboxEnvironmentForRun(
|
|
|
3706
3942
|
environment.OPENGENI_GIT_CLI_WRAPPER_DIR ??= `${home}/.opengeni/bin`;
|
|
3707
3943
|
environment.PATH = prependPathEntry(environment.PATH, environment.OPENGENI_GIT_CLI_WRAPPER_DIR);
|
|
3708
3944
|
}
|
|
3709
|
-
if (settings.
|
|
3710
|
-
environment.
|
|
3945
|
+
if (settings.sandboxBackend !== "selfhosted" && resolveFirstPartyDelegationSecret(settings)) {
|
|
3946
|
+
environment.OPENGENI_CODEMODE_TOKEN_FILE ??= `${environment.HOME ?? descriptor.workspaceRoot}/.opengeni/codemode-token`;
|
|
3711
3947
|
if (settings.ogtoolPackageSpec) {
|
|
3712
3948
|
environment.OPENGENI_OGTOOL_PACKAGE_SPEC ??= settings.ogtoolPackageSpec;
|
|
3713
3949
|
}
|
|
3714
3950
|
if (options.workspaceId) {
|
|
3715
|
-
environment.
|
|
3716
|
-
settings,
|
|
3717
|
-
options.workspaceId,
|
|
3718
|
-
);
|
|
3951
|
+
environment.OPENGENI_CODEMODE_URL ??= codemodeWorkspaceUrl(settings, options.workspaceId);
|
|
3719
3952
|
}
|
|
3720
3953
|
}
|
|
3721
3954
|
return environment;
|
|
@@ -4220,6 +4453,7 @@ function ensureBuiltInMcpServers(settings: Settings): Settings["mcpServers"] {
|
|
|
4220
4453
|
"search_documents",
|
|
4221
4454
|
"fetch_document_chunk",
|
|
4222
4455
|
"list_document_bases",
|
|
4456
|
+
"list_indexed_documents",
|
|
4223
4457
|
"knowledge_search",
|
|
4224
4458
|
"knowledge_fetch",
|
|
4225
4459
|
"memory_search",
|
|
@@ -4272,6 +4506,15 @@ export function firstPartyMcpWorkspaceUrl(settings: Settings, workspaceId: strin
|
|
|
4272
4506
|
return url.toString();
|
|
4273
4507
|
}
|
|
4274
4508
|
|
|
4509
|
+
export function codemodeWorkspaceUrl(settings: Settings, workspaceId: string): string {
|
|
4510
|
+
const url = new URL(firstPartyMcpWorkspaceUrl(settings, workspaceId));
|
|
4511
|
+
if (!url.pathname.endsWith("/mcp")) {
|
|
4512
|
+
throw new Error("First-party MCP URL cannot be projected to the Codemode endpoint");
|
|
4513
|
+
}
|
|
4514
|
+
url.pathname = `${url.pathname.slice(0, -4)}/codemode`;
|
|
4515
|
+
return url.toString();
|
|
4516
|
+
}
|
|
4517
|
+
|
|
4275
4518
|
function firstPartyMcpServerUrl(settings: Settings): string {
|
|
4276
4519
|
return firstPartyMcpBaseUrl(settings);
|
|
4277
4520
|
}
|
|
@@ -4286,9 +4529,6 @@ function firstPartyFilesMcpServerUrl(mcpUrl: string): string {
|
|
|
4286
4529
|
|
|
4287
4530
|
function validateSettings(settings: Settings): void {
|
|
4288
4531
|
temporalConnectionOptions(settings);
|
|
4289
|
-
if (settings.toolspaceEnabled && !settings.delegationSecret) {
|
|
4290
|
-
throw new Error("OPENGENI_DELEGATION_SECRET is required when OPENGENI_TOOLSPACE_ENABLED=true");
|
|
4291
|
-
}
|
|
4292
4532
|
if (settings.productAccessMode === "managed") {
|
|
4293
4533
|
if (!settings.publicBaseUrl) {
|
|
4294
4534
|
throw new Error(
|
|
@@ -4342,11 +4582,6 @@ function validateSettings(settings: Settings): void {
|
|
|
4342
4582
|
);
|
|
4343
4583
|
}
|
|
4344
4584
|
if (settings.slackClientId) {
|
|
4345
|
-
if (!settings.slackSigningSecret) {
|
|
4346
|
-
throw new Error(
|
|
4347
|
-
"OPENGENI_SLACK_SIGNING_SECRET is required when the OpenGeni Slack app is configured",
|
|
4348
|
-
);
|
|
4349
|
-
}
|
|
4350
4585
|
if (!settings.publicBaseUrl) {
|
|
4351
4586
|
throw new Error(
|
|
4352
4587
|
"OPENGENI_PUBLIC_BASE_URL is required when the OpenGeni Slack app is configured",
|
|
@@ -4391,6 +4626,31 @@ function validateSettings(settings: Settings): void {
|
|
|
4391
4626
|
);
|
|
4392
4627
|
}
|
|
4393
4628
|
}
|
|
4629
|
+
if (Boolean(settings.atlassianClientId) !== Boolean(settings.atlassianClientSecret)) {
|
|
4630
|
+
throw new Error(
|
|
4631
|
+
"OPENGENI_ATLASSIAN_CLIENT_ID and OPENGENI_ATLASSIAN_CLIENT_SECRET must be configured together",
|
|
4632
|
+
);
|
|
4633
|
+
}
|
|
4634
|
+
if (settings.atlassianClientId) {
|
|
4635
|
+
if (!settings.publicBaseUrl) {
|
|
4636
|
+
throw new Error(
|
|
4637
|
+
"OPENGENI_PUBLIC_BASE_URL is required when the Atlassian integration is configured",
|
|
4638
|
+
);
|
|
4639
|
+
}
|
|
4640
|
+
if (
|
|
4641
|
+
!settings.publicBaseUrl.startsWith("https://") &&
|
|
4642
|
+
!["local", "test"].includes(settings.environment)
|
|
4643
|
+
) {
|
|
4644
|
+
throw new Error(
|
|
4645
|
+
"OPENGENI_PUBLIC_BASE_URL must use https when the Atlassian integration is configured outside local/test",
|
|
4646
|
+
);
|
|
4647
|
+
}
|
|
4648
|
+
if (!settings.integrationsStateSecret) {
|
|
4649
|
+
throw new Error(
|
|
4650
|
+
"OPENGENI_INTEGRATIONS_STATE_SECRET is required when the Atlassian integration is configured",
|
|
4651
|
+
);
|
|
4652
|
+
}
|
|
4653
|
+
}
|
|
4394
4654
|
parseIntegrationsOauthClientsJson(settings.integrationsOauthClientsJson);
|
|
4395
4655
|
parseSocialOauthClientsJson(settings.socialOauthClientsJson);
|
|
4396
4656
|
if (
|
|
@@ -4614,6 +4874,7 @@ function validateSettings(settings: Settings): void {
|
|
|
4614
4874
|
{
|
|
4615
4875
|
const reaperPeriod = settings.sandboxLeaseReaperPeriodMs;
|
|
4616
4876
|
const viewerTtl = settings.sandboxViewerHolderTtlMs;
|
|
4877
|
+
const interactionTtl = settings.sandboxInteractionHolderTtlMs;
|
|
4617
4878
|
const idleGraceMs = settings.sandboxIdleGraceMs;
|
|
4618
4879
|
const providerLifetimeMs = settings.modalTimeoutSeconds * 1000;
|
|
4619
4880
|
const rotationLeadMs = settings.sandboxRotationLeadMs;
|
|
@@ -4632,6 +4893,13 @@ function validateSettings(settings: Settings): void {
|
|
|
4632
4893
|
`than the TTL it polices, or stale viewer holders outlive a full reaper period.`,
|
|
4633
4894
|
);
|
|
4634
4895
|
}
|
|
4896
|
+
if (!(reaperPeriod < interactionTtl)) {
|
|
4897
|
+
throw new Error(
|
|
4898
|
+
`OPENGENI_SANDBOX_LEASE_REAPER_PERIOD_MS (${reaperPeriod}) must be strictly less than ` +
|
|
4899
|
+
`OPENGENI_SANDBOX_INTERACTION_HOLDER_TTL_MS (${interactionTtl}): the reaper must run ` +
|
|
4900
|
+
`more often than the controller-heartbeat horizon.`,
|
|
4901
|
+
);
|
|
4902
|
+
}
|
|
4635
4903
|
if (!(idleTimeoutMs <= providerLifetimeMs)) {
|
|
4636
4904
|
throw new Error(
|
|
4637
4905
|
`OPENGENI_MODAL_IDLE_TIMEOUT_SECONDS*1000 (${idleTimeoutMs}) must not exceed the hard provider ` +
|
|
@@ -4645,10 +4913,15 @@ function validateSettings(settings: Settings): void {
|
|
|
4645
4913
|
`OPENGENI_MODAL_TIMEOUT_SECONDS*1000 (${providerLifetimeMs}).`,
|
|
4646
4914
|
);
|
|
4647
4915
|
}
|
|
4648
|
-
|
|
4916
|
+
// This is provider-hard-deadline headroom, not a retry delay. Rotation is
|
|
4917
|
+
// admitted immediately before the same sweep's drain inventory, so only the
|
|
4918
|
+
// worst-case time until that sweep plus the complete durable capture window
|
|
4919
|
+
// is required. No second schedule period belongs in the availability path.
|
|
4920
|
+
const captureTimeoutMs = sandboxArchiveCaptureTimeoutMs(settings);
|
|
4921
|
+
if (!(rotationLeadMs > captureTimeoutMs + reaperPeriod)) {
|
|
4649
4922
|
throw new Error(
|
|
4650
|
-
`OPENGENI_SANDBOX_ROTATION_LEAD_MS (${rotationLeadMs}) must exceed the
|
|
4651
|
-
`plus
|
|
4923
|
+
`OPENGENI_SANDBOX_ROTATION_LEAD_MS (${rotationLeadMs}) must exceed the durable capture ` +
|
|
4924
|
+
`timeout plus one reaper period (${captureTimeoutMs + reaperPeriod}).`,
|
|
4652
4925
|
);
|
|
4653
4926
|
}
|
|
4654
4927
|
if (!(viewerTtl < idleTimeoutMs)) {
|
|
@@ -4658,6 +4931,13 @@ function validateSettings(settings: Settings): void {
|
|
|
4658
4931
|
`under it (the provider idle-timeout is the backstop).`,
|
|
4659
4932
|
);
|
|
4660
4933
|
}
|
|
4934
|
+
if (!(interactionTtl < idleTimeoutMs)) {
|
|
4935
|
+
throw new Error(
|
|
4936
|
+
`OPENGENI_SANDBOX_INTERACTION_HOLDER_TTL_MS (${interactionTtl}) must be strictly less than ` +
|
|
4937
|
+
`the effective box idle timeout (${idleTimeoutMs}): a dead browser controller must be ` +
|
|
4938
|
+
`reapable before the provider reclaims its placement.`,
|
|
4939
|
+
);
|
|
4940
|
+
}
|
|
4661
4941
|
if (!(reaperPeriod + idleGraceMs < idleTimeoutMs)) {
|
|
4662
4942
|
throw new Error(
|
|
4663
4943
|
`OPENGENI_SANDBOX_LEASE_REAPER_PERIOD_MS + OPENGENI_SANDBOX_IDLE_GRACE_MS ` +
|