@bitkyc08/opencodex 2.42.0 → 2.43.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 (246) hide show
  1. package/AGENTS_INSTALL.md +2 -2
  2. package/README.md +31 -0
  3. package/bin/ocx.mjs +10 -1
  4. package/gui/dist/assets/index-DS1NE4Jn.css +1 -0
  5. package/gui/dist/assets/index-Djowl68T.js +112 -0
  6. package/gui/dist/index.html +2 -2
  7. package/package.json +1 -1
  8. package/src/adapters/anthropic-image-codec.ts +304 -0
  9. package/src/adapters/anthropic-image-normalize.ts +8 -298
  10. package/src/adapters/anthropic.ts +30 -7
  11. package/src/adapters/command-code.ts +7 -5
  12. package/src/adapters/cursor/desktop-executor-contract.ts +15 -0
  13. package/src/adapters/cursor/images.ts +36 -6
  14. package/src/adapters/cursor/live-transport.ts +7 -2
  15. package/src/adapters/cursor/native-exec-desktop.ts +2 -15
  16. package/src/adapters/cursor/protobuf-request.ts +13 -3
  17. package/src/adapters/cursor/tool-definitions.ts +5 -670
  18. package/src/adapters/cursor/tool-guidance.ts +236 -0
  19. package/src/adapters/cursor/tool-naming.ts +252 -0
  20. package/src/adapters/cursor/tool-schemas.ts +195 -0
  21. package/src/adapters/cursor/types.ts +6 -3
  22. package/src/adapters/exec-tool-result-normalize.ts +1 -1
  23. package/src/adapters/google-errors.ts +9 -1
  24. package/src/adapters/google.ts +1 -0
  25. package/src/adapters/kiro-calibration.ts +181 -0
  26. package/src/adapters/kiro.ts +135 -3
  27. package/src/adapters/openai-responses.ts +216 -26
  28. package/src/adapters/responses-code-mode.ts +59 -0
  29. package/src/adapters/tool-catalog-nudge.ts +1 -1
  30. package/src/adapters/xai-schema-analysis.ts +86 -0
  31. package/src/adapters/xai-tool-schema.ts +2 -87
  32. package/src/adapters/xai-web-search.ts +1 -1
  33. package/src/bridge.ts +22 -10
  34. package/src/chat/inbound.ts +11 -3
  35. package/src/claude/inbound-content-options.ts +60 -0
  36. package/src/claude/inbound-model-options.ts +142 -0
  37. package/src/claude/inbound-records.ts +7 -0
  38. package/src/claude/inbound.ts +10 -202
  39. package/src/claude/model-info.ts +45 -0
  40. package/src/cli/account-auth.ts +21 -6
  41. package/src/cli/capabilities.ts +13 -2
  42. package/src/cli/claude.ts +232 -39
  43. package/src/cli/config-command.ts +9 -1
  44. package/src/cli/dispatch.ts +5 -1
  45. package/src/cli/doctor.ts +10 -0
  46. package/src/cli/effort.ts +372 -0
  47. package/src/cli/export-command.ts +3 -9
  48. package/src/cli/help.ts +1 -0
  49. package/src/cli/index.ts +13 -0
  50. package/src/cli/init.ts +4 -0
  51. package/src/cli/model-selection-guidance.ts +30 -0
  52. package/src/cli/models-runtime.ts +3 -2
  53. package/src/cli/models.ts +8 -3
  54. package/src/cli/opencode.ts +4 -1
  55. package/src/cli/provider-runtime.ts +65 -0
  56. package/src/cli/provider.ts +8 -0
  57. package/src/cli/registry.ts +16 -2
  58. package/src/cli/runtime-api.ts +3 -1
  59. package/src/cli/star-prompt.ts +22 -6
  60. package/src/cli/status-probes.ts +168 -0
  61. package/src/cli/status.ts +5 -168
  62. package/src/clients/config-export/constants.ts +69 -0
  63. package/src/clients/config-export/contracts.ts +154 -0
  64. package/src/clients/config-export/dsh.ts +132 -0
  65. package/src/clients/config-export/fast-models.ts +29 -0
  66. package/src/clients/config-export/mcode.ts +83 -0
  67. package/src/clients/config-export/model-metadata.ts +108 -0
  68. package/src/clients/config-export/omp.ts +104 -0
  69. package/src/clients/config-export/zcode.ts +92 -0
  70. package/src/clients/config-export.ts +18 -710
  71. package/src/codex/account-lifecycle.ts +20 -3
  72. package/src/codex/account-usability.ts +2 -0
  73. package/src/codex/auth-api.ts +170 -23
  74. package/src/codex/auth-context.ts +328 -24
  75. package/src/codex/catalog/effort.ts +30 -4
  76. package/src/codex/catalog/metadata.ts +39 -9
  77. package/src/codex/catalog/native-models.ts +53 -12
  78. package/src/codex/catalog/parsing.ts +119 -5
  79. package/src/codex/catalog/provider-fetch.ts +150 -20
  80. package/src/codex/catalog/reserve.ts +52 -0
  81. package/src/codex/catalog/sync.ts +89 -16
  82. package/src/codex/convergence-types.ts +1 -0
  83. package/src/codex/convergence.ts +2 -0
  84. package/src/codex/data/upstream-models.json +1 -1
  85. package/src/codex/forward-transport-headers.ts +25 -0
  86. package/src/codex/inject.ts +3 -28
  87. package/src/codex/legacy-config-keys.ts +68 -0
  88. package/src/codex/log-guard/inspect-schema.ts +137 -0
  89. package/src/codex/log-guard/inspect.ts +2 -134
  90. package/src/codex/loopback-target.ts +54 -0
  91. package/src/codex/main-account-cache.ts +63 -1
  92. package/src/codex/main-account-hard-lock.ts +52 -0
  93. package/src/codex/main-account.ts +3 -1
  94. package/src/codex/management-convergence.ts +3 -0
  95. package/src/codex/model-entitlements.ts +54 -4
  96. package/src/codex/project-config-warnings.ts +92 -2
  97. package/src/codex/prompt-layers/encoding.ts +80 -0
  98. package/src/codex/prompt-layers/paths.ts +54 -0
  99. package/src/codex/prompt-layers/revision.ts +55 -0
  100. package/src/codex/prompt-layers/toml-edit.ts +163 -0
  101. package/src/codex/prompt-layers/toml-read.ts +181 -0
  102. package/src/codex/prompt-layers.ts +14 -520
  103. package/src/codex/quota-auto-refresh-state.ts +16 -0
  104. package/src/codex/quota-auto-refresh.ts +219 -0
  105. package/src/codex/quota-types.ts +51 -0
  106. package/src/codex/quota.ts +252 -93
  107. package/src/codex/reserve-availability.ts +177 -0
  108. package/src/codex/routing.ts +28 -9
  109. package/src/codex/shim.ts +53 -11
  110. package/src/codex/subagent-model-fallback.ts +23 -3
  111. package/src/combos/failover.ts +125 -7
  112. package/src/combos/identifiers.ts +89 -0
  113. package/src/combos/index.ts +4 -0
  114. package/src/combos/resolve.ts +80 -9
  115. package/src/combos/types.ts +20 -93
  116. package/src/config/subagent-models.ts +24 -0
  117. package/src/config.ts +156 -13
  118. package/src/generated/compatibility-version.json +454 -166
  119. package/src/generated/model-metadata.ts +1 -1
  120. package/src/integrations/journal.ts +65 -4
  121. package/src/integrations/store.ts +5 -0
  122. package/src/lab/events/limits.ts +4 -0
  123. package/src/lib/destination-policy.ts +31 -2
  124. package/src/lib/errors.ts +39 -0
  125. package/src/lib/provider-outbound.ts +69 -3
  126. package/src/lib/proxy-env.ts +22 -0
  127. package/src/lib/redact-folding.ts +176 -0
  128. package/src/lib/redact.ts +2 -175
  129. package/src/lib/state-store-sweeper.ts +20 -6
  130. package/src/lib/token-estimate.ts +94 -27
  131. package/src/lib/windows-user-principal.ts +53 -5
  132. package/src/oauth/anthropic-routing.ts +99 -3
  133. package/src/oauth/generic-account-failover.ts +36 -13
  134. package/src/oauth/index.ts +127 -26
  135. package/src/oauth/login-cli.ts +5 -0
  136. package/src/oauth/meta-muse.ts +117 -15
  137. package/src/oauth/pool-settings-capability.ts +15 -4
  138. package/src/providers/api-keys.ts +8 -10
  139. package/src/providers/default-aliases.ts +39 -0
  140. package/src/providers/derive.ts +10 -2
  141. package/src/providers/fastwire.ts +36 -7
  142. package/src/providers/initial-model-selection-runtime.ts +90 -0
  143. package/src/providers/initial-model-selection.ts +120 -0
  144. package/src/providers/key-failover.ts +134 -54
  145. package/src/providers/key-store.ts +11 -1
  146. package/src/providers/label.ts +1 -1
  147. package/src/providers/model-discovery.ts +76 -0
  148. package/src/providers/model-rename-startup.ts +72 -8
  149. package/src/providers/openai-sidecar.ts +17 -5
  150. package/src/providers/openai-tiers-destination.ts +102 -0
  151. package/src/providers/openai-tiers.ts +2 -99
  152. package/src/providers/opencode-go-transport.ts +41 -0
  153. package/src/providers/quota-key-accounts.ts +141 -0
  154. package/src/providers/quota-types.ts +9 -0
  155. package/src/providers/quota.ts +535 -104
  156. package/src/providers/registry.ts +60 -17
  157. package/src/providers/xai-responses-opt-in.ts +31 -5
  158. package/src/quota/reset-activation.ts +81 -0
  159. package/src/quota/reset-detector.ts +305 -0
  160. package/src/quota/reset-notify-config.ts +162 -0
  161. package/src/quota/reset-observer.ts +125 -0
  162. package/src/quota/reset-poller.ts +160 -0
  163. package/src/quota/reset-seen-store.ts +385 -0
  164. package/src/quota/reset-sinks.ts +199 -0
  165. package/src/quota/window-mapping.ts +106 -0
  166. package/src/responses/apply-patch-envelope.ts +46 -0
  167. package/src/responses/code-mode-helper-compat.ts +39 -1
  168. package/src/responses/custom-tool-compat.ts +10 -4
  169. package/src/responses/hosted-tool-policy.ts +12 -4
  170. package/src/responses/parser-content.ts +133 -0
  171. package/src/responses/parser-text-format.ts +24 -0
  172. package/src/responses/parser-tools.ts +188 -0
  173. package/src/responses/parser.ts +3 -326
  174. package/src/responses/state.ts +124 -28
  175. package/src/router.ts +48 -13
  176. package/src/routing/analytics.ts +1 -0
  177. package/src/routing/capability.ts +17 -4
  178. package/src/server/auth-cors.ts +7 -1
  179. package/src/server/background-lifecycle.ts +23 -1
  180. package/src/server/chat-completions.ts +25 -3
  181. package/src/server/claude-messages.ts +62 -5
  182. package/src/server/effort-row.ts +1 -1
  183. package/src/server/fast-row.ts +295 -0
  184. package/src/server/gui-static.ts +30 -4
  185. package/src/server/index.ts +107 -21
  186. package/src/server/management/agent-settings-routes.ts +2 -2
  187. package/src/server/management/combo-routes.ts +37 -9
  188. package/src/server/management/config-routes.ts +93 -2
  189. package/src/server/management/integration-routes.ts +108 -0
  190. package/src/server/management/model-routes.ts +13 -3
  191. package/src/server/management/model-rows.ts +20 -1
  192. package/src/server/management/native-integration-routes.ts +4 -1
  193. package/src/server/management/oauth-account-routes.ts +38 -10
  194. package/src/server/management/provider-routes.ts +34 -3
  195. package/src/server/management/quota-reset-routes.ts +57 -0
  196. package/src/server/management/route-registry.ts +7 -4
  197. package/src/server/management/shared.ts +19 -5
  198. package/src/server/management/system-routes.ts +3 -2
  199. package/src/server/management-api.ts +14 -2
  200. package/src/server/ports.ts +12 -2
  201. package/src/server/relay-eager.ts +38 -23
  202. package/src/server/relay.ts +4 -0
  203. package/src/server/request-log.ts +6 -0
  204. package/src/server/responses/codex-ws-correlation.ts +65 -0
  205. package/src/server/responses/codex-ws-exchange.ts +261 -0
  206. package/src/server/responses/codex-ws-metadata.ts +134 -0
  207. package/src/server/responses/codex-ws-pool.ts +162 -0
  208. package/src/server/responses/codex-ws-request.ts +87 -0
  209. package/src/server/responses/codex-ws-session.ts +93 -0
  210. package/src/server/responses/codex-ws-wire.ts +144 -0
  211. package/src/server/responses/collaboration.ts +41 -1
  212. package/src/server/responses/compact.ts +105 -12
  213. package/src/server/responses/core.ts +476 -57
  214. package/src/server/responses/fetch-helpers.ts +10 -3
  215. package/src/server/responses/input-admission.ts +16 -9
  216. package/src/server/responses/responses-field-backfill.ts +1 -1
  217. package/src/server/responses/ws-upstream.ts +34 -318
  218. package/src/server/responses-custom-tool-repair.ts +20 -4
  219. package/src/server/responses-undeclared-tool-guard.ts +100 -8
  220. package/src/server/safe-response-headers.ts +23 -0
  221. package/src/server/search.ts +9 -0
  222. package/src/server/subagent-models-startup.ts +27 -0
  223. package/src/server/system-env-shell.ts +238 -0
  224. package/src/server/system-env.ts +7 -234
  225. package/src/server/ws-bridge.ts +3 -25
  226. package/src/server/xai-responses-startup.ts +21 -0
  227. package/src/service-manager-probe.ts +1 -1
  228. package/src/service.ts +55 -16
  229. package/src/types/config.ts +108 -12
  230. package/src/types/provider.ts +36 -7
  231. package/src/types/tools.ts +26 -1
  232. package/src/types.ts +2 -0
  233. package/src/update/notify.ts +8 -2
  234. package/src/usage/cost.ts +38 -28
  235. package/src/usage/expected-prices.ts +34 -15
  236. package/src/usage/log.ts +2 -0
  237. package/src/usage/model-identity.ts +26 -0
  238. package/src/usage/summary.ts +15 -1
  239. package/src/vision/describe.ts +6 -0
  240. package/src/vision/image-rewrite.ts +108 -0
  241. package/src/vision/index.ts +19 -306
  242. package/src/vision/plan.ts +205 -0
  243. package/src/web-search/executor.ts +6 -0
  244. package/src/web-search/index.ts +8 -1
  245. package/gui/dist/assets/index-BU1tE0sr.js +0 -112
  246. package/gui/dist/assets/index-DL9-iS6J.css +0 -1
