@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.
- package/AGENTS_INSTALL.md +2 -2
- package/README.md +31 -0
- package/bin/ocx.mjs +10 -1
- package/gui/dist/assets/index-DS1NE4Jn.css +1 -0
- package/gui/dist/assets/index-Djowl68T.js +112 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic-image-codec.ts +304 -0
- package/src/adapters/anthropic-image-normalize.ts +8 -298
- package/src/adapters/anthropic.ts +30 -7
- package/src/adapters/command-code.ts +7 -5
- package/src/adapters/cursor/desktop-executor-contract.ts +15 -0
- package/src/adapters/cursor/images.ts +36 -6
- package/src/adapters/cursor/live-transport.ts +7 -2
- package/src/adapters/cursor/native-exec-desktop.ts +2 -15
- package/src/adapters/cursor/protobuf-request.ts +13 -3
- package/src/adapters/cursor/tool-definitions.ts +5 -670
- package/src/adapters/cursor/tool-guidance.ts +236 -0
- package/src/adapters/cursor/tool-naming.ts +252 -0
- package/src/adapters/cursor/tool-schemas.ts +195 -0
- package/src/adapters/cursor/types.ts +6 -3
- package/src/adapters/exec-tool-result-normalize.ts +1 -1
- package/src/adapters/google-errors.ts +9 -1
- package/src/adapters/google.ts +1 -0
- package/src/adapters/kiro-calibration.ts +181 -0
- package/src/adapters/kiro.ts +135 -3
- package/src/adapters/openai-responses.ts +216 -26
- package/src/adapters/responses-code-mode.ts +59 -0
- package/src/adapters/tool-catalog-nudge.ts +1 -1
- package/src/adapters/xai-schema-analysis.ts +86 -0
- package/src/adapters/xai-tool-schema.ts +2 -87
- package/src/adapters/xai-web-search.ts +1 -1
- package/src/bridge.ts +22 -10
- package/src/chat/inbound.ts +11 -3
- package/src/claude/inbound-content-options.ts +60 -0
- package/src/claude/inbound-model-options.ts +142 -0
- package/src/claude/inbound-records.ts +7 -0
- package/src/claude/inbound.ts +10 -202
- package/src/claude/model-info.ts +45 -0
- package/src/cli/account-auth.ts +21 -6
- package/src/cli/capabilities.ts +13 -2
- package/src/cli/claude.ts +232 -39
- package/src/cli/config-command.ts +9 -1
- package/src/cli/dispatch.ts +5 -1
- package/src/cli/doctor.ts +10 -0
- package/src/cli/effort.ts +372 -0
- package/src/cli/export-command.ts +3 -9
- package/src/cli/help.ts +1 -0
- package/src/cli/index.ts +13 -0
- package/src/cli/init.ts +4 -0
- package/src/cli/model-selection-guidance.ts +30 -0
- package/src/cli/models-runtime.ts +3 -2
- package/src/cli/models.ts +8 -3
- package/src/cli/opencode.ts +4 -1
- package/src/cli/provider-runtime.ts +65 -0
- package/src/cli/provider.ts +8 -0
- package/src/cli/registry.ts +16 -2
- package/src/cli/runtime-api.ts +3 -1
- package/src/cli/star-prompt.ts +22 -6
- package/src/cli/status-probes.ts +168 -0
- package/src/cli/status.ts +5 -168
- package/src/clients/config-export/constants.ts +69 -0
- package/src/clients/config-export/contracts.ts +154 -0
- package/src/clients/config-export/dsh.ts +132 -0
- package/src/clients/config-export/fast-models.ts +29 -0
- package/src/clients/config-export/mcode.ts +83 -0
- package/src/clients/config-export/model-metadata.ts +108 -0
- package/src/clients/config-export/omp.ts +104 -0
- package/src/clients/config-export/zcode.ts +92 -0
- package/src/clients/config-export.ts +18 -710
- package/src/codex/account-lifecycle.ts +20 -3
- package/src/codex/account-usability.ts +2 -0
- package/src/codex/auth-api.ts +170 -23
- package/src/codex/auth-context.ts +328 -24
- package/src/codex/catalog/effort.ts +30 -4
- package/src/codex/catalog/metadata.ts +39 -9
- package/src/codex/catalog/native-models.ts +53 -12
- package/src/codex/catalog/parsing.ts +119 -5
- package/src/codex/catalog/provider-fetch.ts +150 -20
- package/src/codex/catalog/reserve.ts +52 -0
- package/src/codex/catalog/sync.ts +89 -16
- package/src/codex/convergence-types.ts +1 -0
- package/src/codex/convergence.ts +2 -0
- package/src/codex/data/upstream-models.json +1 -1
- package/src/codex/forward-transport-headers.ts +25 -0
- package/src/codex/inject.ts +3 -28
- package/src/codex/legacy-config-keys.ts +68 -0
- package/src/codex/log-guard/inspect-schema.ts +137 -0
- package/src/codex/log-guard/inspect.ts +2 -134
- package/src/codex/loopback-target.ts +54 -0
- package/src/codex/main-account-cache.ts +63 -1
- package/src/codex/main-account-hard-lock.ts +52 -0
- package/src/codex/main-account.ts +3 -1
- package/src/codex/management-convergence.ts +3 -0
- package/src/codex/model-entitlements.ts +54 -4
- package/src/codex/project-config-warnings.ts +92 -2
- package/src/codex/prompt-layers/encoding.ts +80 -0
- package/src/codex/prompt-layers/paths.ts +54 -0
- package/src/codex/prompt-layers/revision.ts +55 -0
- package/src/codex/prompt-layers/toml-edit.ts +163 -0
- package/src/codex/prompt-layers/toml-read.ts +181 -0
- package/src/codex/prompt-layers.ts +14 -520
- package/src/codex/quota-auto-refresh-state.ts +16 -0
- package/src/codex/quota-auto-refresh.ts +219 -0
- package/src/codex/quota-types.ts +51 -0
- package/src/codex/quota.ts +252 -93
- package/src/codex/reserve-availability.ts +177 -0
- package/src/codex/routing.ts +28 -9
- package/src/codex/shim.ts +53 -11
- package/src/codex/subagent-model-fallback.ts +23 -3
- package/src/combos/failover.ts +125 -7
- package/src/combos/identifiers.ts +89 -0
- package/src/combos/index.ts +4 -0
- package/src/combos/resolve.ts +80 -9
- package/src/combos/types.ts +20 -93
- package/src/config/subagent-models.ts +24 -0
- package/src/config.ts +156 -13
- package/src/generated/compatibility-version.json +454 -166
- package/src/generated/model-metadata.ts +1 -1
- package/src/integrations/journal.ts +65 -4
- package/src/integrations/store.ts +5 -0
- package/src/lab/events/limits.ts +4 -0
- package/src/lib/destination-policy.ts +31 -2
- package/src/lib/errors.ts +39 -0
- package/src/lib/provider-outbound.ts +69 -3
- package/src/lib/proxy-env.ts +22 -0
- package/src/lib/redact-folding.ts +176 -0
- package/src/lib/redact.ts +2 -175
- package/src/lib/state-store-sweeper.ts +20 -6
- package/src/lib/token-estimate.ts +94 -27
- package/src/lib/windows-user-principal.ts +53 -5
- package/src/oauth/anthropic-routing.ts +99 -3
- package/src/oauth/generic-account-failover.ts +36 -13
- package/src/oauth/index.ts +127 -26
- package/src/oauth/login-cli.ts +5 -0
- package/src/oauth/meta-muse.ts +117 -15
- package/src/oauth/pool-settings-capability.ts +15 -4
- package/src/providers/api-keys.ts +8 -10
- package/src/providers/default-aliases.ts +39 -0
- package/src/providers/derive.ts +10 -2
- package/src/providers/fastwire.ts +36 -7
- package/src/providers/initial-model-selection-runtime.ts +90 -0
- package/src/providers/initial-model-selection.ts +120 -0
- package/src/providers/key-failover.ts +134 -54
- package/src/providers/key-store.ts +11 -1
- package/src/providers/label.ts +1 -1
- package/src/providers/model-discovery.ts +76 -0
- package/src/providers/model-rename-startup.ts +72 -8
- package/src/providers/openai-sidecar.ts +17 -5
- package/src/providers/openai-tiers-destination.ts +102 -0
- package/src/providers/openai-tiers.ts +2 -99
- package/src/providers/opencode-go-transport.ts +41 -0
- package/src/providers/quota-key-accounts.ts +141 -0
- package/src/providers/quota-types.ts +9 -0
- package/src/providers/quota.ts +535 -104
- package/src/providers/registry.ts +60 -17
- package/src/providers/xai-responses-opt-in.ts +31 -5
- package/src/quota/reset-activation.ts +81 -0
- package/src/quota/reset-detector.ts +305 -0
- package/src/quota/reset-notify-config.ts +162 -0
- package/src/quota/reset-observer.ts +125 -0
- package/src/quota/reset-poller.ts +160 -0
- package/src/quota/reset-seen-store.ts +385 -0
- package/src/quota/reset-sinks.ts +199 -0
- package/src/quota/window-mapping.ts +106 -0
- package/src/responses/apply-patch-envelope.ts +46 -0
- package/src/responses/code-mode-helper-compat.ts +39 -1
- package/src/responses/custom-tool-compat.ts +10 -4
- package/src/responses/hosted-tool-policy.ts +12 -4
- package/src/responses/parser-content.ts +133 -0
- package/src/responses/parser-text-format.ts +24 -0
- package/src/responses/parser-tools.ts +188 -0
- package/src/responses/parser.ts +3 -326
- package/src/responses/state.ts +124 -28
- package/src/router.ts +48 -13
- package/src/routing/analytics.ts +1 -0
- package/src/routing/capability.ts +17 -4
- package/src/server/auth-cors.ts +7 -1
- package/src/server/background-lifecycle.ts +23 -1
- package/src/server/chat-completions.ts +25 -3
- package/src/server/claude-messages.ts +62 -5
- package/src/server/effort-row.ts +1 -1
- package/src/server/fast-row.ts +295 -0
- package/src/server/gui-static.ts +30 -4
- package/src/server/index.ts +107 -21
- package/src/server/management/agent-settings-routes.ts +2 -2
- package/src/server/management/combo-routes.ts +37 -9
- package/src/server/management/config-routes.ts +93 -2
- package/src/server/management/integration-routes.ts +108 -0
- package/src/server/management/model-routes.ts +13 -3
- package/src/server/management/model-rows.ts +20 -1
- package/src/server/management/native-integration-routes.ts +4 -1
- package/src/server/management/oauth-account-routes.ts +38 -10
- package/src/server/management/provider-routes.ts +34 -3
- package/src/server/management/quota-reset-routes.ts +57 -0
- package/src/server/management/route-registry.ts +7 -4
- package/src/server/management/shared.ts +19 -5
- package/src/server/management/system-routes.ts +3 -2
- package/src/server/management-api.ts +14 -2
- package/src/server/ports.ts +12 -2
- package/src/server/relay-eager.ts +38 -23
- package/src/server/relay.ts +4 -0
- package/src/server/request-log.ts +6 -0
- package/src/server/responses/codex-ws-correlation.ts +65 -0
- package/src/server/responses/codex-ws-exchange.ts +261 -0
- package/src/server/responses/codex-ws-metadata.ts +134 -0
- package/src/server/responses/codex-ws-pool.ts +162 -0
- package/src/server/responses/codex-ws-request.ts +87 -0
- package/src/server/responses/codex-ws-session.ts +93 -0
- package/src/server/responses/codex-ws-wire.ts +144 -0
- package/src/server/responses/collaboration.ts +41 -1
- package/src/server/responses/compact.ts +105 -12
- package/src/server/responses/core.ts +476 -57
- package/src/server/responses/fetch-helpers.ts +10 -3
- package/src/server/responses/input-admission.ts +16 -9
- package/src/server/responses/responses-field-backfill.ts +1 -1
- package/src/server/responses/ws-upstream.ts +34 -318
- package/src/server/responses-custom-tool-repair.ts +20 -4
- package/src/server/responses-undeclared-tool-guard.ts +100 -8
- package/src/server/safe-response-headers.ts +23 -0
- package/src/server/search.ts +9 -0
- package/src/server/subagent-models-startup.ts +27 -0
- package/src/server/system-env-shell.ts +238 -0
- package/src/server/system-env.ts +7 -234
- package/src/server/ws-bridge.ts +3 -25
- package/src/server/xai-responses-startup.ts +21 -0
- package/src/service-manager-probe.ts +1 -1
- package/src/service.ts +55 -16
- package/src/types/config.ts +108 -12
- package/src/types/provider.ts +36 -7
- package/src/types/tools.ts +26 -1
- package/src/types.ts +2 -0
- package/src/update/notify.ts +8 -2
- package/src/usage/cost.ts +38 -28
- package/src/usage/expected-prices.ts +34 -15
- package/src/usage/log.ts +2 -0
- package/src/usage/model-identity.ts +26 -0
- package/src/usage/summary.ts +15 -1
- package/src/vision/describe.ts +6 -0
- package/src/vision/image-rewrite.ts +108 -0
- package/src/vision/index.ts +19 -306
- package/src/vision/plan.ts +205 -0
- package/src/web-search/executor.ts +6 -0
- package/src/web-search/index.ts +8 -1
- package/gui/dist/assets/index-BU1tE0sr.js +0 -112
- package/gui/dist/assets/index-DL9-iS6J.css +0 -1
package/src/server/system-env.ts
CHANGED
|
@@ -1,245 +1,18 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
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
|
|
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
|
-
|
|
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",
|
package/src/server/ws-bridge.ts
CHANGED
|
@@ -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
|
-
*
|
|
209
|
-
* Present means `bunPath`/`cliPath` are provenance for the install,
|
|
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
|
-
|
|
490
|
-
|
|
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
|
-
|
|
499
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
package/src/types/config.ts
CHANGED
|
@@ -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
|
|
746
|
-
*
|
|
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
|
|
804
|
+
* Generic OAuth multi-account PROACTIVE account preference (#2568, #695).
|
|
762
805
|
*
|
|
763
|
-
*
|
|
764
|
-
*
|
|
765
|
-
*
|
|
766
|
-
*
|
|
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
|
-
*
|
|
769
|
-
*
|
|
770
|
-
*
|
|
771
|
-
*
|
|
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
|
+
}
|