@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/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.12"),
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
- toolspaceEnabled: EnvBoolean.default(false),
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. Defaults to
575
- // `snapshot_filesystem` so EVERY box is created persistence-capable: the reaper
576
- // snapshots the live box before it terminates a drained group, and a later
577
- // cold-restore hydrates a fresh box from that snapshot (sandbox-file-persistence).
578
- // `snapshot_filesystem` requires the manifest declare NO ephemeralPersistencePaths
579
- // (buildManifest never sets entry.ephemeral, so it never downgrades to tar). Set
580
- // OPENGENI_MODAL_WORKSPACE_PERSISTENCE=tar to opt back out (no native snapshot;
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("snapshot_filesystem"),
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.number().int().positive().default(60_000),
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 each global reaper pass so a rollout that discovers many legacy boxes
828
- // with unknown creation clocks cannot create a provider/API thundering herd.
829
- // One is the safe admission default: the reaper services provider transitions
830
- // sequentially, so claiming a wider batch would fence boxes before the same
831
- // sweep can service them. Larger fleets may raise this only as an explicit,
832
- // observed deployment choice.
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
- toolspaceEnabled: optional("OPENGENI_TOOLSPACE_ENABLED"),
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
- 60 * 60_000,
2076
- Math.max(settings.sandboxSnapshotTimeoutMs + 30_000, settings.sandboxSnapshotTimeoutMs * 2),
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: { reasoningEffort: boolean; hostedWebSearch: boolean; vision?: boolean },
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 total = 0;
3628
+ let providerCostMicros = 0;
3629
+ let creditCostMicros = 0;
3444
3630
  for (const [pricing, rawCost] of rawCostByPricing) {
3445
3631
  const marginBps = pricing.marginBps ?? 0;
3446
- total += Math.ceil((rawCost * (10_000 + marginBps)) / 10_000);
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
- total = Math.ceil((total * multiplierBps) / 10_000);
3646
+ providerCostMicros = Math.ceil((providerCostMicros * multiplierBps) / 10_000);
3647
+ creditCostMicros = Math.ceil((creditCostMicros * multiplierBps) / 10_000);
3460
3648
  }
3461
3649
  }
3462
- return total;
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 = decimalDigits * 1_000_000n * marginBps;
3689
+ const numerator = providerNumerator * marginBps;
3490
3690
  const denominator = decimalScale * 10_000n;
3491
- const micros = (numerator + denominator - 1n) / denominator;
3492
- if (micros > BigInt(Number.MAX_SAFE_INTEGER)) {
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 Number(micros);
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.toolspaceEnabled && settings.sandboxBackend !== "selfhosted") {
3710
- environment.OPENGENI_TOOLSPACE_TOKEN_FILE ??= `${environment.HOME ?? descriptor.workspaceRoot}/.opengeni/toolspace-token`;
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.OPENGENI_TOOLSPACE_URL ??= firstPartyMcpWorkspaceUrl(
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
- if (!(rotationLeadMs > settings.sandboxSnapshotTimeoutMs + 2 * reaperPeriod)) {
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 snapshot timeout ` +
4651
- `plus two reaper periods (${settings.sandboxSnapshotTimeoutMs + 2 * reaperPeriod}).`,
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 ` +