@@ -1,245 +1,18 @@
1
1
  import { execFileSync } from "node:child_process";
2
- import { accessSync, constants, readFileSync, writeFileSync, unlinkSync, mkdirSync, statSync } from "node:fs";
3
- import { delimiter, join } from "node:path";
2
+ import { readFileSync, writeFileSync, unlinkSync, mkdirSync } from "node:fs";
3
+ import { join } from "node:path";
4
4
  import { getConfigDir } from "../config";
5
5
  import { resolveAutoContext, type AutoContextMode } from "../claude/context-windows";
6
- import { PROXY_MARKER, defaultAuthDetectDeps, detectClaudeAuth, ownAdmissionTokens, type AuthDetectDeps } from "../claude/auth-detect";
7
- import { resolveClaudeAuthMode } from "../claude/auth-mode";
8
- import { ANTHROPIC_PARENT_ENV_SLOTS, trustedNodeLauncherContext, type AnthropicParentEnvSlot } from "../cli/launcher-context";
6
+ import { PROXY_MARKER } from "../claude/auth-detect";
9
7
  import { isProxyAdmissionSecret } from "./auth-cors";
10
8
  import type { OcxConfig } from "../types";
11
9
  import { recordOwnedConfigPath } from "../lib/config-ownership";
12
10
  import { providerContextCap } from "../providers/context-cap";
