@yansigit/opencodex 2.33.1 → 2.35.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 (177) hide show
  1. package/README.md +3 -3
  2. package/gui/dist/assets/index-BjCaHxdz.js +112 -0
  3. package/gui/dist/assets/index-DLkXOXLC.css +1 -0
  4. package/gui/dist/index.html +2 -2
  5. package/package.json +1 -3
  6. package/src/adapters/anthropic.ts +79 -2
  7. package/src/adapters/command-code.ts +40 -3
  8. package/src/adapters/cursor/call-id.ts +44 -0
  9. package/src/adapters/cursor/checkpoint-store.ts +15 -10
  10. package/src/adapters/cursor/discovery.ts +60 -2
  11. package/src/adapters/cursor/effort-map.ts +79 -1
  12. package/src/adapters/cursor/envelope-echo.ts +9 -9
  13. package/src/adapters/cursor/live-models.ts +7 -2
  14. package/src/adapters/cursor/live-transport.ts +14 -0
  15. package/src/adapters/cursor/message-mapper.ts +4 -1
  16. package/src/adapters/cursor/native-exec-shell.ts +103 -6
  17. package/src/adapters/cursor/native-exec.ts +20 -0
  18. package/src/adapters/cursor/protobuf-request.ts +74 -39
  19. package/src/adapters/cursor/request-builder.ts +32 -10
  20. package/src/adapters/cursor/tool-definitions.ts +13 -10
  21. package/src/adapters/cursor/tool-result-normalize.ts +35 -3
  22. package/src/adapters/cursor/types.ts +16 -4
  23. package/src/adapters/cursor.ts +97 -70
  24. package/src/adapters/google-antigravity-replay.ts +105 -25
  25. package/src/adapters/google-antigravity-wire.ts +5 -0
  26. package/src/adapters/google-errors.ts +41 -12
  27. package/src/adapters/google-http.ts +12 -11
  28. package/src/adapters/google.ts +156 -65
  29. package/src/adapters/image.ts +1 -1
  30. package/src/adapters/kiro-constants.ts +15 -0
  31. package/src/adapters/kiro-tools.ts +43 -15
  32. package/src/adapters/kiro.ts +54 -9
  33. package/src/adapters/openai-chat.ts +286 -242
  34. package/src/adapters/openai-responses.ts +334 -24
  35. package/src/adapters/run-turn-queue.ts +36 -1
  36. package/src/adapters/tool-catalog-nudge.ts +2 -2
  37. package/src/adapters/xai-tool-schema.ts +436 -0
  38. package/src/bridge.ts +67 -26
  39. package/src/chat/inbound.ts +14 -1
  40. package/src/chat/outbound.ts +15 -7
  41. package/src/claude/agents-inject.ts +8 -1
  42. package/src/claude/outbound.ts +10 -8
  43. package/src/cli/account-api.ts +27 -7
  44. package/src/cli/account-extended.ts +10 -3
  45. package/src/cli/account.ts +29 -5
  46. package/src/cli/alias.ts +66 -0
  47. package/src/cli/claude.ts +26 -1
  48. package/src/cli/dispatch.ts +13 -1
  49. package/src/cli/help.ts +1 -0
  50. package/src/cli/index.ts +5 -0
  51. package/src/cli/init.ts +1 -0
  52. package/src/cli/models-runtime.ts +95 -0
  53. package/src/cli/models.ts +13 -7
  54. package/src/cli/provider-runtime.ts +16 -2
  55. package/src/cli/registry.ts +6 -1
  56. package/src/cli/telemetry-commands.ts +25 -0
  57. package/src/cli/v2.ts +34 -10
  58. package/src/codex/account-pause.ts +2 -1
  59. package/src/codex/account-priority.ts +3 -2
  60. package/src/codex/app-server-processes.ts +80 -6
  61. package/src/codex/auth-api.ts +48 -8
  62. package/src/codex/auth-context.ts +21 -18
  63. package/src/codex/catalog/aggregation.ts +6 -0
  64. package/src/codex/catalog/model-metadata.ts +13 -1
  65. package/src/codex/catalog/native-models.ts +5 -2
  66. package/src/codex/catalog/parsing.ts +16 -0
  67. package/src/codex/catalog/provider-fetch.ts +20 -37
  68. package/src/codex/catalog/sync.ts +127 -2
  69. package/src/codex/catalog.ts +1 -1
  70. package/src/codex/codex-write-lock.ts +3 -1
  71. package/src/codex/convergence-types.ts +1 -1
  72. package/src/codex/convergence.ts +22 -2
  73. package/src/codex/desired-state.ts +2 -2
  74. package/src/codex/desktop-app-restart.ts +18 -5
  75. package/src/codex/inject-coordination.ts +83 -0
  76. package/src/codex/inject.ts +14 -1
  77. package/src/codex/log-guard/inspect.ts +22 -4
  78. package/src/codex/model-entitlements.ts +9 -2
  79. package/src/codex/prompt-layers.ts +371 -25
  80. package/src/codex/prompt-text-probe.ts +238 -0
  81. package/src/codex/quota.ts +123 -18
  82. package/src/codex/routing.ts +9 -0
  83. package/src/codex/subagent-model-fallback.ts +198 -27
  84. package/src/codex/transition-state.ts +107 -8
  85. package/src/combos/types.ts +10 -0
  86. package/src/compatibility/openai-responses.ts +33 -1
  87. package/src/config/autonomous-remediation.ts +21 -0
  88. package/src/config/provider-validation.ts +14 -0
  89. package/src/config/rebase-provenance.ts +68 -0
  90. package/src/config.ts +191 -17
  91. package/src/generated/compatibility-version.json +229 -153
  92. package/src/images/loop.ts +5 -4
  93. package/src/lab/conformance/fixtures/protocol-v1-cases.json +1 -1
  94. package/src/lab/fabric/producer-child.ts +1 -1
  95. package/src/lib/config-ownership.ts +20 -0
  96. package/src/lib/errors.ts +11 -2
  97. package/src/lib/package-tree-integrity.ts +101 -0
  98. package/src/oauth/aistudio-credentials.ts +65 -0
  99. package/src/oauth/aistudio-native-daemon.ts +54 -0
  100. package/src/oauth/generic-account-failover.ts +231 -0
  101. package/src/oauth/index.ts +57 -5
  102. package/src/oauth/key-providers.ts +10 -1
  103. package/src/oauth/kiro.ts +45 -0
  104. package/src/oauth/login-cli.ts +19 -20
  105. package/src/oauth/types.ts +15 -0
  106. package/src/providers/codex-capacity.ts +5 -2
  107. package/src/providers/command-code-efforts.ts +38 -6
  108. package/src/providers/context-cap.ts +4 -3
  109. package/src/providers/default-aliases.ts +65 -0
  110. package/src/providers/derive.ts +28 -0
  111. package/src/providers/fastwire.ts +7 -1
  112. package/src/providers/model-presets.ts +119 -0
  113. package/src/providers/new-model-policy.ts +146 -0
  114. package/src/providers/provider-id-rewrite.ts +2 -1
  115. package/src/providers/quota.ts +73 -14
  116. package/src/providers/registry.ts +160 -68
  117. package/src/providers/slug-codec.ts +52 -0
  118. package/src/responses/code-mode-helper-compat.ts +50 -0
  119. package/src/responses/custom-tool-compat.ts +34 -10
  120. package/src/responses/parser.ts +4 -0
  121. package/src/responses/schema.ts +5 -1
  122. package/src/responses/thought-signature-replay.ts +17 -0
  123. package/src/router.ts +40 -2
  124. package/src/routing/analytics.ts +1 -0
  125. package/src/routing/quota.ts +10 -0
  126. package/src/server/auth-cors.ts +23 -0
  127. package/src/server/chat-completions.ts +24 -16
  128. package/src/server/chat-native-sse.ts +3 -3
  129. package/src/server/chat-native.ts +30 -11
  130. package/src/server/claude-messages.ts +1 -1
  131. package/src/server/effort-policy.ts +16 -0
  132. package/src/server/index.ts +152 -80
  133. package/src/server/lifecycle.ts +52 -1
  134. package/src/server/management/agent-settings-routes.ts +31 -15
  135. package/src/server/management/codex-prompt-routes.ts +570 -0
  136. package/src/server/management/combo-routes.ts +2 -1
  137. package/src/server/management/config-routes.ts +27 -9
  138. package/src/server/management/context.ts +9 -0
  139. package/src/server/management/logs-usage-routes.ts +1 -1
  140. package/src/server/management/model-routes.ts +266 -0
  141. package/src/server/management/provider-routes.ts +137 -3
  142. package/src/server/management/routing-profile-routes.ts +2 -2
  143. package/src/server/management-api.ts +2 -0
  144. package/src/server/relay-eager.ts +147 -20
  145. package/src/server/relay.ts +251 -19
  146. package/src/server/request-log-conversation.ts +21 -0
  147. package/src/server/request-log.ts +46 -20
  148. package/src/server/responses/collaboration.ts +42 -5
  149. package/src/server/responses/combo-stream-preflight.ts +10 -3
  150. package/src/server/responses/core.ts +571 -137
  151. package/src/server/responses/empty-completion-guard.ts +35 -0
  152. package/src/server/responses/fetch-helpers.ts +14 -6
  153. package/src/server/responses/input-admission.ts +3 -1
  154. package/src/server/responses/passthrough-error.ts +33 -9
  155. package/src/server/responses/responses-field-backfill.ts +105 -13
  156. package/src/server/responses/ws-upstream.ts +35 -5
  157. package/src/server/responses-custom-tool-repair.ts +52 -7
  158. package/src/server/responses-terminal-repair.ts +25 -4
  159. package/src/server/sse-frame-buffer.ts +31 -4
  160. package/src/server/ws-bridge.ts +15 -4
  161. package/src/telemetry/dispatcher.ts +44 -0
  162. package/src/telemetry/fingerprint.ts +24 -0
  163. package/src/telemetry/hook.ts +43 -0
  164. package/src/telemetry/ledger.ts +54 -0
  165. package/src/telemetry/types.ts +23 -0
  166. package/src/types/config.ts +66 -14
  167. package/src/types/provider.ts +77 -0
  168. package/src/types/request.ts +16 -10
  169. package/src/types/tools.ts +20 -25
  170. package/src/types.ts +1 -0
  171. package/src/usage/expected-prices.ts +43 -24
  172. package/src/usage/log.ts +17 -7
  173. package/src/web-search/exa-executor.ts +40 -9
  174. package/src/web-search/loop.ts +5 -4
  175. package/gui/dist/assets/index-CIDo4y4k.js +0 -102
  176. package/gui/dist/assets/index-DrSQdTRd.css +0 -1
  177. package/src/server/aistudio-ws-hub.ts +0 -295
