@bitkyc08/opencodex 2.18.2 → 2.20.0

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.
Files changed (131) hide show
  1. package/gui/dist/assets/index-DF_UFrGS.css +1 -0
  2. package/gui/dist/assets/index-DSK3S5HY.js +76 -0
  3. package/gui/dist/index.html +2 -2
  4. package/package.json +1 -1
  5. package/src/adapters/client-fingerprint.ts +14 -10
  6. package/src/adapters/google-antigravity-wire.ts +4 -3
  7. package/src/adapters/google.ts +1 -1
  8. package/src/adapters/mimo-free.ts +17 -0
  9. package/src/adapters/openai-chat.ts +367 -32
  10. package/src/adapters/registry.ts +144 -0
  11. package/src/chat/inbound.ts +37 -8
  12. package/src/cli/claude.ts +2 -1
  13. package/src/cli/combo.ts +3 -0
  14. package/src/cli/dispatch.ts +8 -0
  15. package/src/cli/export-command.ts +2 -2
  16. package/src/cli/help.ts +2 -0
  17. package/src/cli/index.ts +3 -2
  18. package/src/cli/lab.ts +135 -1
  19. package/src/cli/minimax.ts +491 -0
  20. package/src/cli/models-runtime.ts +22 -1
  21. package/src/cli/models.ts +67 -2
  22. package/src/cli/opencode.ts +2 -1
  23. package/src/cli/registry.ts +22 -2
  24. package/src/clients/config-export.ts +125 -7
  25. package/src/codex/app-server-processes.ts +57 -2
  26. package/src/codex/app-server-restart-service.ts +232 -0
  27. package/src/codex/catalog/aggregation.ts +10 -1
  28. package/src/codex/catalog/effort.ts +15 -3
  29. package/src/codex/catalog/parsing.ts +3 -1
  30. package/src/codex/catalog/provider-fetch.ts +45 -5
  31. package/src/codex/catalog/sync.ts +74 -4
  32. package/src/codex/convergence.ts +2 -0
  33. package/src/codex/shim.ts +100 -5
  34. package/src/combos/index.ts +1 -0
  35. package/src/combos/request.ts +30 -0
  36. package/src/combos/types.ts +6 -0
  37. package/src/config.ts +52 -0
  38. package/src/generated/compatibility-version.json +239 -87
  39. package/src/images/loop.ts +11 -1
  40. package/src/integrations/registry.ts +7 -0
  41. package/src/lab/conformance/jcs.ts +42 -2
  42. package/src/lab/conformance/negative-controls.ts +6 -2
  43. package/src/lab/conformance/runner.ts +16 -5
  44. package/src/lab/fabric/observe.ts +49 -14
  45. package/src/lab/index.ts +16 -0
  46. package/src/lab/ledger/purge.ts +152 -83
  47. package/src/lab/ledger/store.ts +168 -54
  48. package/src/lab/observe/from-conformance.ts +8 -6
  49. package/src/lab/observe/from-live.ts +8 -2
  50. package/src/lab/paths.ts +23 -0
  51. package/src/lab/public/bundle.ts +217 -0
  52. package/src/lab/public/community-authority.ts +175 -0
  53. package/src/lab/public/community-files.ts +29 -0
  54. package/src/lab/public/community.ts +479 -0
  55. package/src/lab/public/file-safety.ts +155 -0
  56. package/src/lab/public/ids.ts +26 -0
  57. package/src/lab/public/index.ts +16 -0
  58. package/src/lab/public/mutation-lock.ts +424 -0
  59. package/src/lab/public/operator.ts +353 -0
  60. package/src/lab/public/origin-purge.ts +79 -0
  61. package/src/lab/public/origin.ts +203 -0
  62. package/src/lab/public/privacy.ts +143 -0
  63. package/src/lab/public/private-file.ts +261 -0
  64. package/src/lab/public/project.ts +124 -0
  65. package/src/lab/public/purge-test-fault.ts +21 -0
  66. package/src/lab/public/purge.ts +223 -0
  67. package/src/lab/public/registry.ts +44 -0
  68. package/src/lab/public/revocation.ts +252 -0
  69. package/src/lab/public/signature.ts +219 -0
  70. package/src/lab/public/storage.ts +105 -0
  71. package/src/lab/public/strict-json.ts +206 -0
  72. package/src/lab/public/time.ts +26 -0
  73. package/src/lab/public/types.ts +172 -0
  74. package/src/lab/public/validate.ts +391 -0
  75. package/src/lib/codex-restart-contract.ts +120 -0
  76. package/src/lib/errors.ts +27 -0
  77. package/src/lib/lab-activation.ts +109 -47
  78. package/src/lib/lab-live-pinned-sender.ts +16 -5
  79. package/src/lib/pinned-http.ts +70 -16
  80. package/src/lib/self-launch-argv.ts +15 -0
  81. package/src/lib/state-store-registrations.ts +2 -0
  82. package/src/lib/upstream-reachability.ts +4 -0
  83. package/src/lib/windows-elevation.ts +10 -1
  84. package/src/providers/derive.ts +24 -4
  85. package/src/providers/registry.ts +9 -3
  86. package/src/providers/request-pacing.ts +310 -0
  87. package/src/providers/service-tier.ts +143 -0
  88. package/src/providers/static-model-discovery.ts +86 -0
  89. package/src/reasoning-effort.ts +27 -1
  90. package/src/responses/spill-store.ts +20 -1
  91. package/src/responses/state.ts +159 -3
  92. package/src/router.ts +23 -6
  93. package/src/routing/capability.ts +4 -2
  94. package/src/routing/compatibility/behavior.ts +5 -1
  95. package/src/server/adapter-resolve.ts +2 -32
  96. package/src/server/auth-cors.ts +8 -0
  97. package/src/server/chat-completions.ts +74 -34
  98. package/src/server/chat-native-sse.ts +331 -0
  99. package/src/server/chat-native.ts +371 -0
  100. package/src/server/effort-policy.ts +18 -0
  101. package/src/server/management/combo-routes.ts +16 -2
  102. package/src/server/management/config-routes.ts +6 -4
  103. package/src/server/management/context.ts +17 -0
  104. package/src/server/management/lab-routes.ts +181 -19
  105. package/src/server/management/model-routes.ts +76 -2
  106. package/src/server/management/model-rows.ts +8 -0
  107. package/src/server/management/provider-capability-config.ts +48 -0
  108. package/src/server/management/provider-routes.ts +76 -4
  109. package/src/server/management/system-restart.ts +4 -2
  110. package/src/server/management/system-routes.ts +38 -0
  111. package/src/server/relay.ts +17 -3
  112. package/src/server/responses/compact.ts +4 -1
  113. package/src/server/responses/core.ts +278 -48
  114. package/src/server/responses/empty-completion-guard.ts +276 -0
  115. package/src/server/responses/fetch-helpers.ts +35 -4
  116. package/src/server/responses/input-admission.ts +169 -0
  117. package/src/server/responses/pacing-overload.ts +13 -0
  118. package/src/server/responses/policy-fallback.ts +16 -2
  119. package/src/server/responses/terminal-guard.ts +1 -1
  120. package/src/server/responses/upstream-error.ts +5 -0
  121. package/src/server/responses.ts +17 -2
  122. package/src/service-manager-probe.ts +2 -3
  123. package/src/types.ts +76 -6
  124. package/src/update/index.ts +6 -5
  125. package/src/update/job.ts +5 -6
  126. package/src/update/notify.ts +5 -3
  127. package/src/usage/log.ts +11 -1
  128. package/src/vision/index.ts +21 -4
  129. package/src/web-search/index.ts +2 -1
  130. package/gui/dist/assets/index-CXI1262_.js +0 -76
  131. package/gui/dist/assets/index-DUCH59lJ.css +0 -1