13
11
  import { OPENAI_CODEX_PROVIDER_ID } from "../providers/openai-tiers";
14
-
15
- /**
16
- * Does the opencodex dummy marker belong in the system environment?
17
- *
18
- * Keyed on the SAME resolver `ocx claude` uses, so an auto config with no Claude auth
19
- * also reaches plain `claude` launches — before this, auto-absent users got nothing
20
- * from auto-connect and the feature looked broken for exactly the people it helps
21
- * (devlog 260726_claude_auth_auto/035).
22
- *
23
- * NOTE this is a SNAPSHOT: the file only changes when this runs (proxy start, `ocx
24
- * ensure`, or a settings save). `ocx claude` re-resolves live on every launch.
25
- */
26
- export type SystemEnvDeps = {
27
- /** Test seam; production uses the authenticated Node-launcher context. */
28
- preBunAnthropicSlots?: readonly AnthropicParentEnvSlot[] | null;
29
- /** Test seam for auth sources; `env` and `ownTokens` stay bound below. */
30
- authDetect?: Omit<Partial<AuthDetectDeps>, "env" | "ownTokens">;
31
- };
32
-
33
- /**
34
- * Bun may synthesize Anthropic variables from a project `.env` before this module runs.
35
- * Only values recorded by the plain-Node launcher are trusted as parent exports. Direct
36
- * Bun/service launches have no proof-bound slot list, so they fail closed and let the
37
- * file/keychain auth sources decide instead of allowing dotenv to select subscription mode.
38
- */
39
- function systemEnvAnthropicEnv(
40
- env: NodeJS.ProcessEnv,
41
- preBunAnthropicSlots: readonly AnthropicParentEnvSlot[] | null | undefined,
42
- ): NodeJS.ProcessEnv {
43
- const trustedSlots = preBunAnthropicSlots === undefined
44
- ? trustedNodeLauncherContext()?.anthropicEnvSlots ?? []
45
- : preBunAnthropicSlots ?? [];
46
- const exported = new Set<AnthropicParentEnvSlot>(trustedSlots);
47
- const sanitized = { ...env };
48
- for (const name of ANTHROPIC_PARENT_ENV_SLOTS) {
49
- if (sanitized[name] !== undefined && !exported.has(name)) delete sanitized[name];
50
- }
51
- return sanitized;
52
- }
53
-
54
- function systemEnvMarkerMode(config: OcxConfig, deps: SystemEnvDeps = {}): "proxy" | "subscription" {
55
- const env = systemEnvAnthropicEnv(process.env, deps.preBunAnthropicSlots);
56
- const ownTokens = ownAdmissionTokens(config);
57
- return resolveClaudeAuthMode(config, detectClaudeAuth({
58
- ...defaultAuthDetectDeps(env, ownTokens),
59
- ...(deps.authDetect ?? {}),
60
- env: () => env,
61
- ownTokens,
62
- })).markerMode;
63
- }
64
-
65
- // ---------------------------------------------------------------------------
66
- // Shell-hook env file: written on inject, sourced by the shell hook in .zshrc.
67
- // This works for ALL new shells immediately, unlike launchctl setenv which only
68
- // reaches processes launched directly by launchd (not Terminal.app children).
69
- // ---------------------------------------------------------------------------
70
-
71
- export function getShellEnvFilePath(): string {
72
- return join(getConfigDir(), "claude-env.sh");
73
- }
74
-
75
- function shellValue(value: string): string {
76
- return `'${value.replaceAll("'", `'\\''`)}'`;
77
- }
78
-
79
- function writeShellEnvFile(
80
- port: number,
81
- config: OcxConfig,
82
- modelEnv: Record<string, string> = {},
83
- auto?: AutoContextMode,
84
- deps: SystemEnvDeps = {},
85
- ): void {
86
- const lines = [
87
- `# Generated by opencodex — do not edit manually`,
88
- `export ANTHROPIC_BASE_URL=${shellValue(`http://127.0.0.1:${port}`)}`,
89
- `export CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY=${shellValue("1")}`,
90
- ];
91
- // New lever keys are CONDITIONAL exports (audit 139 R2#1): a value the user already
92
- // exported in their shell wins even though launchctl knows nothing about it.
93
- const conditional = (name: string, value: string) =>
94
- `[ -z "\${${name}+x}" ] && export ${name}=${shellValue(value)}`;
95
- if (systemEnvMarkerMode(config, deps) === "proxy") {
96
- if (config.apiKeys?.length) {
97
- lines.push(`export ANTHROPIC_AUTH_TOKEN=${shellValue(config.apiKeys[0].key)}`);
98
- } else {
99
- lines.push(conditional("ANTHROPIC_AUTH_TOKEN", PROXY_MARKER));
100
- }
101
- }
102
- // Model slots (default + tiers + legacy small-fast) with [1m] applied (devlog 260712 B2).
103
- if (modelEnv.ANTHROPIC_MODEL) {
104
- lines.push(`export ANTHROPIC_MODEL=${shellValue(modelEnv.ANTHROPIC_MODEL)}`);
105
- } else if (config.claudeCode?.model) {
106
- lines.push(`export ANTHROPIC_MODEL=${shellValue(config.claudeCode.model)}`);
107
- }
108
- for (const [name, value] of Object.entries(modelEnv)) {
109
- if (name === "ANTHROPIC_MODEL") continue;
110
- lines.push(conditional(name, value));
111
- }
112
- const maxCtx = config.claudeCode?.maxContextTokens;
113
- if (typeof maxCtx === "number" && Number.isFinite(maxCtx) && maxCtx > 0) {
114
- lines.push(conditional("CLAUDE_CODE_MAX_CONTEXT_TOKENS", String(Math.floor(maxCtx))));
115
- lines.push(conditional("DISABLE_COMPACT", "1"));
116
- }
117
- // Auto-context (devlog 260712 020): same contract as `ocx claude` / launchctl.
118
- const autoShell = auto ?? resolveAutoContext(config.claudeCode);
119
- if (autoShell.enabled) lines.push(conditional("CLAUDE_CODE_AUTO_COMPACT_WINDOW", String(autoShell.compactWindow)));
120
- if (config.claudeCode?.alwaysEnableEffort === true) {
121
- lines.push(conditional("CLAUDE_CODE_ALWAYS_ENABLE_EFFORT", "1"));
122
- }
123
- const shellEnvPath = getShellEnvFilePath();
124
- recordOwnedConfigPath(getConfigDir(), shellEnvPath);
125
- mkdirSync(getConfigDir(), { recursive: true, mode: 0o700 });
126
- writeFileSync(shellEnvPath, lines.join("\n") + "\n", { encoding: "utf8", mode: 0o600 });
127
- }
128
-
129
- function removeShellEnvFile(): void {
130
- try { unlinkSync(getShellEnvFilePath()); } catch { /* already gone */ }
131
- }
132
-
133
- // ---------------------------------------------------------------------------
134
- // .zshrc hook auto-install: adds a one-liner that sources claude-env.sh.
135
- // Idempotent — skips if the hook line already exists.
136
- // ---------------------------------------------------------------------------
137
-
138
- const SHELL_HOOK_MARKER = "# opencodex claude-env hook";
139
- const SHELL_HOOK_LINE = `${SHELL_HOOK_MARKER}\n[ -f ~/.opencodex/claude-env.sh ] && source ~/.opencodex/claude-env.sh`;
140
-
141
- export function installShellHook(): { installed: boolean; reason?: string } {
142
- if (process.platform !== "darwin") return { installed: false, reason: "not macOS" };
143
- const home = process.env.HOME;
144
- if (!home) return { installed: false, reason: "no HOME" };
145
- const zshrcPath = join(home, ".zshrc");
146
- try {
147
- let content = "";
148
- try { content = readFileSync(zshrcPath, "utf8"); } catch { /* file doesn't exist yet */ }
149
- if (content.includes(SHELL_HOOK_MARKER)) return { installed: false, reason: "already installed" };
150
- const addition = `\n${SHELL_HOOK_LINE}\n`;
151
- writeFileSync(zshrcPath, content + addition, { encoding: "utf8", mode: 0o644 });
152
- return { installed: true };
153
- } catch (err) {
154
- return { installed: false, reason: `write failed: ${err instanceof Error ? err.message : String(err)}` };
155
- }
156
- }
157
-
158
- export function uninstallShellHook(): { removed: boolean; reason?: string } {
159
- if (process.platform !== "darwin") return { removed: false, reason: "not macOS" };
160
- const home = process.env.HOME;
161
- if (!home) return { removed: false, reason: "no HOME" };
162
- const zshrcPath = join(home, ".zshrc");
163
- try {
164
- const content = readFileSync(zshrcPath, "utf8");
165
- if (!content.includes(SHELL_HOOK_MARKER)) return { removed: false, reason: "not installed" };
166
- // Match CR?LF, not LF alone. A .zshrc with CRLF line endings — ordinary on a home
167
- // directory an editor or another OS has touched — did not match, so the file was
168
- // rewritten unchanged and the caller was told the hook was removed. Reporting success
169
- // while the hook still sources on every new shell is the worse of the two failures.
170
- const cleaned = content.replace(/\r?\n?# opencodex claude-env hook\r?\n\[.*claude-env\.sh.*(?:\r?\n)?/g, "\n");
171
- // Verify instead of assuming: if the marker survives, the block is shaped in a way this
172
- // pattern does not own, and the honest answer is failure rather than a silent no-op.
173
- if (cleaned.includes(SHELL_HOOK_MARKER)) {
174
- return { removed: false, reason: "hook block present but not in the expected shape; remove it manually" };
175
- }
176
- writeFileSync(zshrcPath, cleaned, { encoding: "utf8", mode: 0o644 });
177
- return { removed: true };
178
- } catch (error) {
179
- if (error && typeof error === "object" && (error as { code?: unknown }).code === "ENOENT") {
180
- return { removed: false, reason: "not installed" };
181
- }
182
- return { removed: false, reason: "read/write failed" };
183
- }
184
- }
185
-
186
- /** Whether a real `claude` executable is discoverable from this process's PATH. */
187
- export function claudeCodeCliInstalled(pathValue = process.env.PATH): boolean {
188
- if (!pathValue) return false;
189
- for (const directory of pathValue.split(delimiter)) {
190
- // An empty PATH segment means the current directory. Do not let the proxy treat a
191
- // workspace-local file as a durable user installation.
192
- if (!directory) continue;
193
- const candidate = join(directory, "claude");
194
- try {
195
- if (!statSync(candidate).isFile()) continue;
196
- accessSync(candidate, constants.X_OK);
197
- return true;
198
- } catch {
199
- // Keep scanning PATH after missing, non-file, and non-executable entries.
200
- }
201
- }
202
- return false;
203
- }
204
-
205
- /**
206
- * Keep the shell hook aligned with the integration that can actually consume it.
207
- * Claude Desktop uses its own profile and does not source `.zshrc`; this hook exists
208
- * only for plain Claude Code CLI launches.
209
- *
210
- * Reconciliation is PATH-sensitive by construction: "Claude Code is installed" is answered
211
- * from the PATH of whichever process calls this. A launchd/service context with a stripped
212
- * PATH can therefore fail to see a `claude` the user's interactive shell finds, and this will
213
- * remove the hook. That is the intended failure direction — removing an OpenCodex-owned block
214
- * is reversible on the next foreground `ocx start`, whereas leaving a hook pointing at an
215
- * uninstalled CLI is the stale state this reconciliation exists to clear. Only the block
216
- * carrying our own marker is ever touched; user lines are preserved.
217
- */
218
- export function reconcileShellHook(systemEnvInjected: boolean): {
219
- changed: boolean;
220
- state: "installed" | "absent" | "failed";
221
- reason?: string;
222
- } {
223
- if (process.platform !== "darwin") return { changed: false, state: "absent", reason: "not macOS" };
224
- if (systemEnvInjected && claudeCodeCliInstalled()) {
225
- const result = installShellHook();
226
- if (result.installed) return { changed: true, state: "installed" };
227
- if (result.reason === "already installed") {
228
- return { changed: false, state: "installed", reason: result.reason };
229
- }
230
- return { changed: false, state: "failed", reason: result.reason ?? "install failed" };
231
- }
232
-
233
- const result = uninstallShellHook();
234
- if (!result.removed && result.reason !== "not installed") {
235
- return { changed: false, state: "failed", reason: result.reason ?? "remove failed" };
236
- }
237
- return {
238
- changed: result.removed,
239
- state: "absent",
240
- reason: systemEnvInjected ? "Claude Code not installed" : "system environment inactive",
241
- };
242
- }
12
+ export { getShellEnvFilePath, installShellHook, uninstallShellHook, claudeCodeCliInstalled, reconcileShellHook } from "./system-env-shell";
13
+ export type { SystemEnvDeps } from "./system-env-shell";
14
+ import { systemEnvMarkerMode, writeShellEnvFile, removeShellEnvFile } from "./system-env-shell";
15
+ import type { SystemEnvDeps } from "./system-env-shell";
243
16
 
244
17
  const SYSTEM_ENV_NAMES = [
245
18
  "ANTHROPIC_BASE_URL",
@@ -7,19 +7,13 @@ import type { ResponsesTerminalStatus } from "../bridge";
7
7
  import type { DataPlaneAdmission } from "./auth-cors";
8
8
  import type { AdmissionLease, AdmissionReservation } from "../lib/admission";
9
9
  import { BoundedSseFrameBuffer } from "./sse-frame-buffer";
10
+ import { safeResponseHeaders } from "./safe-response-headers";
11
+
12
+ export { safeResponseHeaders } from "./safe-response-headers";
10
13
 
11
14
  const OPEN = 1;
12
15
  type ResponsesTerminalReporter = (status: ResponsesTerminalStatus) => void;
13
16
  type ResponsesPayloadObserver = (payload: string) => void;
14
- const SAFE_RESPONSE_HEADER_EXACT = new Set([
15
- "retry-after",
16
- "x-request-id",
17
- "openai-request-id",
18
- "x-codex-turn-state",
19
- "openai-model",
20
- "x-models-etag",
21
- "x-reasoning-included",
22
- ]);
23
17
 
24
18
  export interface WsData {
25
19
  headers?: Headers; // base inbound forward headers only; per-turn auth refresh injects current pool tokens
@@ -104,22 +98,6 @@ export function selectForwardHeadersForAuthContext(headers: Headers, ctx: CodexA
104
98
  return headersForCodexAuthContext(headers, ctx);
105
99
  }
106
100
 
107
- export function safeResponseHeaders(headers: Headers): Record<string, string> {
108
- const out: Record<string, string> = {};
109
- for (const [name, value] of headers) {
110
- const lower = name.toLowerCase();
111
- if (
112
- SAFE_RESPONSE_HEADER_EXACT.has(lower) ||
113
- lower.startsWith("x-ratelimit-") ||
114
- /^x-codex(?:-[a-z0-9-]+)?-(primary|secondary|tertiary)-(used-percent|window-minutes|reset-at)$/.test(lower) ||
115
- /^x-codex(?:-[a-z0-9-]+)?-limit-name$/.test(lower)
116
- ) {
117
- out[lower] = value;
118
- }
119
- }
120
- return out;
121
- }
122
-
123
101
  export function buildWarmupCompletionFrames(frame: Record<string, unknown>): string[] {
124
102
  const createdAt = Math.floor(Date.now() / 1000);
125
103
  const baseResponse: Record<string, unknown> = {
@@ -0,0 +1,21 @@
1
+ import { mutatePersistedConfig } from "../config";
2
+ import { migrateXaiResponsesDefault } from "../providers/xai-responses-opt-in";
3
+ import type { OcxConfig } from "../types";
4
+
5
+ /** Rebase the one-time wire upgrade before initializing any live config consumers. */
6
+ export function migrateStartupXaiResponses(config: OcxConfig): OcxConfig {
7
+ const projection = { ...config };
8
+ if (!migrateXaiResponsesDefault(projection)) return config;
9
+ try {
10
+ const outcome = mutatePersistedConfig(fresh => ({
11
+ changed: migrateXaiResponsesDefault(fresh),
12
+ value: fresh,
13
+ }));
14
+ if (outcome.status !== "unavailable") return outcome.value;
15
+ console.warn(`[xai-responses-migration] Persistence unavailable (${outcome.reason}); using Responses in memory only.`);
16
+ } catch {
17
+ // Filesystem errors can carry private paths. Startup must still remain available.
18
+ console.warn("[xai-responses-migration] Persistence failed; using Responses in memory only.");
19
+ }
20
+ return projection;
21
+ }
@@ -619,7 +619,7 @@ function windowsTaskListContains(body: string, taskName: string): boolean {
619
619
  * denied", so a locked-down host answers the control and the real query
620
620
  * identically, and comparing them yields a false `absent` — the one direction
621
621
  * that lets an unattended write proceed into a home another process owns.
622
- * `tests/codex-service-manager-probe.test.ts` covers exactly that host.
622
+ * `tests/codex-integration/codex-service-manager-probe.test.ts` covers exactly that host.
623
623
  */
624
624
  const SCHTASKS_TASK_NOT_FOUND_EN = /cannot find the file specified/i;
625
625
 
package/src/service.ts CHANGED
@@ -205,9 +205,9 @@ export interface ServiceInstallState {
205
205
  bunPath?: string;
206
206
  cliPath?: string;
207
207
  /**
208
- * Linux only. The stable `ocx` launcher the unit actually invokes, when one was found.
209
- * Present means `bunPath`/`cliPath` are provenance for the install, NOT what systemd
210
- * runs — so staleness must be judged against THIS path instead. A version-manager
208
+ * launchd and systemd. The stable `ocx` launcher the service definition actually invokes,
209
+ * when one was found. Present means `bunPath`/`cliPath` are provenance for the install,
210
+ * NOT what the service runs — so staleness must be judged against THIS path instead. A version-manager
211
211
  * upgrade replaces the directory those two point into while the launcher survives, and
212
212
  * checking the old pair would report a stale service that is in fact healthy.
213
213
  */
@@ -486,8 +486,21 @@ function writeServiceApiTokenFile(): string | null {
486
486
  return path;
487
487
  }
488
488
 
489
- export function buildPlist(proxyEnv: { name: string; value: string }[] = resolvedProxyEnv()): string {
490
- const { bun, bunRuntimeSource, cli } = cliEntry();
489
+ /**
490
+ * Render the launchd plist. Mirrors `buildUnit`: when `deps.launcher` names a stable `ocx`
491
+ * executable, the job execs that launcher instead of the package-local Bun + CLI pair, so a
492
+ * version-manager upgrade (mise, asdf, nvm) that replaces the package directory is picked up
493
+ * on the next launchd start instead of leaving the old build serving (#3464 — the macOS
494
+ * counterpart of #2898). Discovery belongs to `installLaunchd()`; the default here is the
495
+ * legacy pair so callers and tests stay hermetic.
496
+ */
497
+ export function buildPlist(
498
+ proxyEnv: { name: string; value: string }[] = resolvedProxyEnv(),
499
+ deps: { launcher?: string | null; runtime?: DurableBunRuntime } = {},
500
+ ): string {
501
+ const runtime = deps.runtime ?? durableBunRuntime();
502
+ const { bun, bunRuntimeSource, cli } = cliEntry(runtime);
503
+ const launcher = deps.launcher ?? null;
491
504
  const log = logPath();
492
505
  const path = process.env.PATH ?? "/usr/local/bin:/usr/bin:/bin";
493
506
  const codexHome = process.env.CODEX_HOME?.trim();
@@ -495,8 +508,16 @@ export function buildPlist(proxyEnv: { name: string; value: string }[] = resolve
495
508
  const opencodexHome = process.env.OPENCODEX_HOME?.trim();
496
509
  const envLines = [
497
510
  ` <key>OCX_SERVICE</key><string>1</string>`,
498
- ` <key>${BUN_RUNTIME_SOURCE_ENV}</key><string>${bunRuntimeSource}</string>`,
499
- ` <key>${BUN_RUNTIME_PATH_ENV}</key><string>${plistString(bun)}</string>`,
511
+ ...(launcher ? [] : [
512
+ ` <key>${BUN_RUNTIME_SOURCE_ENV}</key><string>${bunRuntimeSource}</string>`,
513
+ ` <key>${BUN_RUNTIME_PATH_ENV}</key><string>${plistString(bun)}</string>`,
514
+ ]),
515
+ // A launcher resolves the current package's bundled Bun after every upgrade. Preserve
516
+ // only a proof-bound shell override; baking a package-local path here would recreate
517
+ // the version-manager pin that launcher mode exists to remove (same rule as buildUnit).
518
+ launcher && runtime.source === "override"
519
+ ? ` <key>${runtime.overrideEnv}</key><string>${plistString(runtime.path)}</string>`
520
+ : null,
500
521
  ` <key>PATH</key><string>${plistString(path)}</string>`,
501
522
  codexHome ? ` <key>CODEX_HOME</key><string>${plistString(codexHome)}</string>` : null,
502
523
  codexSqliteHome ? ` <key>CODEX_SQLITE_HOME</key><string>${plistString(codexSqliteHome)}</string>` : null,
@@ -504,7 +525,9 @@ export function buildPlist(proxyEnv: { name: string; value: string }[] = resolve
504
525
  ...proxyEnv.map(({ name, value }) =>
505
526
  ` <key>${name}</key><string>${plistString(value)}</string>`),
506
527
  ].filter((line): line is string => Boolean(line)).join("\n");
507
- const command = buildServiceShellCommand(bun, cli);
528
+ const command = launcher
529
+ ? buildServiceLauncherShellCommand(launcher)
530
+ : buildServiceShellCommand(bun, cli);
508
531
  return `<?xml version="1.0" encoding="UTF-8"?>
509
532
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
510
533
  <plist version="1.0">
@@ -569,6 +592,23 @@ function buildServiceLauncherShellCommand(launcher: string, port = resolveServic
569
592
  return `if [ -f ${shellQuote(tokenFile)} ]; then OPENCODEX_API_AUTH_TOKEN="$(cat ${shellQuote(tokenFile)})"; export OPENCODEX_API_AUTH_TOKEN; fi; exec ${shellQuote(launcher)} start --port ${port}`;
570
593
  }
571
594
 
595
+ /**
596
+ * The exec line the installed launchd plist is expected to carry, derived from the recorded
597
+ * install state rather than rediscovered: a launcher install runs the launcher, a legacy or
598
+ * stateless install runs the Bun + CLI pair. `start` and `status` compare the live job
599
+ * against this, so both must follow the launcher or a healthy launcher-backed job reads as
600
+ * "an OLDER plist" (#3464). PATH is deliberately NOT re-walked here.
601
+ */
602
+ export function expectedLaunchdCommand(
603
+ port: number,
604
+ deps: { state?: ServiceInstallState | null; entry?: { bun: string; cli: string } } = {},
605
+ ): string {
606
+ const state = deps.state === undefined ? readServiceInstallState() : deps.state;
607
+ if (state?.launcherPath) return buildServiceLauncherShellCommand(state.launcherPath, port);
608
+ const entry = deps.entry ?? cliEntry();
609
+ return buildServiceShellCommand(entry.bun, entry.cli, port);
610
+ }
611
+
572
612
  /**
573
613
  * The `--port <n>` actually baked into the installed launchd plist, or null when it
574
614
  * cannot be read. macOS only — named for launchd rather than "service" so no caller
@@ -2251,7 +2291,10 @@ function installLaunchd(): void {
2251
2291
  // Capture this BEFORE writing: the write below makes the plist exist unconditionally,
2252
2292
  // so a post-write existsSync would call every fresh install an "installed" service.
2253
2293
  const wasInstalled = existsSync(p);
2254
- writeServiceDefinitionFile(p, buildPlist(), "utf8");
2294
+ // Resolve the launcher ONCE and hand the same value to the plist and to install state,
2295
+ // so the staleness diagnostic judges exactly what launchd runs.
2296
+ const launcher = stableLauncherEntry();
2297
+ writeServiceDefinitionFile(p, buildPlist(resolvedProxyEnv(), { launcher }), "utf8");
2255
2298
  // Best-effort: an absent job is fine here, and a failed unload is caught by the
2256
2299
  // load verification below with a better message than a raw unload error.
2257
2300
  runLaunchctl(["unload", p]);
@@ -2268,7 +2311,7 @@ function installLaunchd(): void {
2268
2311
  + `then re-run '${wasInstalled ? "ocx service repair" : "ocx service install"}'.`,
2269
2312
  );
2270
2313
  }
2271
- writeServiceInstallState();
2314
+ writeServiceInstallState("scheduler", launcher);
2272
2315
  }
2273
2316
  /**
2274
2317
  * Deps are named for the layer they replace, not for the process API: `launchctl`
@@ -2291,9 +2334,8 @@ export function startLaunchd(deps: {
2291
2334
  // already be bootstrapped from THIS plist, which is a no-op rather than an error.
2292
2335
  // `install` can assume a stale job (it just rewrote the plist); `start` cannot, and
2293
2336
  // throwing here would break `ocx service start` on every healthy service.
2294
- const entry = cliEntry();
2295
2337
  const live = (deps.matches ?? launchdJobMatchesPlist)(
2296
- buildServiceShellCommand(entry.bun, entry.cli),
2338
+ expectedLaunchdCommand(installedServiceListenPort()),
2297
2339
  );
2298
2340
  if (live.loaded && live.matchesPlist) {
2299
2341
  console.log("ℹ️ service was already loaded from the current plist; nothing to do.");
@@ -4234,14 +4276,11 @@ export async function serviceStatusReport(
4234
4276
  // Linux/Windows and make the stale-plist case untestable there.
4235
4277
  const stalePlist = deps.matchesPlist?.() ?? (process.platform === "darwin"
4236
4278
  ? (() => {
4237
- const entry = cliEntry();
4238
4279
  // Pass the INSTALLED port explicitly: the default third argument is
4239
4280
  // resolveServiceListenPort(), which reads OCX_BAKE_PORT/config.port, so after
4240
4281
  // a config edit the expected string would never match and every run would
4241
4282
  // print a false "OLDER plist".
4242
- return launchdJobMatchesPlist(
4243
- buildServiceShellCommand(entry.bun, entry.cli, installedServiceListenPort()),
4244
- );
4283
+ return launchdJobMatchesPlist(expectedLaunchdCommand(installedServiceListenPort()));
4245
4284
  })()
4246
4285
  : null);
4247
4286
  const staleLine = stalePlist && stalePlist.loaded && !stalePlist.matchesPlist
@@ -375,6 +375,27 @@ export interface OcxConfig {
375
375
  * from Cursor's built-in effort table. Omitted/false preserves discovery output.
376
376
  */
377
377
  cursorEffortRows?: boolean;
378
+ /**
379
+ * Default-on synthetic Fast selectors. The raw OpenAI-style `/v1/models` list and
380
+ * Claude Code discovery add a `<base-id>--fast` row for every model whose resolved Fast
381
+ * policy is eligible, and selecting one routes the base model with the canonical
382
+ * `priority` service tier. Client config exports include the same selectors. Set false
383
+ * to disable them; omission enables them.
384
+ */
385
+ fastRows?: boolean;
386
+ /**
387
+ * Opt-in Ultra Fast service tier, default off.
388
+ *
389
+ * This does NOT synthesize an `ultrafast` row: `src/codex/data/upstream-models.json`
390
+ * advertises only `priority`, and PR #2994 was closed precisely because a catalog row
391
+ * the wire cannot honor is a picker entry that lies. What the flag turns on is honesty
392
+ * about a tier the operator supplies themselves — the catalog stops stripping an
393
+ * `ultrafast` the user configured, and the request path names it instead of recording
394
+ * "no fast tier was requested".
395
+ */
396
+ ultraFastTier?: boolean;
397
+ /** Stop new identity-matched main-account requests at observed 99% usage. Default off. */
398
+ codexMainAccountHardLock?: boolean;
378
399
  /** Explicit top-level deletion intent used by stale whole-config rebases. */
379
400
  configRebaseProvenance?: OcxConfigRebaseProvenance | Record<string, unknown>;
380
401
  /** OpenAI provider-contract migration marker (v2 = single `openai` provider with account mode). */
@@ -395,6 +416,8 @@ export interface OcxConfig {
395
416
  * into a selector-qualified group; Codex still advertises only the first 5 visible rows.
396
417
  */
397
418
  subagentModels?: string[];
419
+ /** One-time featured-roster upgrade marker; later user ordering is preserved. */
420
+ subagentModelsVersion?: number;
398
421
  /**
399
422
  * Optional full picker ordering for the Codex model catalog, independent of the
400
423
  * 5-slot `subagentModels` spawn_agent cap. DISPLAY-ONLY: it controls the visual order of
@@ -573,6 +596,13 @@ export interface OcxConfig {
573
596
  /** Maximum in-memory ciphertext-to-assignment entries. Default: 200. */
574
597
  cacheEntries?: number;
575
598
  };
599
+ /**
600
+ * Quota-reset detection and notification. Absent means off: no detection, no timer, no sink.
601
+ *
602
+ * Not in `getDefaultConfig()` on purpose — that function carries no optional-feature keys,
603
+ * so absence is the only default state this feature has.
604
+ */
605
+ quotaResetNotify?: OcxQuotaResetNotifyConfig;
576
606
  /** Provider-level Codex-visible context caps. Values only lower known model context windows. */
577
607
  providerContextCaps?: Record<string, number>;
578
608
  /** Global Codex-visible context cap value (tokens). Falls back to DEFAULT_PROVIDER_CONTEXT_CAP. */
@@ -675,6 +705,14 @@ export interface OcxConfig {
675
705
  codexAccounts?: CodexAccount[];
676
706
  /** Account ids administratively excluded from future pool selection until resumed. */
677
707
  pausedCodexAccountIds?: string[];
708
+ /** Opt-in per-account activation of newly reset Codex quota windows. */
709
+ codexQuotaAutoRefresh?: Record<string, {
710
+ fiveHour?: boolean;
711
+ weekly?: boolean;
712
+ /** Upstream reset timestamps already activated, retained across restarts. */
713
+ lastFiveHourResetAt?: number;
714
+ lastWeeklyResetAt?: number;
715
+ }>;
678
716
  /**
679
717
  * Selection order per account id, higher used earlier; absent = 0. Keyed by id
680
718
  * rather than stored on `codexAccounts` rows so the Desktop login (`__main__`),
@@ -742,9 +780,14 @@ export interface OcxConfig {
742
780
  */
743
781
  maxUpstreamBodyBytes?: number;
744
782
  /**
745
- * Opt-in Anthropic OAuth account pool (#294). Default OFF.
746
- * Failover on 429 + sticky affinity; new sessions may pick lowest known 5h usage.
783
+ * Opt-in Anthropic OAuth PROACTIVE routing (#294). Default OFF.
784
+ * Sticky session affinity; new sessions may pick lowest known 5h usage.
747
785
  * Experimental — see docs and GUI warning before enabling.
786
+ *
787
+ * Reactive 429 failover is NOT gated here. It activates on account presence, like every
788
+ * other multi-credential provider, and cannot be switched off: rotating away from an account
789
+ * upstream has just rate-limited only ever runs after a refusal, so stranding it while a
790
+ * second logged-in account sits idle is a defect rather than a configuration choice.
748
791
  */
749
792
  anthropicAccountPool?: {
750
793
  enabled?: boolean;
@@ -758,17 +801,17 @@ export interface OcxConfig {
758
801
  quotaWindow?: OcxAccountPoolQuotaWindow;
759
802
  };
760
803
  /**
761
- * Generic OAuth multi-account 429 failover (#2568). Presence-driven by default.
804
+ * Generic OAuth multi-account PROACTIVE account preference (#2568, #695).
762
805
  *
763
- * Rotates to another logged-in account of the SAME provider when one is rate-limited, for
764
- * OAuth providers that have no pool of their own xAI, Cursor, Kimi, GitHub Copilot,
765
- * Antigravity, Nous. The Codex pool and the Anthropic pool own their own rotation and are
766
- * excluded; this setting changes neither.
806
+ * Reactive 429 rotation — moving to another logged-in account of the SAME provider when one
807
+ * is rate-limited is presence-driven and NOT configurable here. It activates whenever a
808
+ * provider has 2 or more eligible stored accounts, the same consent rule an `apiKeyPool` of
809
+ * two keys already applies, and a single account remains a strict no-op.
767
810
  *
768
- * With the key absent, rotation activates when a provider has 2 or more eligible stored
769
- * accounts the same consent rule API-key pools already apply to a 2+ key pool (#2568d). A
770
- * single account is a strict no-op. Set `false` to keep strict single-account behaviour;
771
- * `providers.<name>.oauthAccountFailover` overrides this per provider.
811
+ * What `enabled: false` still refuses is the PRE-DISPATCH preference: steering a request
812
+ * upstream has not refused toward the account with more known headroom. That moves a healthy
813
+ * request, so it stays a real choice. `providers.<name>.oauthAccountFailover` overrides this
814
+ * per provider in either direction; reactive 429 rotation remains presence-driven.
772
815
  */
773
816
  oauthAccountFailover?: {
774
817
  enabled?: boolean;
@@ -820,6 +863,14 @@ export interface OcxComboConfig {
820
863
  strategy?: OcxComboStrategy;
821
864
  /** Successful requests retained on one RR selection batch. Default 1; range 1..100. */
822
865
  stickyLimit?: number;
866
+ /**
867
+ * Optional per-target cooldown used only when the upstream response has no Retry-After or Codex reset signal.
868
+ * Unset uses the upstream fallback (5 s for request-rate 429 codes 1302/1305, otherwise 60 s);
869
+ * an explicit value overrides that fallback. Range 1..600000.
870
+ */
871
+ cooldownMs?: number;
872
+ /** Maximum wait for an eligible target cooldown to expire before failing closed. Default 0; range 0..600000, per selection attempt. */
873
+ waitForCooldownMs?: number;
823
874
  /** Used when the client omits reasoning.effort. null/omitted leaves the target default unchanged. */
824
875
  defaultEffort?: OcxComboDefaultEffort | null;
825
876
  /**
@@ -1062,6 +1113,51 @@ export interface OcxWebSearchSidecarConfig {
1062
1113
  * stays atomic. Tradeoff: text the model emits BEFORE deciding to search — which buffered mode
1063
1114
  * silently drops — becomes visible to the client and may partially repeat in the post-search
1064
1115
  * answer. Default: false (buffered, previous behavior).
1065
- */
1116
+ */
1066
1117
  streamRoutedModelOutput?: boolean;
1067
1118
  }
1119
+
1120
+ /**
1121
+ * Quota-reset notification settings.
1122
+ *
1123
+ * Every field is optional and the whole section defaults to off. `enabled: true` alone is not
1124
+ * sufficient: without a webhook or a command there is nowhere to deliver, and treating that as
1125
+ * off is what keeps the "a default install runs no detection code" guarantee true rather than
1126
+ * nearly true.
1127
+ */
1128
+ export interface OcxQuotaResetNotifyConfig {
1129
+ /** Master switch. Default false — nothing detects, nothing polls, nothing fires. */
1130
+ enabled?: boolean;
1131
+ /** Which reset kinds to deliver. Default: both. */
1132
+ kinds?: Array<"scheduled" | "surprise">;
1133
+ /**
1134
+ * Idle poll interval in seconds. Default 900, floor 60, and 0 disables polling entirely.
1135
+ *
1136
+ * Polling exists because the interesting case is a reset that happens while no request is in
1137
+ * flight: without a poll, a window that reset overnight is only noticed on the next request.
1138
+ */
1139
+ pollSeconds?: number;
1140
+ /**
1141
+ * POST the event as JSON here.
1142
+ *
1143
+ * Treated as a credential: for Slack and Discord the URL itself is the authorization, so it
1144
+ * is redacted by `ocx config show` and excluded from `config export`.
1145
+ */
1146
+ webhookUrl?: string;
1147
+ /**
1148
+ * Permit a loopback or private-network webhook target. Default false.
1149
+ *
1150
+ * An operator-supplied URL is an SSRF surface, so the default refuses anything that resolves
1151
+ * private. Self-hosted receivers are the legitimate case for opting in.
1152
+ */
1153
+ allowPrivateNetwork?: boolean;
1154
+ /** Webhook timeout in milliseconds. Default 5000. */
1155
+ timeoutMs?: number;
1156
+ /**
1157
+ * Run a local command with the event JSON on stdin.
1158
+ *
1159
+ * An argv array, never a shell string: the command is spawned directly, so an operator value
1160
+ * cannot become a shell-injection surface.
1161
+ */
1162
+ command?: string[];
1163
+ }