@@ -0,0 +1,68 @@
1
+ import type { OcxConfig } from "../types";
2
+
3
+ const pendingTopLevelDeletions = new WeakMap<OcxConfig, Set<string>>();
4
+ export const CONFIG_REBASE_PROVENANCE_KEY = "configRebaseProvenance";
5
+
6
+ export function parsedConfigRebaseDeletionKeys(config: OcxConfig): Set<string> | null {
7
+ const value = config.configRebaseProvenance;
8
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
9
+ const record = value as Record<string, unknown>;
10
+ if (record.version !== 1 || !Array.isArray(record.deletedTopLevelKeys)) return null;
11
+ if (!record.deletedTopLevelKeys.every(key => typeof key === "string" && key !== CONFIG_REBASE_PROVENANCE_KEY)) {
12
+ return null;
13
+ }
14
+ return new Set(record.deletedTopLevelKeys as string[]);
15
+ }
16
+
17
+ export function configRebaseDeletionKeys(config: OcxConfig): Set<string> {
18
+ const deleted = new Set([
19
+ ...(parsedConfigRebaseDeletionKeys(config) ?? []),
20
+ ...(pendingTopLevelDeletions.get(config) ?? []),
21
+ ]);
22
+ const record = config as unknown as Record<string, unknown>;
23
+ for (const key of [...deleted]) {
24
+ if (Object.hasOwn(record, key) && record[key] !== undefined) deleted.delete(key);
25
+ }
26
+ return deleted;
27
+ }
28
+
29
+ export function configHasRebaseProvenance(config: OcxConfig): boolean {
30
+ return parsedConfigRebaseDeletionKeys(config) !== null || pendingTopLevelDeletions.has(config);
31
+ }
32
+
33
+ export function projectConfigRebaseProvenance(config: OcxConfig): OcxConfig {
34
+ const pending = pendingTopLevelDeletions.get(config);
35
+ const parsed = parsedConfigRebaseDeletionKeys(config);
36
+ // Preserve unknown future metadata byte-for-value. It carries no authority here.
37
+ if (config.configRebaseProvenance !== undefined && parsed === null) return config;
38
+ const deleted = new Set([...(parsed ?? []), ...(pending ?? [])]);
39
+ const record = config as unknown as Record<string, unknown>;
40
+ for (const key of [...deleted]) {
41
+ if (Object.hasOwn(record, key) && record[key] !== undefined) deleted.delete(key);
42
+ }
43
+ // A SHALLOW copy, deliberately. A provider entry may carry a non-cloneable value — a test
44
+ // fixture injects its own `fetch`, and `structuredClone` throws DataCloneError on a function,
45
+ // which took every provider-probe and CLI-parity suite down. Only the provenance key is
46
+ // rewritten here, so nothing below the top level needs to be copied at all; the deep clone
47
+ // was doing work this projection never asked for.
48
+ const projected = { ...config };
49
+ if (deleted.size === 0) delete projected.configRebaseProvenance;
50
+ else projected.configRebaseProvenance = {
51
+ version: 1,
52
+ deletedTopLevelKeys: [...deleted].sort(),
53
+ };
54
+ return projected;
55
+ }
56
+
57
+ /** Delete one top-level config key and retain the writer's explicit intent for rebasing. */
58
+ export function deleteConfigTopLevelKey<K extends keyof OcxConfig>(config: OcxConfig, key: K): void {
59
+ delete config[key];
60
+ if (key === CONFIG_REBASE_PROVENANCE_KEY) return;
61
+ const deleted = pendingTopLevelDeletions.get(config) ?? new Set<string>();
62
+ deleted.add(key);
63
+ pendingTopLevelDeletions.set(config, deleted);
64
+ }
65
+
66
+ export function clearPendingConfigTopLevelDeletions(config: OcxConfig): void {
67
+ pendingTopLevelDeletions.delete(config);
68
+ }
package/src/config.ts CHANGED
@@ -16,7 +16,9 @@ import {
16
16
  providerBaseUrlConfigError,
17
17
  providerHeadersConfigError,
18
18
  reasoningSummaryDeliveryRecordConfigError,
19
+ maxWsFrameBytesConfigError,
19
20
  upstreamHttpVersionConfigError,
21
+ wsUpstreamConfigError,
20
22
  } from "./config/provider-validation";