@@ -32,6 +32,11 @@ import { modelInList } from "../../types";
32
32
  import { CODEX_REASONING_LEVELS, codexEffortRank, configuredReasoningEfforts, modelRecordValue, sanitizeCodexReasoningEfforts } from "../../reasoning-effort";
33
33
  import { getModelMetadata, getModelMetadataCaseInsensitive, listModelMetadata, resolveMetadataProvider } from "../../generated/model-metadata";
34
34
  import { enrichProviderFromRegistry, shouldCaseFoldMetadataModelId } from "../../providers/derive";
35
+ import {
36
+ captureServiceTierAdapterAuthority,
37
+ serviceTierSupportForModel,
38
+ type CapturedServiceTierAdapterAuthority,
39
+ } from "../../providers/service-tier";
35
40
  import { effectiveGoogleMode, getProviderRegistryEntry, providerMatchesRegistryTransport } from "../../providers/registry";
36
41
  import { parseAntigravityAvailableModels } from "../../providers/antigravity-models";
37
42
  import { applyProviderContextCap, providerContextCap } from "../../providers/context-cap";
@@ -150,6 +155,7 @@ interface CapturedProviderGather {
150
155
  readonly discovery: ResolvedProviderModelDiscovery;
151
156
  readonly policy: CatalogProviderDiscoveryPolicySnapshot;
152
157
  readonly request: CapturedModelsRequest;
158
+ readonly serviceTierAdapterAuthority: CapturedServiceTierAdapterAuthority;
153
159
  readonly observedAuth?: ModelsAuthResolution;
154
160
  /**
155
161
  * Configured model ids this provider must keep even when live discovery omits
@@ -401,6 +407,12 @@ function captureProviderGather(
401
407
  ): CapturedProviderGather {
402
408
  const enriched = detachedClone(withCanonicalOpenAiForwardAuthDefault(name, configured));
403
409
  enrichProviderFromRegistry(name, enriched);
410
+ const registryTransportMatch = providerMatchesRegistryTransport(name, enriched);
411
+ const serviceTierAdapterAuthority = captureServiceTierAdapterAuthority(
412
+ name,
413
+ enriched,
414
+ registryTransportMatch,
415
+ );
404
416
  const provider = recursivelyFreeze(enriched);
405
417
  const observedAuth = authResolver.kind === "observed"
406
418
  && provider.authMode !== "forward"
@@ -414,7 +426,6 @@ function captureProviderGather(
414
426
  maxResponseBytes: resolved.maxResponseBytes,
415
427
  maxModels: resolved.maxModels,
416
428
  });
417
- const registryTransportMatch = providerMatchesRegistryTransport(name, provider);
418
429
  const trustedOpenAiApi = captureTrustedOpenAiApiPolicy(name, registryTransportMatch);
419
430
  const policy = detachedFrozen({
420
431
  provider: name,
@@ -438,6 +449,7 @@ function captureProviderGather(
438
449
  discovery,
439
450
  policy,
440
451
  request,
452
+ serviceTierAdapterAuthority,
441
453
  ...(observedAuth ? { observedAuth: Object.freeze({ ...observedAuth }) } : {}),
442
454
  ...(retainConfiguredModelIds && retainConfiguredModelIds.size > 0
443
455
  ? { retainConfiguredModelIds }
@@ -506,6 +518,7 @@ function captureGatherFlight(
506
518
  // It is the one member of a provider row that is legitimately a function,
507
519
  // so it is dropped here rather than allowed to break every encode.
508
520
  provider: omitProviderTransportExecutor(provider.provider),
521
+ serviceTierAdapterAuthority: provider.serviceTierAdapterAuthority,
509
522
  // Combo retention is capture-time state, not a provider-row field. Two
510
523
  // gathers that share providers but differ in combo targets must not join.
511
524
  retainConfiguredModelIds: [...(provider.retainConfiguredModelIds ?? [])].sort(),
@@ -561,6 +574,7 @@ function providerCatalogFingerprint(name: string, prov: OcxProviderConfig): Reco
561
574
  defRe: prov.modelDefaultReasoningEfforts ?? null,
562
575
  rsSum: prov.modelSupportsReasoningSummaries ?? null,
563
576
  rsDel: prov.modelReasoningSummaryDelivery ?? null,
577
+ serviceTier: prov.modelSupportsServiceTier ?? null,
564
578
  noVis: [...(prov.noVisionModels ?? [])].sort(),
565
579
  ptc: prov.parallelToolCalls ?? null,
566
580
  gMode: prov.googleMode ?? null,
@@ -632,8 +646,10 @@ export function applyProviderConfigHints(name: string, prov: OcxProviderConfig,
632
646
  const reasoningEfforts = configuredReasoningEfforts(prov, model.id);
633
647
  const defaultReasoningEffort = modelRecordValue(prov.modelDefaultReasoningEfforts, model.id) ?? model.defaultReasoningEffort;
634
648
  const supportsReasoningSummaries = configuredReasoningSummarySupport(prov, model.id);
649
+ const supportsServiceTier = serviceTierSupportForModel(prov, model.id, name);
650
+ const { supportsServiceTier: _staleServiceTier, ...modelWithoutServiceTier } = model;
635
651
  const hinted = {
636
- ...model,
652
+ ...modelWithoutServiceTier,
637
653
  ...(configuredCap !== undefined
638
654
  ? {
639
655
  contextWindow: typeof model.contextWindow === "number" && model.contextWindow > 0
@@ -652,6 +668,7 @@ export function applyProviderConfigHints(name: string, prov: OcxProviderConfig,
652
668
  : {}),
653
669
  ...(defaultReasoningEffort ? { defaultReasoningEffort } : {}),
654
670
  ...(typeof supportsReasoningSummaries === "boolean" ? { supportsReasoningSummaries } : {}),
671
+ ...(typeof supportsServiceTier === "boolean" ? { supportsServiceTier } : {}),
655
672
  ...(prov.adapter === "kiro" ? { supportsVerbosity: false } : {}),
656
673
  // Default-on for openai-chat providers (explicit false opts out); other adapters
657
674
  // advertise only on explicit opt-in.
@@ -1740,6 +1757,7 @@ async function gatherRoutedModelsUncached(
1740
1757
  const replacedByRoutedSlug = new Map(all.map(model => [routedSlug(model.provider, model.id), model]));
1741
1758
  const customModels = (config.customModels ?? []).map(cm => {
1742
1759
  const rawProvider = config.providers[cm.provider];
1760
+ const effectiveProvider = enrichedByName.get(cm.provider) ?? rawProvider;
1743
1761
  // Registry routing backfills an omitted authMode on the built-in OpenAI provider to
1744
1762
  // forward. Keep the catalog projection on the same contract while still failing closed
1745
1763
  // for every explicit non-forward mode and every non-canonical endpoint.
@@ -1762,6 +1780,9 @@ async function gatherRoutedModelsUncached(
1762
1780
  ? nativeDefaultReasoningEffort(cm.modelId)
1763
1781
  : undefined;
1764
1782
  const supportsReasoningSummaries = configuredReasoningSummarySupport(rawProvider, cm.modelId);
1783
+ const supportsServiceTier = effectiveProvider
1784
+ ? serviceTierSupportForModel(effectiveProvider, cm.modelId, cm.provider)
1785
+ : undefined;
1765
1786
  const base: CatalogModel = {
1766
1787
  id: cm.modelId,
1767
1788
  provider: cm.provider,
@@ -1775,14 +1796,27 @@ async function gatherRoutedModelsUncached(
1775
1796
  ? { inputModalities: cm.inputModalities }
1776
1797
  : codexForwardNativeCapabilityAlias ? { inputModalities: nativeInputModalities(cm.modelId) } : {}),
1777
1798
  ...(typeof supportsReasoningSummaries === "boolean" ? { supportsReasoningSummaries } : {}),
1799
+ // Native-alias defaults apply only where the custom row declares nothing: the explicit
1800
+ // spreads below must win (later in object order), so a stored `[]` stays empty and a
1801
+ // declared ladder is never replaced by the alias's native ladder.
1778
1802
  ...(codexForwardNativeCapabilityAlias
1779
1803
  ? {
1780
1804
  codexForwardNativeCapabilityAlias: true,
1781
- reasoningEfforts: nativeReasoningEfforts(cm.modelId),
1782
1805
  parallelToolCalls: nativeParallelToolCalls(cm.modelId),
1783
- ...(nativeAliasDefaultEffort ? { defaultReasoningEffort: nativeAliasDefaultEffort } : {}),
1806
+ ...(Array.isArray(cm.reasoningEfforts)
1807
+ ? {}
1808
+ : {
1809
+ reasoningEfforts: nativeReasoningEfforts(cm.modelId),
1810
+ ...(nativeAliasDefaultEffort ? { defaultReasoningEffort: nativeAliasDefaultEffort } : {}),
1811
+ }),
1784
1812
  }
1785
1813
  : {}),
1814
+ // Explicit custom-row ladder wins over the inherited provider row below: the merge only
1815
+ // gap-fills, so a stored `[]` (explicit "no reasoning") or a declared ladder is kept
1816
+ // verbatim instead of being replaced by the replaced row's metadata.
1817
+ ...(Array.isArray(cm.reasoningEfforts) ? { reasoningEfforts: [...cm.reasoningEfforts] } : {}),
1818
+ ...(cm.defaultReasoningEffort ? { defaultReasoningEffort: cm.defaultReasoningEffort } : {}),
1819
+ ...(typeof supportsServiceTier === "boolean" ? { supportsServiceTier } : {}),
1786
1820
  };
1787
1821
  // #962: the dedupe below drops the provider-derived row this custom row replaces. Inherit that
1788
1822
  // row's provider capability metadata (reasoning ladder, default effort, parallel tool calls,
@@ -1791,13 +1825,19 @@ async function gatherRoutedModelsUncached(
1791
1825
  // noReasoningModels model loses its empty ladder and the catalog synthesizes the generic one,
1792
1826
  // which Codex then rejects for spawn_agent with effort "none".
1793
1827
  const replaced = replacedByRoutedSlug.get(routedSlug(cm.provider, cm.modelId));
1828
+ // The final ladder is what the catalog will advertise; the inherited default only rides
1829
+ // along when it is actually a member — otherwise a provider default like "xhigh" would
1830
+ // re-apply onto a narrower custom ladder and override the fallback in applyReasoningLevels.
1831
+ const effectiveLadder = base.reasoningEfforts ?? replaced?.reasoningEfforts;
1794
1832
  const merged: CatalogModel = replaced ? {
1795
1833
  ...base,
1796
1834
  ...(base.contextWindow === undefined && replaced.contextWindow !== undefined ? { contextWindow: replaced.contextWindow } : {}),
1797
1835
  ...(base.maxInputTokens === undefined && replaced.maxInputTokens !== undefined ? { maxInputTokens: replaced.maxInputTokens } : {}),
1798
1836
  ...(base.inputModalities === undefined && replaced.inputModalities !== undefined ? { inputModalities: replaced.inputModalities } : {}),
1799
1837
  ...(base.reasoningEfforts === undefined && replaced.reasoningEfforts !== undefined ? { reasoningEfforts: replaced.reasoningEfforts } : {}),
1800
- ...(base.defaultReasoningEffort === undefined && replaced.defaultReasoningEffort !== undefined ? { defaultReasoningEffort: replaced.defaultReasoningEffort } : {}),
1838
+ ...(base.defaultReasoningEffort === undefined && replaced.defaultReasoningEffort !== undefined
1839
+ && Array.isArray(effectiveLadder) && effectiveLadder.includes(replaced.defaultReasoningEffort)
1840
+ ? { defaultReasoningEffort: replaced.defaultReasoningEffort } : {}),
1801
1841
  ...(base.parallelToolCalls === undefined && replaced.parallelToolCalls !== undefined ? { parallelToolCalls: replaced.parallelToolCalls } : {}),
1802
1842
  ...(base.supportsVerbosity === undefined && replaced.supportsVerbosity !== undefined ? { supportsVerbosity: replaced.supportsVerbosity } : {}),
1803
1843
  ...(base.supportsReasoningSummaries === undefined && replaced.supportsReasoningSummaries !== undefined ? { supportsReasoningSummaries: replaced.supportsReasoningSummaries } : {}),
@@ -67,6 +67,18 @@ import { accountBoundNativeDisplayName, CODEX_ACCOUNT_BOUND_CATALOG_KIND, truste
67
67
 
68
68
  export const MAX_SPAWN_AGENT_MODEL_OVERRIDES = 5;
69
69
 
70
+ // Base for config.modelPickerOrder display priorities (#1649). modelPickerOrder is a DISPLAY-ONLY
71
+ // reordering of the Codex model picker: it rewrites a row's Codex-visible `priority` but never the
72
+ // spawn_agent candidate window. The window is derived from SPAWN_PRIORITY_FIELD (the natural
73
+ // priority captured before the override), so display order and spawn candidates are decoupled.
74
+ export const PICKER_ORDER_PRIORITY_BASE = 1_000;
75
+
76
+ // OpenCodex-private catalog field: the spawn_agent candidate priority a row would have WITHOUT
77
+ // modelPickerOrder. Codex ignores unknown catalog fields (same as opencodex_catalog_kind), so this
78
+ // is invisible to Codex; effectiveSubagentRoster reads it so a display reorder cannot change which
79
+ // rows are spawn_agent candidates. Absent on rows modelPickerOrder did not move.
80
+ export const SPAWN_PRIORITY_FIELD = "opencodex_spawn_priority";
81
+
70
82
  export type SpawnAgentSurface = "v1" | "v2";
71
83
 
72
84
  export type SubagentRosterExclusionReason =
@@ -144,10 +156,17 @@ export function effectiveSubagentRoster(
144
156
  .filter(({ entry }) => entry.visibility === "list")
145
157
  .filter(({ entry }) => surface !== "v2" || isEligibleV2SubagentEntry(entry))
146
158
  .sort((left, right) => {
147
- const leftPriority = typeof left.entry.priority === "number" && Number.isFinite(left.entry.priority)
148
- ? left.entry.priority : Number.MAX_SAFE_INTEGER;
149
- const rightPriority = typeof right.entry.priority === "number" && Number.isFinite(right.entry.priority)
150
- ? right.entry.priority : Number.MAX_SAFE_INTEGER;
159
+ // Spawn candidates rank by the natural priority (SPAWN_PRIORITY_FIELD when present), so a
160
+ // modelPickerOrder display reorder (#1649) can never change candidate membership. Rows the
161
+ // override did not move fall back to their Codex-visible `priority`.
162
+ const spawnPriorityOf = (entry: RawEntry): number => {
163
+ const spawn = entry[SPAWN_PRIORITY_FIELD];
164
+ if (typeof spawn === "number" && Number.isFinite(spawn)) return spawn;
165
+ return typeof entry.priority === "number" && Number.isFinite(entry.priority)
166
+ ? entry.priority : Number.MAX_SAFE_INTEGER;
167
+ };
168
+ const leftPriority = spawnPriorityOf(left.entry);
169
+ const rightPriority = spawnPriorityOf(right.entry);
151
170
  return leftPriority - rightPriority || left.index - right.index;
152
171
  })
153
172
  .slice(0, MAX_SPAWN_AGENT_MODEL_OVERRIDES);
@@ -362,6 +381,8 @@ export interface ObservedCatalogEntryBuildInput {
362
381
  readonly gptSlugs: readonly string[];
363
382
  readonly goModels: readonly CatalogModel[];
364
383
  readonly featured?: readonly string[];
384
+ /** Optional full picker ordering (config.modelPickerOrder); orders non-featured rows. */
385
+ readonly modelPickerOrder?: readonly string[];
365
386
  readonly wsEnabled: boolean;
366
387
  readonly multiAgentMode: MultiAgentMode;
367
388
  readonly exactComboSlugs: ReadonlySet<string>;
@@ -416,6 +437,7 @@ export function buildCatalogEntriesFromObservedState({
416
437
  gptSlugs,
417
438
  goModels,
418
439
  featured,
440
+ modelPickerOrder,
419
441
  wsEnabled,
420
442
  multiAgentMode,
421
443
  exactComboSlugs,
@@ -433,6 +455,39 @@ export function buildCatalogEntriesFromObservedState({
433
455
  // it sorts to the front. This works for native gpt slugs AND routed slugs alike.
434
456
  const rank = new Map((featured ?? []).map((slug, i) => [slug, i] as const));
435
457
  const priorityStride = Math.max(accountSelectors.length, 1);
458
+ // Optional full picker order (#1649). Independent of the 5-slot spawn_agent cap: it only
459
+ // rewrites the Codex-visible display `priority` of listed non-featured routed rows so a >5
460
+ // catalog stays put across rebuilds. Featured rows keep their existing 0..N-1 band; when
461
+ // modelPickerOrder is unset the helper is a no-op and every priority below is byte-identical to
462
+ // before. The spawn_agent candidate window is derived separately from SPAWN_PRIORITY_FIELD, so
463
+ // this display reorder cannot change which rows are spawn candidates.
464
+ const pickerOrder = Array.isArray(modelPickerOrder)
465
+ ? modelPickerOrder.filter((id): id is string => typeof id === "string" && id.length > 0)
466
+ : [];
467
+ const pickerOrderRank = new Map(pickerOrder.map((slug, i) => [slug, i] as const));
468
+ const pickerOrderActive = pickerOrder.length > 0;
469
+ // The display band reuses the existing high priority tier (>= PICKER_ORDER_PRIORITY_BASE, the
470
+ // same 1_000+ neighborhood account rows occupy), keeping listed rows visually after the featured
471
+ // band. Candidate membership does not depend on this — see SPAWN_PRIORITY_FIELD.
472
+ /**
473
+ * Priority for a non-featured routed row that is explicitly LISTED in modelPickerOrder. Listed
474
+ * slugs sort in declared order within the high picker-order display tier
475
+ * (>= PICKER_ORDER_PRIORITY_BASE). This sets the Codex-visible `priority` only; the caller records
476
+ * the row's natural priority in SPAWN_PRIORITY_FIELD so the spawn_agent candidate window is
477
+ * unchanged. Returns undefined when the feature is off or the row is not listed, so those rows
478
+ * keep their original assignment (default 5 / account 1_000+) untouched.
479
+ *
480
+ * Scope: only the generic routed `<provider>/<model>` rows call this (see the goModels loop
481
+ * below). Native passthrough rows and account-qualified native rows keep their own priority
482
+ * logic and are intentionally not reordered here — this matches the documented contract on
483
+ * OcxConfig.modelPickerOrder (route native ordering through subagentModels instead).
484
+ */
485
+ const pickerOrderPriority = (slug: string, altSlug?: string): number | undefined => {
486
+ if (!pickerOrderActive) return undefined;
487
+ const hit = pickerOrderRank.get(slug) ?? (altSlug !== undefined ? pickerOrderRank.get(altSlug) : undefined);
488
+ if (hit === undefined) return undefined;
489
+ return PICKER_ORDER_PRIORITY_BASE + hit * priorityStride;
490
+ };
436
491
  const out: RawEntry[] = [];
437
492
  const nativeEntries: RawEntry[] = [];
438
493
  const collisionSkipped = resolveSlugAliasCollisions([...goModels]);
@@ -537,11 +592,24 @@ export function buildCatalogEntriesFromObservedState({
537
592
  }
538
593
  // Featured picks may be stored raw (legacy) or encoded — honor both.
539
594
  const rankHit = rank.get(slug) ?? rank.get(`${m.provider}/${m.id}`);
595
+ // Natural priority: what the row would get WITHOUT modelPickerOrder. This is the value the
596
+ // spawn_agent candidate window is derived from (see effectiveSubagentRoster), so it must never
597
+ // move when modelPickerOrder reorders the picker.
540
598
  if (rankHit !== undefined) e.priority = rankHit * priorityStride;
541
599
  else if (accountSelectors.length > 0) {
542
600
  // Keep the generated account rows together in Codex's priority-sorted flat picker.
543
601
  e.priority = 1_000 + (typeof e.priority === "number" ? e.priority : 5);
544
602
  }
603
+ // #1649: modelPickerOrder is a DISPLAY-ONLY override. Record the natural priority spawn_agent
604
+ // must keep using, then let modelPickerOrder move only the Codex-visible `priority`. Featured
605
+ // rows are never overridden (their rank is authoritative for both display and spawn).
606
+ if (rankHit === undefined) {
607
+ const pickerPriority = pickerOrderPriority(slug, `${m.provider}/${m.id}`);
608
+ if (pickerPriority !== undefined) {
609
+ e[SPAWN_PRIORITY_FIELD] = typeof e.priority === "number" ? e.priority : 5;
610
+ e.priority = pickerPriority;
611
+ }
612
+ }
545
613
  out.push(e);
546
614
  }
547
615
  // Central capability override (phase 120.4): the advertised flag must match the implemented WS
@@ -1324,6 +1392,7 @@ function writeRetainedCatalogSync({
1324
1392
  const enabledGo = filterCatalogVisibleModels(goModels, config);
1325
1393
  const featured = config.subagentModels ?? [];
1326
1394
  const orderedGoModels = orderForSubagents(enabledGo, featured); // stable tie-break among equal priorities
1395
+ const modelPickerOrder = config.modelPickerOrder ?? [];
1327
1396
  const multiAgentMode: MultiAgentMode = config.multiAgentMode === "v1" || config.multiAgentMode === "v2" ? config.multiAgentMode : "default";
1328
1397
  const exactComboSlugs = exactComboCatalogSlugs(config);
1329
1398
  const suppressedBareNativeSlugs = desktopAllowlistSuppressedNativeSlugs(config);
@@ -1355,6 +1424,7 @@ function writeRetainedCatalogSync({
1355
1424
  gptSlugs: [],
1356
1425
  goModels: orderedGoModels,
1357
1426
  featured,
1427
+ modelPickerOrder,
1358
1428
  wsEnabled,
1359
1429
  multiAgentMode,
1360
1430
  exactComboSlugs,
@@ -225,6 +225,7 @@ function prepareCatalog(
225
225
  const enabled = filterCatalogVisibleModels(routedModels, config);
226
226
  const featured = config.subagentModels ?? [];
227
227
  const ordered = orderForSubagents(enabled, featured);
228
+ const modelPickerOrder = config.modelPickerOrder ?? [];
228
229
  const multiAgentMode = config.multiAgentMode === "v1" || config.multiAgentMode === "v2"
229
230
  ? config.multiAgentMode : "default";
230
231
  const exactComboSlugs = exactComboCatalogSlugs(config);
@@ -255,6 +256,7 @@ function prepareCatalog(
255
256
  gptSlugs: [],
256
257
  goModels: ordered,
257
258
  featured,
259
+ modelPickerOrder,
258
260
  wsEnabled: websocketsEnabled(config),
259
261
  multiAgentMode,
260
262
  exactComboSlugs,
package/src/codex/shim.ts CHANGED
@@ -8,16 +8,19 @@ import {
8
8
  existsSync,
9
9
  fstatSync,
10
10
  lstatSync,
11
+ linkSync,
11
12
  mkdirSync,
12
13
  mkdtempSync,
13
14
  openSync,
14
15
  readFileSync,
15
16
  readdirSync,
17
+ readlinkSync,
16
18
  readSync,
17
19
  renameSync,
18
20
  rmSync,
19
21
  rmdirSync,
20
22
  statSync,
23
+ symlinkSync,
21
24
  type Stats,
22
25
  unlinkSync,
23
26
  writeFileSync,
@@ -407,6 +410,63 @@ function sameStableShimPathProbe(left: StableShimPathProbe, right: StableShimPat
407
410
  return left.prefix === right.prefix && sameFingerprint(left.fingerprint, right.fingerprint);
408
411
  }
409
412
 
413
+ /**
414
+ * Identity of whatever sits at `path`, read from metadata alone.
415
+ *
416
+ * `stableShimPathProbe` answers a different question: it reads content to decide
417
+ * whether a launcher looks like a healthy shim, and it deliberately returns null
418
+ * for a zero-byte file. That makes it the wrong instrument for rollback
419
+ * bookkeeping. A user can legitimately own an empty `codex` launcher, and a fresh
420
+ * install moves it aside before writing our wrapper; if the move is recorded
421
+ * without a fingerprint, rollback cannot prove the backup is still the file it
422
+ * set aside and refuses to restore it — the launcher stays lost (#1625).
423
+ *
424
+ * Content is irrelevant to that proof, so this reads dev/ino/mode/size/times and
425
+ * re-reads them to reject a path that changed under us, following a symlink to
426
+ * fingerprint its target as well.
427
+ */
428
+ function shimPathFingerprint(path: string): ShimPathFingerprint | null {
429
+ const before = statFingerprint(path, false);
430
+ if (!before) return null;
431
+ if (before.kind !== "symlink") {
432
+ const after = statFingerprint(path, false);
433
+ return after && sameFingerprint(before, after) ? before : null;
434
+ }
435
+ const targetBefore = statFingerprint(path, true);
436
+ if (!targetBefore) return null;
437
+ const targetAfter = statFingerprint(path, true);
438
+ const after = statFingerprint(path, false);
439
+ if (!targetAfter || !after
440
+ || !sameFingerprint(targetBefore, targetAfter)
441
+ || !sameFingerprint(before, after)) return null;
442
+ return { ...before, target: targetBefore };
443
+ }
444
+
445
+ /**
446
+ * Move `from` onto `to` without ever replacing an existing entry.
447
+ *
448
+ * `renameSync` silently clobbers the destination on POSIX, which is wrong for a
449
+ * rollback restore: `sourceOccupied` is sampled before the fingerprint check, so
450
+ * a concurrent installer can publish its own launcher at the original path in
451
+ * between, and the restore would delete it. `link` fails EEXIST instead, which
452
+ * is the no-replace primitive we need and needs no native helper.
453
+ *
454
+ * `link` follows a symlink to its target rather than preserving the link, so a
455
+ * symlink launcher is republished with `symlink`, which is also no-replace: it
456
+ * fails EEXIST on an occupied destination. Checking existence and then renaming
457
+ * would reintroduce exactly the race this function exists to close.
458
+ */
459
+ function restoreWithoutReplacing(from: string, to: string): void {
460
+ const source = lstatSync(from);
461
+ if (source.isSymbolicLink()) {
462
+ symlinkSync(readlinkSync(from), to);
463
+ unlinkSync(from);
464
+ return;
465
+ }
466
+ linkSync(from, to);
467
+ unlinkSync(from);
468
+ }
469
+
410
470
  function isHealthyShimProbe(probe: StableShimPathProbe, platform: NodeJS.Platform): boolean {
411
471
  if (probe.prefix.length < 180 || !probe.prefix.includes(SHIM_MARKER) || !probe.prefix.includes("ensure")) return false;
412
472
  const mode = probe.fingerprint.target?.mode ?? probe.fingerprint.mode;
@@ -644,6 +704,7 @@ let codexShimProbeHookForTests: (() => void) | null = null;
644
704
  let codexShimProbeShellForTests: string | null = null;
645
705
  let codexShimGuardedWriteHookForTests: (() => void) | null = null;
646
706
  let codexShimFreshWriteHookForTests: (() => void) | null = null;
707
+ let codexShimRollbackRestoreHookForTests: ((target: ShimFileState) => void) | null = null;
647
708
  let codexShimProbeObservationMs = CODEX_SHIM_INSTALL_PROBE_TIMEOUT_MS;
648
709
 
649
710
  /** Narrow deterministic seam for transaction rollback tests. */
@@ -671,6 +732,20 @@ export function setCodexShimFreshWriteHookForTests(hook: (() => void) | null): v
671
732
  codexShimFreshWriteHookForTests = hook;
672
733
  }
673
734
 
735
+ /**
736
+ * @internal Test-only seam for the rollback restore race.
737
+ *
738
+ * The window this closes opens after `sourceOccupied` is sampled and closes when
739
+ * the backup is republished, so no earlier hook can reach it: publishing from
740
+ * the fresh-write hook makes `sourceOccupied` true and skips the restore
741
+ * entirely.
742
+ */
743
+ export function setCodexShimRollbackRestoreHookForTests(
744
+ hook: ((target: ShimFileState) => void) | null,
745
+ ): void {
746
+ codexShimRollbackRestoreHookForTests = hook;
747
+ }
748
+
674
749
  function readProbeMetadata(path: string, maxBytes: number): string | null {
675
750
  try {
676
751
  if (!existsSync(path)) return "";
@@ -830,9 +905,9 @@ function rollbackFreshShimInstall(journal: readonly FreshShimInstallJournalEntry
830
905
  }
831
906
  try {
832
907
  if (entry.originalMovedToBackup && existsSync(target.backupPath)) {
833
- const movedOriginal = stableShimPathProbe(target.backupPath);
908
+ const movedOriginal = shimPathFingerprint(target.backupPath);
834
909
  if (!movedOriginal || !entry.movedOriginalFingerprint
835
- || !sameFingerprint(movedOriginal.fingerprint, entry.movedOriginalFingerprint)) {
910
+ || !sameFingerprint(movedOriginal, entry.movedOriginalFingerprint)) {
836
911
  throw new Error("Codex shim fresh-install backup changed during rollback");
837
912
  }
838
913
  if (sourceOccupied) {
@@ -842,7 +917,12 @@ function rollbackFreshShimInstall(journal: readonly FreshShimInstallJournalEntry
842
917
  // lose the command entirely. Keep it in that case — a stray
843
918
  // `codex.opencodex-real` is recoverable, a deleted launcher is not.
844
919
  if (ownsWrapperNow) unlinkSync(target.backupPath);
845
- } else renameSync(target.backupPath, target.originalPath);
920
+ } else {
921
+ // No-replace: sourceOccupied was sampled earlier, so a concurrent
922
+ // installer may have published a launcher at the original path since.
923
+ codexShimRollbackRestoreHookForTests?.(target);
924
+ restoreWithoutReplacing(target.backupPath, target.originalPath);
925
+ }
846
926
  }
847
927
  } catch (error) {
848
928
  errors.push(error instanceof Error ? error : new Error(String(error)));
@@ -1832,10 +1912,25 @@ function installCodexShimInternal(options: InstallCodexShimInternalOptions): { i
1832
1912
  if (existsSync(target.originalPath)) {
1833
1913
  renameSync(target.originalPath, target.backupPath);
1834
1914
  entry.originalMovedToBackup = true;
1915
+ // Metadata-only, and before the content probe: an empty or otherwise
1916
+ // unprobeable launcher must still be restorable during rollback.
1917
+ //
1918
+ // Only Unix reaches the rollback path (Windows rethrows freshApplyError
1919
+ // without rolling back), so only Unix may treat a missing fingerprint as
1920
+ // fatal. Throwing here on Windows would abort AFTER the original moved,
1921
+ // stranding the launcher at its backup path with nothing to restore it.
1922
+ const movedOriginalFingerprint = shimPathFingerprint(target.backupPath);
1923
+ if (movedOriginalFingerprint) entry.movedOriginalFingerprint = movedOriginalFingerprint;
1835
1924
  if (process.platform !== "win32") {
1925
+ if (!movedOriginalFingerprint) {
1926
+ throw new Error("Codex shim fresh install could not fingerprint the staged launcher");
1927
+ }
1836
1928
  const movedOriginal = stableShimPathProbe(target.backupPath);
1837
- if (!movedOriginal) throw new Error("Codex shim fresh install could not fingerprint the staged launcher");
1838
- entry.movedOriginalFingerprint = movedOriginal.fingerprint;
1929
+ // A content probe still runs where it can, purely as a consistency
1930
+ // check: disagreement means the file moved under us mid-install.
1931
+ if (movedOriginal && !sameFingerprint(movedOriginal.fingerprint, movedOriginalFingerprint)) {
1932
+ throw new Error("Codex shim fresh install staged launcher changed while being fingerprinted");
1933
+ }
1839
1934
  }
1840
1935
  }
1841
1936
  if (!target.preserveOnly) {
@@ -39,6 +39,7 @@ export {
39
39
  } from "./failover";
40
40
  export {
41
41
  comboIdFromRawBody,
42
+ comboRequestHasImageInput,
42
43
  concreteComboRequestBody,
43
44
  resetComboEffortWarningStateForTests,
44
45
  } from "./request";
@@ -23,6 +23,36 @@ export function comboIdFromRawBody(body: unknown, config: OcxConfig): string | n
23
23
  return resolveComboId(config, model);
24
24
  }
25
25
 
26
+ /**
27
+ * Detect image-bearing Responses *input* only.
28
+ *
29
+ * Must not walk the full request body: tool JSON schemas, metadata, or extension
30
+ * payloads can legally contain `{ "type": "input_image" }` without any image
31
+ * being dispatched. After previous_response_id expansion, scan the materialised
32
+ * `input` tree (message content and function_call_output.output).
33
+ */
34
+ export function comboRequestHasImageInput(body: unknown): boolean {
35
+ if (!body || typeof body !== "object" || Array.isArray(body)) return false;
36
+ return responsesInputHasImage((body as { input?: unknown }).input);
37
+ }
38
+
39
+ function responsesInputHasImage(input: unknown): boolean {
40
+ if (typeof input === "string" || input == null) return false;
41
+ if (!Array.isArray(input)) return false;
42
+ return input.some(responsesInputNodeHasImage);
43
+ }
44
+
45
+ function responsesInputNodeHasImage(value: unknown): boolean {
46
+ if (!value || typeof value !== "object") return false;
47
+ if (Array.isArray(value)) return value.some(responsesInputNodeHasImage);
48
+ const record = value as Record<string, unknown>;
49
+ if (record.type === "input_image") return true;
50
+ // Message content parts and nested function_call_output content/output arrays.
51
+ if (record.content !== undefined && responsesInputNodeHasImage(record.content)) return true;
52
+ if (record.output !== undefined && responsesInputNodeHasImage(record.output)) return true;
53
+ return false;
54
+ }
55
+
26
56
  export function concreteComboRequestBody(
27
57
  body: unknown,
28
58
  target: Pick<OcxComboTarget, "provider" | "model">,
@@ -37,6 +37,8 @@ export interface NormalizedComboConfig {
37
37
  strategy: OcxComboStrategy;
38
38
  stickyLimit: number;
39
39
  defaultEffort: OcxComboDefaultEffort | null;
40
+ /** Disable image input; `auto` preserves the intersection derived from all targets. */
41
+ imageInput: "auto" | "disabled";
40
42
  /** Trimmed public alias, or null when the combo keeps the default `combo/<id>` slug. */
41
43
  alias: string | null;
42
44
  /** Explicit native-family alias opt-in. */
@@ -220,6 +222,9 @@ export function comboConfigIssues(
220
222
  message: "defaultEffort must be one of: low, medium, high, xhigh, max, ultra",
221
223
  });
222
224
  }
225
+ if (body.imageInput !== undefined && body.imageInput !== "auto" && body.imageInput !== "disabled") {
226
+ issues.push({ path: ["imageInput"], message: 'imageInput must be "auto" or "disabled"' });
227
+ }
223
228
 
224
229
  if (body.alias !== undefined) {
225
230
  if (typeof body.alias !== "string") {
@@ -339,6 +344,7 @@ export function normalizeComboConfig(raw: OcxComboConfig): NormalizedComboConfig
339
344
  strategy: raw.strategy ?? "failover",
340
345
  stickyLimit: raw.stickyLimit ?? 1,
341
346
  defaultEffort: raw.defaultEffort ?? null,
347
+ imageInput: raw.imageInput === "disabled" ? "disabled" : "auto",
342
348
  alias: alias || null,
343
349
  nativeAlias: raw.nativeAlias === true,
344
350
  displayName: displayName || null,
package/src/config.ts CHANGED
@@ -688,6 +688,33 @@ const retryOn429PolicySchema = z.object({
688
688
  respectRetryAfter: z.boolean().optional(),
689
689
  }).strict();
690
690
 
691
+ const requestPacingRuleSchema = z.object({
692
+ // Keep the RPM-derived timer within the same one-hour bound as minIntervalMs.
693
+ requestsPerMinute: z.number().min(1 / 60).max(60_000).optional(),
694
+ minIntervalMs: z.number().int().min(1).max(3_600_000).optional(),
695
+ }).strict().refine(value => value.requestsPerMinute !== undefined || value.minIntervalMs !== undefined, {
696
+ message: "request pacing rules need requestsPerMinute or minIntervalMs",
697
+ });
698
+
699
+ const requestPacingSchema = z.object({
700
+ enabled: z.boolean(),
701
+ requestsPerMinute: z.number().min(1 / 60).max(60_000).optional(),
702
+ minIntervalMs: z.number().int().min(1).max(3_600_000).optional(),
703
+ models: z.record(z.string().trim().min(1), requestPacingRuleSchema).optional(),
704
+ }).strict().refine(value => value.enabled === false
705
+ || value.requestsPerMinute !== undefined
706
+ || value.minIntervalMs !== undefined
707
+ || (value.models !== undefined && Object.keys(value.models).length > 0), {
708
+ message: "enabled request pacing needs a provider rule or model override",
709
+ });
710
+
711
+ export function requestPacingConfigError(value: unknown): string | null {
712
+ if (value === undefined) return null;
713
+ const parsed = requestPacingSchema.safeParse(value);
714
+ if (parsed.success) return null;
715
+ return "requestPacing must contain enabled and a valid requestsPerMinute/minIntervalMs provider rule or model overrides";
716
+ }
717
+
691
718
  /**
692
719
  * Zod schema for one provider entry: known fields are validated strictly while unknown
693
720
  * fields pass through (preserved for runtime extensions).
@@ -695,6 +722,7 @@ const retryOn429PolicySchema = z.object({
695
722
  const providerConfigSchema = z.object({
696
723
  adapter: z.string().min(1),
697
724
  baseUrl: z.string().min(1),
725
+ requestPacing: requestPacingSchema.optional().catch(undefined),
698
726
  mcpMaxTools: z.number().int().positive().optional(),
699
727
  mcpMaxSchemaBytes: z.number().int().positive().optional(),
700
728
  mcpMaxResultBytes: z.number().int().positive().optional(),
@@ -703,6 +731,7 @@ const providerConfigSchema = z.object({
703
731
  statelessResponses: z.boolean().optional(),
704
732
  requiresAdjacentResponsesToolResults: z.boolean().optional(),
705
733
  supportsServiceTier: z.boolean().optional(),
734
+ modelSupportsServiceTier: z.record(z.string().min(1), z.boolean()).optional(),
706
735
  preserveResponsesReasoningContent: z.boolean().optional(),
707
736
  allowPrivateNetwork: z.boolean().optional(),
708
737
  noStructuredOutputModels: z.array(z.string().min(1))
@@ -1244,6 +1273,8 @@ const configSchema = z.object({
1244
1273
  ]).optional().catch(undefined),
1245
1274
  providers: z.record(z.string(), providerConfigSchema),
1246
1275
  defaultProvider: z.string().min(1).default("openai"),
1276
+ // A retry can be billable, so absence and malformed hand edits both stay off.
1277
+ emptyCompletionRetry: z.boolean().optional().catch(false),
1247
1278
  openaiProviderTierVersion: z.union([z.literal(1), z.literal(2)]).optional(),
1248
1279
  // Invalid hand edits must not discard an otherwise usable config.
1249
1280
  googleAntigravityStaticCatalogVersion: z.union([z.literal(1), z.literal(2)]).optional().catch(undefined),
@@ -1483,6 +1514,17 @@ const configSchema = z.object({
1483
1514
  message: reasoningSummariesError,
1484
1515
  });
1485
1516
  }
1517
+ const serviceTierModelsError = booleanRecordConfigError(
1518
+ (provider as { modelSupportsServiceTier?: unknown }).modelSupportsServiceTier,
1519
+ "modelSupportsServiceTier",
1520
+ );
1521
+ if (serviceTierModelsError) {
1522
+ ctx.addIssue({
1523
+ code: "custom",
1524
+ path: ["providers", redactSecretString(name), "modelSupportsServiceTier"],
1525
+ message: serviceTierModelsError,
1526
+ });
1527
+ }
1486
1528
  const reasoningSummaryDeliveryError = reasoningSummaryDeliveryRecordConfigError(
1487
1529
  (provider as { modelReasoningSummaryDelivery?: unknown }).modelReasoningSummaryDelivery,
1488
1530
  (provider as { modelSupportsReasoningSummaries?: unknown }).modelSupportsReasoningSummaries,
@@ -2331,6 +2373,14 @@ function codexAccountPickerEnabledError(value: unknown): string | null {
2331
2373
  return "schema_invalid: codexAccountPickerEnabled: must be a boolean or omitted";
2332
2374
  }
2333
2375
 
2376
+ function emptyCompletionRetryError(value: unknown): string | null {
2377
+ const raw = rawConfigRecord(value);
2378
+ if (!raw || !Object.hasOwn(raw, "emptyCompletionRetry")) return null;
2379
+ const enabled = raw.emptyCompletionRetry;
2380
+ if (enabled === undefined || typeof enabled === "boolean") return null;
2381
+ return "schema_invalid: emptyCompletionRetry: must be a boolean or omitted";
2382
+ }
2383
+
2334
2384
  /** Validate an in-memory config candidate without touching disk. Used by headless CLI import/set. */
2335
2385
  /**
2336
2386
  * Reject a loopback-listener port that collides with the proxy port (#1102).
@@ -2379,6 +2429,7 @@ export function validateConfigCandidate(value: unknown): { ok: true; config: Ocx
2379
2429
  ?? googleAntigravityStaticCatalogVersionError(value)
2380
2430
  ?? codexAccountPrioritiesError(value)
2381
2431
  ?? codexAccountPickerEnabledError(value)
2432
+ ?? emptyCompletionRetryError(value)
2382
2433
  ?? loopbackListenerPortError(value);
2383
2434
  if (boundaryError) return { ok: false, error: boundaryError };
2384
2435
  const result = configSchema.safeParse(value);
@@ -3229,6 +3280,7 @@ export function getDefaultConfig(): OcxConfig {
3229
3280
  // Adding extra providers (e.g. opencode-go) and switching defaultProvider is a user/runtime choice.
3230
3281
  return {
3231
3282
  port: 10100,
3283
+ emptyCompletionRetry: false,
3232
3284
  managementUsageMaxReadBytes: 64 * 1024 * 1024,
3233
3285
  appOwnedMemoryBudgetMb: DEFAULT_APP_OWNED_MEMORY_BUDGET_BYTES / (1024 * 1024),
3234
3286
  // Fresh/re-initialized configs are already written in the current three-tier