21
23
  import {
22
24
  bumpConfigGenerationAtPath,
@@ -62,6 +64,7 @@ import { providerDestinationConfigError } from "./lib/destination-policy";
62
64
  import { redactSecretString } from "./lib/redact";
63
65
  import { antigravityOAuthDestinationConfigError, providerTlsProfileConfigError } from "./lib/provider-tls-profile";
64
66
  import { openRouterRoutingConfigError } from "./providers/openrouter-routing";
67
+ import { MODEL_ALIAS_PATTERN } from "./providers/default-aliases";
65
68
  import {
66
69
  MODEL_ADAPTER_OVERRIDE_ALLOWED,
67
70
  OPENAI_PROVIDER_TIER_VERSION,
@@ -84,6 +87,7 @@ import {
84
87
  registryModelServiceTierCapabilityApplies,
85
88
  } from "./providers/registry";
86
89
  import { resolveOpenAiVirtualModel } from "./providers/openai-virtual-models";
90
+ import { slugEquivalenceKey, slugsEquivalent } from "./providers/slug-codec";
87
91
  import { parseDesktopProfile } from "./claude/desktop-profile";
88
92
  import { isCodexReasoningEffort } from "./reasoning-effort";
89
93
  import { parseSubagentRoles, salvageSubagentRoles } from "./codex/agent-roles";
@@ -144,6 +148,15 @@ export {
144
148
  writeRuntimePort,
145
149
  type RuntimePortState,
146
150
  } from "./config/process-state";
151
+ import {
152
+ clearPendingConfigTopLevelDeletions,
153
+ configHasRebaseProvenance,
154
+ configRebaseDeletionKeys,
155
+ CONFIG_REBASE_PROVENANCE_KEY,
156
+ deleteConfigTopLevelKey,
157
+ projectConfigRebaseProvenance,
158
+ } from "./config/rebase-provenance";
159
+ export { deleteConfigTopLevelKey } from "./config/rebase-provenance";
147
160
 
148
161
  export class OpenAiTierBackupCleanupError extends Error {
149
162
  constructor() { super("OpenAI tier backup temporary cleanup failed"); this.name = "OpenAiTierBackupCleanupError"; }
@@ -491,8 +504,10 @@ const providerConfigSchema = z.object({
491
504
  }).strict().transform(value => value.managedIdentityClientId === undefined
492
505
  ? value
493
506
  : { ...value, managedIdentityClientId: value.managedIdentityClientId.trim() }).optional(),
507
+ alias: z.string().optional(),
508
+ modelAliases: z.record(z.string(), z.string()).optional(),
509
+ defaultAliases: z.boolean().optional(),
494
510
  requestPacing: requestPacingSchema.optional().catch(undefined),
495
- tlsProfile: z.literal("antigravity-browser").optional(),
496
511
  mcpMaxTools: z.number().int().positive().optional(),
497
512
  mcpMaxSchemaBytes: z.number().int().positive().optional(),
498
513
  mcpMaxResultBytes: z.number().int().positive().optional(),
@@ -506,6 +521,8 @@ const providerConfigSchema = z.object({
506
521
  preserveResponsesReasoningContent: z.boolean().optional(),
507
522
  decodesNativeCompactionBlobs: z.boolean().optional(),
508
523
  allowPrivateNetwork: z.boolean().optional(),
524
+ wsUpstream: z.boolean().nullish().transform(value => value ?? undefined),
525
+ maxWsFrameBytes: z.number().int().positive().nullish().transform(value => value ?? undefined),
509
526
  // The management API accepts `null` as "clear this", so a config written before the POST
510
527
  // canonicalization below can hold one on disk. Rejecting it here would send the operator
511
528
  // through invalid-config recovery for a value the API told them was fine.
@@ -523,8 +540,8 @@ const providerConfigSchema = z.object({
523
540
  // accepted, persisted, and then silently resolved to the `code_mode_only` default — the
524
541
  // operator asked for shell mode, got code mode, and was told nothing (#2106).
525
542
  codexToolMode: z.enum(["code_mode_only", "shell"]).optional(),
526
- // Validated rather than passed through: same rationale as codexToolMode above.
527
543
  projectContext: z.enum(["off", "on"]).optional(),
544
+ tlsProfile: z.literal("antigravity-browser").optional(),
528
545
  responsesItemIdRepair: z.object({
529
546
  message: z.array(z.string().min(1)).optional(),
530
547
  reasoning: z.array(z.string().min(1)).optional(),
@@ -548,7 +565,9 @@ export {
548
565
  providerBaseUrlConfigError,
549
566
  providerHeadersConfigError,
550
567
  reasoningSummaryDeliveryRecordConfigError,
568
+ maxWsFrameBytesConfigError,
551
569
  upstreamHttpVersionConfigError,
570
+ wsUpstreamConfigError,
552
571
  } from "./config/provider-validation";
553
572
 
554
573
  function providerResponsesPathConfigError(responsesPath: string | undefined): string | null {
@@ -858,6 +877,12 @@ const v2NativeParentOverrideSchema = z.object({
858
877
 
859
878
  const configSchema = z.object({
860
879
  port: z.number().int().min(0).max(65535).default(10100),
880
+ autonomousRemediation: z.object({
881
+ enabled: z.boolean().optional(),
882
+ instanceId: z.string().trim().min(1).optional(),
883
+ threshold: z.number().int().positive().optional(),
884
+ rollingWindowMs: z.number().int().positive().optional(),
885
+ }).strict().optional().catch(undefined),
861
886
  managementUsageMaxReadBytes: z.number().int().positive().default(64 * 1024 * 1024),
862
887
  // Invalid hand edits disable only this opt-in circuit. Live writes remain strict.
863
888
  upstreamHostCircuitThreshold: z.number().int()
@@ -887,6 +912,10 @@ const configSchema = z.object({
887
912
  ]).optional().catch(undefined),
888
913
  providers: z.record(z.string(), providerConfigSchema),
889
914
  defaultProvider: z.string().min(1).default("openai"),
915
+ defaultModelAliases: z.boolean().optional(),
916
+ // Future versions remain opaque through passthrough-compatible whole-config saves.
917
+ // Only version 1 grants deletion authority in the rebase path.
918
+ configRebaseProvenance: z.unknown().optional(),
890
919
  // A retry can be billable, so absence and malformed hand edits both stay off.
891
920
  emptyCompletionRetry: z.boolean().optional().catch(false),
892
921
  // A malformed hand edit must not silently stop opening the browser: fall back
@@ -899,7 +928,6 @@ const configSchema = z.object({
899
928
  providerContextCaps: z.record(z.string(), z.number().int().positive()).optional(),
900
929
  contextCapValue: z.number().int().positive().optional(),
901
930
  multiAgentGuidanceEnabled: z.boolean().optional(),
902
- // Invalid hand edits disable only this experimental opt-in subtree.
903
931
  v2NativeParentOverride: v2NativeParentOverrideSchema.optional().catch(undefined),
904
932
  // Invalid optional recovery config must not discard unrelated provider/account state.
905
933
  agentTaskRecovery: agentTaskRecoverySchema.optional().catch(undefined),
@@ -911,7 +939,6 @@ const configSchema = z.object({
911
939
  injectionEffort: z.string().optional().catch(undefined),
912
940
  syncCodexSubagentDefaults: z.boolean().optional().catch(undefined),
913
941
  syncCodexAgentRoles: z.boolean().optional().catch(undefined),
914
- // Salvage element by element. A malformed neighbour must not wipe the catalog.
915
942
  subagentRoles: z.unknown().optional(),
916
943
  // Per-primary-model fallback chains. Values must be non-empty string arrays;
917
944
  // malformed entries degrade to undefined rather than rejecting the whole config.
@@ -919,6 +946,10 @@ const configSchema = z.object({
919
946
  z.string(),
920
947
  z.array(z.string().trim().min(1)).min(1),
921
948
  ).optional().catch(undefined),
949
+ subagentCandidates: z.union([
950
+ z.array(z.string().trim().min(1)).min(1),
951
+ z.record(z.string().trim().min(1), z.array(z.string().trim().min(1)).min(1)),
952
+ ]).optional().catch(undefined),
922
953
  codexShimAutoRestore: z.boolean().optional(),
923
954
  pausedCodexAccountIds: z.array(z.string().regex(/^[a-zA-Z0-9._-]{1,64}$/)).optional(),
924
955
  codexAccountNamespaces: codexAccountNamespacesSchema.optional(),
@@ -931,6 +962,9 @@ const configSchema = z.object({
931
962
  // A malformed hand edit must degrade to false without discarding providers, accounts,
932
963
  // or the exact selector map. Live writes remain strict.
933
964
  codexAccountPickerEnabled: z.boolean().optional().catch(false),
965
+ // Same degrade-not-reject rule: a malformed hand edit hides Spark rather than discarding the
966
+ // whole config. Hidden is also the default, so `catch(false)` and the default agree.
967
+ showCodexSparkQuota: z.boolean().optional().catch(false),
934
968
  // Model ids excluded from the Grok Build managed block (dashboard switches).
935
969
  grokExcludedModels: z.array(z.string()).optional(),
936
970
  // Invalid values degrade to undefined ("auto") instead of failing the whole
@@ -1069,6 +1103,22 @@ const configSchema = z.object({
1069
1103
  message: responsesPathError,
1070
1104
  });
1071
1105
  }
1106
+ const wsUpstreamError = wsUpstreamConfigError((provider as { wsUpstream?: unknown }).wsUpstream);
1107
+ if (wsUpstreamError) {
1108
+ ctx.addIssue({
1109
+ code: "custom",
1110
+ path: ["providers", redactSecretString(name), "wsUpstream"],
1111
+ message: wsUpstreamError,
1112
+ });
1113
+ }
1114
+ const maxWsFrameBytesError = maxWsFrameBytesConfigError((provider as { maxWsFrameBytes?: unknown }).maxWsFrameBytes);
1115
+ if (maxWsFrameBytesError) {
1116
+ ctx.addIssue({
1117
+ code: "custom",
1118
+ path: ["providers", redactSecretString(name), "maxWsFrameBytes"],
1119
+ message: maxWsFrameBytesError,
1120
+ });
1121
+ }
1072
1122
  const tlsProfileError = providerTlsProfileConfigError(name, provider);
1073
1123
  if (tlsProfileError) {
1074
1124
  ctx.addIssue({
@@ -1170,6 +1220,17 @@ const configSchema = z.object({
1170
1220
  message: reasoningSummariesError,
1171
1221
  });
1172
1222
  }
1223
+ const verbositySupportError = booleanRecordConfigError(
1224
+ (provider as { modelSupportsVerbosity?: unknown }).modelSupportsVerbosity,
1225
+ "modelSupportsVerbosity",
1226
+ );
1227
+ if (verbositySupportError) {
1228
+ ctx.addIssue({
1229
+ code: "custom",
1230
+ path: ["providers", redactSecretString(name), "modelSupportsVerbosity"],
1231
+ message: verbositySupportError,
1232
+ });
1233
+ }
1173
1234
  const serviceTierModelsError = booleanRecordConfigError(
1174
1235
  (provider as { modelSupportsServiceTier?: unknown }).modelSupportsServiceTier,
1175
1236
  "modelSupportsServiceTier",
@@ -1682,8 +1743,9 @@ function malformedSyncCodexAgentRolesWarning(rawParsed: unknown): string | null
1682
1743
  }
1683
1744
 
1684
1745
  function normalizeSyncCodexAgentRoles(config: OcxConfig, rawParsed: unknown): OcxConfig {
1685
- if (!malformedSyncCodexAgentRolesWarning(rawParsed)) return config;
1686
- return { ...config, syncCodexAgentRoles: false };
1746
+ return malformedSyncCodexAgentRolesWarning(rawParsed)
1747
+ ? { ...config, syncCodexAgentRoles: false }
1748
+ : config;
1687
1749
  }
1688
1750
 
1689
1751
  function warnDegradedSyncCodexAgentRoles(rawParsed: unknown): void {
@@ -1701,8 +1763,7 @@ function subagentRolesError(value: unknown): string | null {
1701
1763
  const raw = rawConfigRecord(value);
1702
1764
  if (!raw || !Object.hasOwn(raw, "subagentRoles")) return null;
1703
1765
  const parsed = parseSubagentRoles(raw.subagentRoles);
1704
- if (parsed.ok) return null;
1705
- return `schema_invalid: ${parsed.error}`;
1766
+ return parsed.ok ? null : `schema_invalid: ${parsed.error}`;
1706
1767
  }
1707
1768
 
1708
1769
  function malformedUpstreamHostCircuitThresholdWarning(rawParsed: unknown): string | null {
@@ -1873,6 +1934,7 @@ export function loadConfig(): OcxConfig {
1873
1934
  try {
1874
1935
  const raw = readFileSync(configPath, "utf-8").replace(/^\uFEFF/, "");
1875
1936
  const parsed = JSON.parse(raw);
1937
+ sanitizeAliasesForLoad(parsed);
1876
1938
  sanitizeRetryOn429ForLoad(parsed);
1877
1939
  sanitizeModelCostsForLoad(parsed);
1878
1940
  const result = configSchema.safeParse(parsed);
@@ -1950,6 +2012,43 @@ export function loadConfig(): OcxConfig {
1950
2012
  }
1951
2013
  }
1952
2014
 
2015
+ /** Hand-edited alias mistakes disable only the bad alias; providers and routing survive. */
2016
+ function sanitizeAliasesForLoad(raw: unknown): void {
2017
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return;
2018
+ const root = raw as Record<string, unknown>;
2019
+ if (!root.providers || typeof root.providers !== "object" || Array.isArray(root.providers)) return;
2020
+ const providers = root.providers as Record<string, Record<string, unknown>>;
2021
+ const providerNames = new Set(Object.keys(providers).map(name => name.toLowerCase()));
2022
+ const claimedProviders = new Set<string>();
2023
+ const comboAliases = new Set(Object.values((root.combos as Record<string, { alias?: unknown }> | undefined) ?? {})
2024
+ .map(combo => typeof combo?.alias === "string" ? combo.alias.toLowerCase() : "").filter(Boolean));
2025
+ const accountNamespaces = new Set(Object.keys((root.codexAccountNamespaces as Record<string, unknown> | undefined) ?? {}).map(name => name.toLowerCase()));
2026
+ for (const provider of Object.values(providers)) {
2027
+ const alias = provider.alias;
2028
+ if (typeof alias !== "string" || !isValidProviderName(alias)
2029
+ || providerNames.has(alias.toLowerCase()) || claimedProviders.has(alias.toLowerCase())
2030
+ || comboAliases.has(alias.toLowerCase()) || accountNamespaces.has(alias.toLowerCase())) {
2031
+ if (alias !== undefined) console.warn("Ignoring invalid or colliding provider alias in config.json");
2032
+ delete provider.alias;
2033
+ } else claimedProviders.add(alias.toLowerCase());
2034
+ if (!provider.modelAliases || typeof provider.modelAliases !== "object" || Array.isArray(provider.modelAliases)) {
2035
+ if (provider.modelAliases !== undefined) delete provider.modelAliases;
2036
+ continue;
2037
+ }
2038
+ const aliases = provider.modelAliases as Record<string, unknown>;
2039
+ const nativeIds = new Set((Array.isArray(provider.models) ? provider.models : []).filter((id): id is string => typeof id === "string").map(id => id.toLowerCase()));
2040
+ const claimed = new Set<string>();
2041
+ for (const [id, value] of Object.entries(aliases)) {
2042
+ const lower = typeof value === "string" ? value.toLowerCase() : "";
2043
+ if (typeof value !== "string" || !MODEL_ALIAS_PATTERN.test(value) || claimed.has(lower)
2044
+ || nativeIds.has(lower) || comboAliases.has(lower) || /^(?:gpt-|o1-|o3-|o4-|codex-)/i.test(value)) {
2045
+ console.warn(`Ignoring invalid or colliding model alias for ${id} in config.json`);
2046
+ delete aliases[id];
2047
+ } else claimed.add(lower);
2048
+ }
2049
+ }
2050
+ }
2051
+
1953
2052
  /** Refresh the user cost-overlay registry from `config` and return it unchanged. */
1954
2053
  function withRefreshedCostOverlays(config: OcxConfig): OcxConfig {
1955
2054
  refreshUserCostOverlays(config);
@@ -2021,6 +2120,52 @@ export function subagentDefaultSyncEffective(
2021
2120
  return config.syncCodexSubagentDefaults === true && Boolean(config.injectionModel?.trim());
2022
2121
  }
2023
2122
 
2123
+ /**
2124
+ * Resolve and normalize configured candidates for one spawned sub-agent.
2125
+ * Supports a global ordered list or a record keyed by role/model, with
2126
+ * `default` and `*` fallbacks for unmatched requests.
2127
+ */
2128
+ export function resolveSubagentCandidates(
2129
+ config: Pick<OcxConfig, "subagentCandidates"> | OcxConfig,
2130
+ roleOrModel?: string,
2131
+ ): string[] {
2132
+ const candidates = config?.subagentCandidates;
2133
+ if (!candidates) return [];
2134
+
2135
+ const normalizeList = (raw: unknown): string[] => {
2136
+ if (!Array.isArray(raw)) return [];
2137
+ const result: string[] = [];
2138
+ const seen = new Set<string>();
2139
+ for (const item of raw) {
2140
+ if (typeof item !== "string") continue;
2141
+ const trimmed = item.trim();
2142
+ if (!trimmed) continue;
2143
+ const key = slugEquivalenceKey(trimmed);
2144
+ if (seen.has(key)) continue;
2145
+ seen.add(key);
2146
+ result.push(trimmed);
2147
+ }
2148
+ return result;
2149
+ };
2150
+
2151
+ if (Array.isArray(candidates)) return normalizeList(candidates);
2152
+ if (typeof candidates !== "object") return [];
2153
+
2154
+ const record = candidates as Record<string, unknown>;
2155
+ const trimmed = roleOrModel?.trim();
2156
+ let selected: unknown;
2157
+ if (trimmed) {
2158
+ if (Object.hasOwn(record, trimmed)) {
2159
+ selected = record[trimmed];
2160
+ } else {
2161
+ const matchingKey = Object.keys(record).find(key => slugsEquivalent(key, trimmed));
2162
+ if (matchingKey) selected = record[matchingKey];
2163
+ }
2164
+ }
2165
+ if (!selected) selected = record.default ?? record["*"];
2166
+ return normalizeList(selected);
2167
+ }
2168
+
2024
2169
  function mergeConfigDefaults(parsed: unknown): unknown {
2025
2170
  if (!parsed || typeof parsed !== "object") return parsed;
2026
2171
  const defaults = getDefaultConfig();
@@ -2565,7 +2710,12 @@ function persistConfigUnlocked(config: OcxConfig): boolean {
2565
2710
  // External editors can add provider rows the live config deliberately does
2566
2711
  // not route with yet; merge them at the serialization boundary so an
2567
2712
  // unrelated in-process save cannot erase the provider or its overlay.
2713
+ // Provider preservation reads symbol-keyed live-owner state, which structuredClone
2714
+ // intentionally drops. Resolve that ownership before projecting JSON provenance.
2715
+ const provenanceProjection = projectConfigRebaseProvenance(config);
2568
2716
  const persisted = withPreservedDiskOnlyProviders(config);
2717
+ if (provenanceProjection.configRebaseProvenance === undefined) delete persisted.configRebaseProvenance;
2718
+ else persisted.configRebaseProvenance = provenanceProjection.configRebaseProvenance;
2569
2719
  const bytes = JSON.stringify(persisted, null, 2) + "\n";
2570
2720
  let unchanged = false;
2571
2721
  try {
@@ -2593,9 +2743,15 @@ export function saveConfig(config: OcxConfig): void {
2593
2743
  // Keep the real-home assertion ahead of even lock-directory preparation.
2594
2744
  assertNotRealHomeUnderTest(getConfigDir());
2595
2745
  withConfigMutationLockSync(() => {
2596
- const projected = projectCustomModelCatalogMigration(readRawConfigJson(), config);
2597
- if (persistConfigUnlocked(projected)) bumpGenerationForCooperatingConfigWrite();
2598
- adoptCustomModelCatalogMigration(config, projected);
2746
+ const withProvenance = projectCustomModelCatalogMigration(
2747
+ readRawConfigJson(),
2748
+ projectConfigRebaseProvenance(config),
2749
+ );
2750
+ if (persistConfigUnlocked(withProvenance)) bumpGenerationForCooperatingConfigWrite();
2751
+ adoptCustomModelCatalogMigration(config, withProvenance);
2752
+ if (withProvenance.configRebaseProvenance === undefined) delete config.configRebaseProvenance;
2753
+ else config.configRebaseProvenance = structuredClone(withProvenance.configRebaseProvenance);
2754
+ clearPendingConfigTopLevelDeletions(config);
2599
2755
  });
2600
2756
  }
2601
2757
 
@@ -2714,7 +2870,6 @@ const claudeCodeBaseline = new WeakMap<OcxConfig, unknown>();
2714
2870
  * reconciliation paths below.
2715
2871
  */
2716
2872
  const liveConfigBaseline = new WeakMap<OcxConfig, OcxConfig>();
2717
-
2718
2873
  /**
2719
2874
  * The live config retains the address of the socket Bun actually opened, while
2720
2875
  * this map retains the operator's desired address for the next process start.
@@ -3018,6 +3173,8 @@ export function saveConfigPreservingClaudeCode(config: OcxConfig): void {
3018
3173
  if (baseline && onDisk !== undefined) {
3019
3174
  const persistedDiagnostics = configDiagnosticsFromRaw(JSON.stringify(onDisk));
3020
3175
  if (persistedDiagnostics.source === "file") {
3176
+ const deletedKeys = configRebaseDeletionKeys(config);
3177
+ const provenanceExists = configHasRebaseProvenance(config);
3021
3178
  // Only keys this live config is actually known to have diverged on may be
3022
3179
  // rebased. The baseline is captured once when the server arms it, so any key
3023
3180
  // that appeared on disk afterwards — through saveConfig(), a hand edit, or
@@ -3031,8 +3188,11 @@ export function saveConfigPreservingClaudeCode(config: OcxConfig): void {
3031
3188
  const rebaseableKeys = new Set([
3032
3189
  ...Object.keys(baseline as unknown as Record<string, unknown>),
3033
3190
  ...Object.keys(config as unknown as Record<string, unknown>),
3191
+ ...(provenanceExists
3192
+ ? Object.keys(persistedDiagnostics.config as unknown as Record<string, unknown>)
3193
+ : []),
3034
3194
  ]);
3035
- const skipped = new Set(["hostname", "port", "claudeCode"]);
3195
+ const skipped = new Set(["hostname", "port", "claudeCode", CONFIG_REBASE_PROVENANCE_KEY]);
3036
3196
  for (const key of Object.keys(persistedDiagnostics.config as unknown as Record<string, unknown>)) {
3037
3197
  if (!rebaseableKeys.has(key)) skipped.add(key);
3038
3198
  }
@@ -3042,6 +3202,7 @@ export function saveConfigPreservingClaudeCode(config: OcxConfig): void {
3042
3202
  persistedDiagnostics.config as unknown as Record<string, unknown>,
3043
3203
  skipped,
3044
3204
  );
3205
+ for (const key of deletedKeys) delete (config as unknown as Record<string, unknown>)[key];
3045
3206
  }
3046
3207
  }
3047
3208
  if (claudeCodeBaseline.has(config)) {
@@ -3055,10 +3216,13 @@ export function saveConfigPreservingClaudeCode(config: OcxConfig): void {
3055
3216
  }
3056
3217
  }
3057
3218
  }
3219
+ const provenanceProjection = projectConfigRebaseProvenance(config);
3058
3220
  const projectedConfig = projectCustomModelCatalogMigration(
3059
3221
  onDisk,
3060
3222
  config,
3061
3223
  );
3224
+ if (provenanceProjection.configRebaseProvenance === undefined) delete projectedConfig.configRebaseProvenance;
3225
+ else projectedConfig.configRebaseProvenance = provenanceProjection.configRebaseProvenance;
3062
3226
  const persistedBinding = bindingBaseline && onDisk
3063
3227
  ? readPersistedServerBinding(onDisk, bindingBaseline)
3064
3228
  : bindingBaseline;
@@ -3076,8 +3240,11 @@ export function saveConfigPreservingClaudeCode(config: OcxConfig): void {
3076
3240
  claudeCodeBaseline.set(config, structuredClone(config.claudeCode));
3077
3241
  }
3078
3242
  if (liveConfigBaseline.has(config)) {
3079
- liveConfigBaseline.set(config, structuredClone(config));
3243
+ if (projectedConfig.configRebaseProvenance === undefined) delete config.configRebaseProvenance;
3244
+ else config.configRebaseProvenance = structuredClone(projectedConfig.configRebaseProvenance);
3245
+ liveConfigBaseline.set(config, structuredClone(projectedConfig));
3080
3246
  }
3247
+ clearPendingConfigTopLevelDeletions(config);
3081
3248
  });
3082
3249
  }
3083
3250
 
@@ -3153,10 +3320,17 @@ export function applyProxyEnv(config: OcxConfig): void {
3153
3320
  const existing = process.env.NO_PROXY ?? process.env.no_proxy ?? "";
3154
3321
  const entries = existing.split(",").map(s => s.trim()).filter(Boolean);
3155
3322
  const seen = new Set(entries.map(e => e.toLowerCase()));
3156
- for (const host of ["localhost", "127.0.0.1", "::1", "[::1]"]) {
3157
- if (!seen.has(host)) {
3323
+ // Configured entries first, then loopback: loopback is unconditional, so appending it last
3324
+ // keeps it present even when the operator lists a loopback host themselves.
3325
+ const raw = config.noProxy;
3326
+ const configured = (Array.isArray(raw) ? raw : (resolveEnvValue(raw) ?? "").split(","))
3327
+ .map(entry => entry.trim())
3328
+ .filter(Boolean);
3329
+ for (const host of [...configured, "localhost", "127.0.0.1", "::1", "[::1]"]) {
3330
+ const key = host.toLowerCase();
3331
+ if (!seen.has(key)) {
3158
3332
  entries.push(host);
3159
- seen.add(host);
3333
+ seen.add(key);
3160
3334
  }
3161
3335
  }
3162
3336
  process.env.NO_PROXY = entries.join(",");