@bitkyc08/opencodex 2.37.0 → 2.39.0-preview.20260901
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/bin/ocx.mjs +127 -16
- package/gui/dist/assets/index-6HZDkIpX.js +112 -0
- package/gui/dist/assets/index-uvENYLin.css +1 -0
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/aside.svg +3 -0
- package/gui/dist/provider-icons/baseten.svg +13 -0
- package/gui/dist/provider-icons/bizrouter.svg +41 -0
- package/gui/dist/provider-icons/cerebras.svg +26 -0
- package/gui/dist/provider-icons/deepinfra.svg +75 -0
- package/gui/dist/provider-icons/deepseek-harness.svg +3 -0
- package/gui/dist/provider-icons/digitalocean.svg +10 -0
- package/gui/dist/provider-icons/featherless.svg +4 -0
- package/gui/dist/provider-icons/gajae-code.svg +410 -0
- package/gui/dist/provider-icons/hermes-agent.svg +207 -0
- package/gui/dist/provider-icons/hyperbolic.svg +18 -0
- package/gui/dist/provider-icons/kilo.svg +13 -0
- package/gui/dist/provider-icons/litellm.svg +1 -0
- package/gui/dist/provider-icons/minimax.svg +1 -0
- package/gui/dist/provider-icons/nanogpt.svg +74 -0
- package/gui/dist/provider-icons/nebius.svg +1 -0
- package/gui/dist/provider-icons/neuralwatt.svg +27 -0
- package/gui/dist/provider-icons/nous.svg +149 -0
- package/gui/dist/provider-icons/novita.svg +32 -0
- package/gui/dist/provider-icons/oh-my-pi.svg +11 -0
- package/gui/dist/provider-icons/openclaw.svg +54 -0
- package/gui/dist/provider-icons/orcarouter.svg +175 -0
- package/gui/dist/provider-icons/parallel.svg +13 -0
- package/gui/dist/provider-icons/prime-agent.svg +21 -0
- package/gui/dist/provider-icons/sambanova.svg +276 -0
- package/gui/dist/provider-icons/scaleway.svg +11 -0
- package/gui/dist/provider-icons/siliconflow.svg +18 -0
- package/gui/dist/provider-icons/synthetic.svg +12 -0
- package/gui/dist/provider-icons/together.svg +18 -0
- package/gui/dist/provider-icons/umans.svg +30 -0
- package/gui/dist/provider-icons/venice.svg +165 -0
- package/gui/dist/provider-icons/vultr.svg +15 -0
- package/gui/dist/provider-icons/zai.svg +218 -0
- package/gui/dist/provider-icons/zcode.svg +219 -0
- package/gui/dist/provider-icons/zenmux.svg +1 -0
- package/package.json +1 -1
- package/src/adapters/cursor/live-models.ts +1 -0
- package/src/adapters/cursor/protobuf-request.ts +4 -1
- package/src/adapters/cursor/tool-definitions.ts +36 -4
- package/src/adapters/openai-responses.ts +72 -10
- package/src/bridge.ts +15 -15
- package/src/cli/capabilities.ts +14 -0
- package/src/cli/codex-cli-update.ts +96 -0
- package/src/cli/codex-shim-autorestore.ts +3 -0
- package/src/cli/dispatch.ts +54 -18
- package/src/cli/export-command.ts +18 -17
- package/src/cli/help.ts +2 -2
- package/src/cli/index.ts +423 -32
- package/src/cli/integrations.ts +24 -2
- package/src/cli/launcher-context.ts +53 -2
- package/src/cli/opencode.ts +126 -33
- package/src/cli/registry.ts +16 -10
- package/src/cli/system-command.ts +6 -1
- package/src/cli/uninstall-plan.ts +86 -0
- package/src/clients/config-export.ts +293 -28
- package/src/codex/account-store.ts +131 -12
- package/src/codex/auth-api.ts +202 -33
- package/src/codex/autostart-health.ts +3 -3
- package/src/codex/catalog/provider-fetch.ts +96 -10
- package/src/codex/catalog/sync.ts +4 -3
- package/src/codex/cli-install-provenance.ts +795 -0
- package/src/codex/convergence.ts +4 -3
- package/src/codex/credential-mutation-epoch.ts +11 -0
- package/src/codex/history-job.ts +10 -0
- package/src/codex/history-manifest.ts +35 -2
- package/src/codex/history-provider.ts +196 -19
- package/src/codex/history-worker.ts +3 -0
- package/src/codex/main-account.ts +2 -0
- package/src/codex/model-entitlements.ts +430 -27
- package/src/codex/native-profile-manager.ts +4 -0
- package/src/codex/quota-401-recovery.ts +190 -0
- package/src/codex/quota-recovery-timing.ts +28 -0
- package/src/codex/quota.ts +6 -0
- package/src/codex/reset-credit-operation-ledger.ts +1411 -0
- package/src/codex/reset-credit-recovery.ts +20 -2
- package/src/codex/routing.ts +68 -16
- package/src/codex/shim.ts +204 -18
- package/src/codex/subagent-model-fallback.ts +4 -1
- package/src/codex/user-identity.ts +2 -1
- package/src/config/paths.ts +18 -3
- package/src/config/pending-teardown-names.d.mts +8 -0
- package/src/config/pending-teardown-names.mjs +69 -0
- package/src/config/pending-teardown.ts +286 -0
- package/src/config.ts +23 -0
- package/src/generated/compatibility-version.json +155 -67
- package/src/integrations/journal.ts +12 -1
- package/src/integrations/registry.ts +112 -0
- package/src/integrations/state.ts +67 -5
- package/src/integrations/writer.ts +107 -16
- package/src/lib/bounded-subprocess.ts +36 -0
- package/src/lib/process-control.ts +30 -5
- package/src/lib/state-store-registrations.ts +8 -0
- package/src/lib/strict-semver.ts +47 -0
- package/src/lib/windows-elevation.ts +32 -1
- package/src/lib/windows-secret-acl.ts +47 -25
- package/src/lib/windows-service-mutation-lock.ts +133 -0
- package/src/lib/windows-user-principal.ts +15 -17
- package/src/oauth/index.ts +220 -13
- package/src/oauth/store.ts +220 -18
- package/src/responses/spill-store.ts +354 -29
- package/src/responses/state.ts +775 -8
- package/src/server/index.ts +4 -3
- package/src/server/lifecycle.ts +5 -1
- package/src/server/management/integration-routes.ts +36 -5
- package/src/server/management/model-rows.ts +11 -2
- package/src/server/management/provider-routes.ts +4 -0
- package/src/server/management/system-restart.ts +5 -5
- package/src/server/management-api.ts +72 -13
- package/src/server/responses/encrypted-payload.ts +18 -1
- package/src/server/startup-action-control.ts +3 -2
- package/src/server/stop-teardown.ts +84 -0
- package/src/service.ts +749 -53
- package/src/sidecar/candidates.ts +1 -1
- package/src/update/codex-cli-update-launch-policy.d.mts +18 -0
- package/src/update/codex-cli-update-launch-policy.mjs +30 -0
- package/src/update/index.ts +39 -6
- package/src/update/job.ts +10 -11
- package/src/update/proxy-liveness-probe.d.mts +6 -0
- package/src/update/proxy-liveness-probe.mjs +84 -0
- package/src/update/stop-contract.d.mts +2 -0
- package/src/update/stop-contract.mjs +15 -0
- package/src/update/stop-decision.d.mts +10 -0
- package/src/update/stop-decision.mjs +34 -0
- package/src/vision/eligibility.ts +19 -1
- package/src/vision/index.ts +4 -18
- package/gui/dist/assets/index-CowztZdo.js +0 -112
- package/gui/dist/assets/index-jqE_VOKI.css +0 -1
|
@@ -37,7 +37,7 @@ export async function pickerVisibleSidecarCandidates(
|
|
|
37
37
|
auth: SidecarAuthState,
|
|
38
38
|
): Promise<SidecarCandidate[]> {
|
|
39
39
|
let rows: Awaited<ReturnType<typeof listManagementModelRows>> = [];
|
|
40
|
-
try { rows = await listManagementModelRows(config); } catch { rows = []; }
|
|
40
|
+
try { rows = await listManagementModelRows(config, { entitlementWaitMs: 0 }); } catch { rows = []; }
|
|
41
41
|
const byKey = new Map<string, SidecarCandidate>();
|
|
42
42
|
for (const row of rows) {
|
|
43
43
|
if (row.disabled === true) continue;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export const CODEX_CLI_VERSION_MANAGER_ROOT_ENV_SLOTS: readonly [
|
|
2
|
+
"ASDF_DATA_DIR",
|
|
3
|
+
"FNM_DIR",
|
|
4
|
+
"FNM_MULTISHELL_PATH",
|
|
5
|
+
"MISE_DATA_DIR",
|
|
6
|
+
"NODENV_ROOT",
|
|
7
|
+
"NVS_HOME",
|
|
8
|
+
"NVS_NODE_PATH",
|
|
9
|
+
"N_PREFIX",
|
|
10
|
+
"NVM_DIR",
|
|
11
|
+
"NVM_HOME",
|
|
12
|
+
"NVM_SYMLINK",
|
|
13
|
+
"PROTO_HOME",
|
|
14
|
+
"SCOOP",
|
|
15
|
+
"SCOOP_GLOBAL",
|
|
16
|
+
"VOLTA_HOME",
|
|
17
|
+
];
|
|
18
|
+
export function isCodexCliUpdateInspectionArgv(argv: readonly string[]): boolean;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export const CODEX_CLI_VERSION_MANAGER_ROOT_ENV_SLOTS = Object.freeze([
|
|
2
|
+
"ASDF_DATA_DIR",
|
|
3
|
+
"FNM_DIR",
|
|
4
|
+
"FNM_MULTISHELL_PATH",
|
|
5
|
+
"MISE_DATA_DIR",
|
|
6
|
+
"NODENV_ROOT",
|
|
7
|
+
"NVS_HOME",
|
|
8
|
+
"NVS_NODE_PATH",
|
|
9
|
+
"N_PREFIX",
|
|
10
|
+
"NVM_DIR",
|
|
11
|
+
"NVM_HOME",
|
|
12
|
+
"NVM_SYMLINK",
|
|
13
|
+
"PROTO_HOME",
|
|
14
|
+
"SCOOP",
|
|
15
|
+
"SCOOP_GLOBAL",
|
|
16
|
+
"VOLTA_HOME",
|
|
17
|
+
]);
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Detect the read-only Codex CLI updater inspection namespace before Bun loads.
|
|
21
|
+
* Keep this exact and argument-position based: malformed actions still inherit
|
|
22
|
+
* the zero-effect launcher contract and are rejected by the Bun-side parser.
|
|
23
|
+
*/
|
|
24
|
+
export function isCodexCliUpdateInspectionArgv(argv) {
|
|
25
|
+
// Bun consumes every internal launch-proof argument before ordinary command
|
|
26
|
+
// parsing. Classify the same effective argv here so a user-supplied invalid
|
|
27
|
+
// proof cannot hide this namespace from the pre-Bun zero-effect policy.
|
|
28
|
+
const args = argv.slice(2).filter(value => !value.startsWith("--ocx-internal-launch-proof="));
|
|
29
|
+
return args[0] === "system" && args[1] === "codex-cli-update";
|
|
30
|
+
}
|
package/src/update/index.ts
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import { spawn, spawnSync } from "node:child_process";
|
|
2
|
+
import { STOP_HISTORY_INCOMPLETE_EXIT_CODE } from "./stop-contract.mjs";
|
|
3
|
+
import { proxyIdentityAt } from "../server/proxy-liveness";
|
|
4
|
+
import { probeProxyLiveness } from "./proxy-liveness-probe.mjs";
|
|
5
|
+
import { decidePostStopUpdate } from "./stop-decision.mjs";
|
|
2
6
|
import { readFileSync, readdirSync } from "node:fs";
|
|
3
7
|
import { fileURLToPath } from "node:url";
|
|
4
8
|
import { dirname, join } from "node:path";
|
|
5
9
|
import { getConfigDir, loadConfig } from "../config";
|
|
6
10
|
import { readPid, readRuntimePort } from "../config/process-state";
|
|
11
|
+
import { pendingTeardownOutstanding } from "../config/pending-teardown";
|
|
7
12
|
import { npmInvocation } from "./npm-invocation.mjs";
|
|
8
13
|
import {
|
|
9
14
|
npmCachePreflightFailureMessage,
|
|
@@ -246,8 +251,13 @@ export async function runUpdate(): Promise<void> {
|
|
|
246
251
|
// modules after startup, so an in-place update leaves it executing mixed old/new code.
|
|
247
252
|
// Gate on the service and the runtime-port record too, not just the pid file — a
|
|
248
253
|
// service-managed or orphaned proxy can be live while ocx.pid is stale/missing.
|
|
254
|
+
//
|
|
255
|
+
// An outstanding pending-teardown receipt is a fourth reason to run the stop. After a
|
|
256
|
+
// parent crashed mid-deferral all three of the other signals can be absent while the
|
|
257
|
+
// shared client config still points at a proxy that is gone; installing over that
|
|
258
|
+
// silently skips the recovery the receipt was written to trigger (#3008).
|
|
249
259
|
// Full `ocx stop` semantics (drain, service stop, restore).
|
|
250
|
-
if (serviceWasInstalled || readPid() || readRuntimePort()) {
|
|
260
|
+
if (serviceWasInstalled || readPid() || readRuntimePort() || pendingTeardownOutstanding()) {
|
|
251
261
|
console.log("⏹ Stopping the running proxy before updating...");
|
|
252
262
|
const stopStdio = updateChildStdio();
|
|
253
263
|
const stop = spawnSync(process.execPath, selfLaunchArgv(["stop"]), {
|
|
@@ -256,17 +266,39 @@ export async function runUpdate(): Promise<void> {
|
|
|
256
266
|
windowsHide: true,
|
|
257
267
|
});
|
|
258
268
|
if (stopStdio === "pipe") logSpawnOutput("", stop);
|
|
259
|
-
|
|
269
|
+
// One decision, shared with the npm launcher (#3008). The two lanes disagreeing about
|
|
270
|
+
// the same situation is how this shipped fixed on one side only. Absent PID and runtime
|
|
271
|
+
// files are weak evidence - a crashed-but-listening proxy leaves none - so the captured
|
|
272
|
+
// endpoint is asked, and `null` from proxyIdentityAt covers refusal AND timeout alike.
|
|
273
|
+
const identity = await proxyIdentityAt(capturedListen.port, { hostname: capturedListen.hostname });
|
|
274
|
+
const decision = decidePostStopUpdate({
|
|
275
|
+
status: stop.status,
|
|
276
|
+
hasRuntimeState: !!(readPid() || readRuntimePort()),
|
|
277
|
+
// Re-checked AFTER the stop: a quarantined receipt lets the stop itself succeed
|
|
278
|
+
// (there is nothing left to stop), so a pre-stop check alone let the retry install
|
|
279
|
+
// over a teardown that never ran.
|
|
280
|
+
teardownOutstanding: pendingTeardownOutstanding(),
|
|
281
|
+
liveness: identity ? "live" : probeProxyLiveness(capturedListen.port, capturedListen.hostname),
|
|
282
|
+
});
|
|
283
|
+
const historyOnlyStop = decision.reason === "history-only";
|
|
284
|
+
if (!decision.proceed) {
|
|
260
285
|
if (trayWasRunning) {
|
|
261
286
|
try {
|
|
262
287
|
const { startWindowsTray } = await import("../tray/windows");
|
|
263
288
|
startWindowsTray();
|
|
264
289
|
} catch { /* preserve the proxy stop failure */ }
|
|
265
290
|
}
|
|
266
|
-
|
|
291
|
+
if (decision.reason === "teardown-outstanding") {
|
|
292
|
+
console.error("⚠️ A shared teardown from an earlier stop is still outstanding and needs manual review; aborting the update.");
|
|
293
|
+
console.error(" Confirm no proxy is running, run 'ocx restore', then remove the pending-teardown file in your opencodex home.");
|
|
294
|
+
} else {
|
|
295
|
+
console.error(decision.reason === "proxy-unknown"
|
|
296
|
+
? `⚠️ Could not confirm the proxy on ${capturedListen.hostname}:${capturedListen.port} is stopped; aborting the update. Run 'ocx stop' and retry.`
|
|
297
|
+
: "⚠️ Could not stop the running proxy; aborting the update. Run 'ocx stop' and retry.");
|
|
298
|
+
}
|
|
267
299
|
process.exit(1);
|
|
268
300
|
}
|
|
269
|
-
if (historyRestoreIncomplete()) {
|
|
301
|
+
if (historyOnlyStop || historyRestoreIncomplete()) {
|
|
270
302
|
console.warn(
|
|
271
303
|
"⚠️ Codex resume-history metadata restore is incomplete (a backup manifest remains).\n" +
|
|
272
304
|
" The DB may be busy or the manifest/target may need review; untracked routed history is intentionally unchanged.\n" +
|
|
@@ -347,8 +379,9 @@ export async function runUpdate(): Promise<void> {
|
|
|
347
379
|
}
|
|
348
380
|
}
|
|
349
381
|
if (!serviceRefreshed || !serviceViable) {
|
|
350
|
-
//
|
|
351
|
-
//
|
|
382
|
+
// Repair normally avoids elevation for a healthy scheduler task, but a stale
|
|
383
|
+
// definition may require guarded create/elevation. It can also fail — or exit 0
|
|
384
|
+
// while leaving stale/missing assets that never start
|
|
352
385
|
// the proxy. Fall back to a direct detached proxy start so the update
|
|
353
386
|
// never leaves the user without a running proxy — but only when the port is free.
|
|
354
387
|
if (!freed) {
|
package/src/update/job.ts
CHANGED
|
@@ -459,8 +459,8 @@ export function restartCommand(
|
|
|
459
459
|
const startArgs = pinPort
|
|
460
460
|
? [launcher, "start", "--port", String(Math.trunc(port))]
|
|
461
461
|
: [launcher, "start"];
|
|
462
|
-
// Default to the
|
|
463
|
-
//
|
|
462
|
+
// Default to the in-place refresh: `install` always registers, while repair reuses a healthy
|
|
463
|
+
// Windows scheduler definition and re-registers only when the live definition is stale.
|
|
464
464
|
const svcArgs = serviceInstalled ? [launcher, ...(serviceArgs ?? ["service", "repair"])] : startArgs;
|
|
465
465
|
if (installer === "npm") {
|
|
466
466
|
const bin = nodeBin();
|
|
@@ -1116,12 +1116,10 @@ async function restartAfterUpdate(
|
|
|
1116
1116
|
const preServiceAllow = reclaimKillAllowlist();
|
|
1117
1117
|
const freed = await waitFn(port, hostname, reclaimOptsFor(preServiceAllow));
|
|
1118
1118
|
let skipServiceInstall = false;
|
|
1119
|
-
// This skip existed because
|
|
1120
|
-
//
|
|
1121
|
-
//
|
|
1122
|
-
//
|
|
1123
|
-
// skipping would leave the dashboard-triggered update — the most common Windows
|
|
1124
|
-
// path — with a stale service it could have refreshed.
|
|
1119
|
+
// This skip existed because refresh ran `ocx service install`, whose Windows path always
|
|
1120
|
+
// registers. `service repair` normally reuses the live task and can refresh a stale
|
|
1121
|
+
// definition through its guarded create/elevation path, so the install-only skip no longer
|
|
1122
|
+
// applies and would leave the common dashboard update with stale service assets.
|
|
1125
1123
|
//
|
|
1126
1124
|
// Only a caller that still passes install argv keeps the old behavior.
|
|
1127
1125
|
const refreshRegisters = (svcArgs ?? []).includes("install");
|
|
@@ -1161,9 +1159,10 @@ async function restartAfterUpdate(
|
|
|
1161
1159
|
const result = run(job, cmd.bin, cmd.args);
|
|
1162
1160
|
serviceOk = result.status === 0;
|
|
1163
1161
|
if (!serviceOk) {
|
|
1164
|
-
// The refresh that just failed was `ocx service repair` (serviceReinstallArgs)
|
|
1165
|
-
//
|
|
1166
|
-
//
|
|
1162
|
+
// The refresh that just failed was `ocx service repair` (serviceReinstallArgs).
|
|
1163
|
+
// It normally reuses a healthy registration, but a stale definition may have tried
|
|
1164
|
+
// guarded re-registration/elevation. Advising `install` here would unconditionally
|
|
1165
|
+
// send the user to re-registration — a UAC prompt on
|
|
1167
1166
|
// Windows and a possible WinSW-to-scheduler backend switch — to fix a service
|
|
1168
1167
|
// that is already registered. Point at the same command that failed so its
|
|
1169
1168
|
// output explains why, on every platform.
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Is something still answering `/healthz` as an opencodex proxy on this endpoint?
|
|
5
|
+
*
|
|
6
|
+
* Absent PID and runtime-port files are weak evidence that the proxy is gone: a crashed
|
|
7
|
+
* but still-listening process, or one supervised outside our records, leaves no files and
|
|
8
|
+
* keeps the port. Replacing package files under it leaves a server running a mix of old
|
|
9
|
+
* and new modules, which is the hazard `ocx update` stops the proxy to avoid (#3008).
|
|
10
|
+
*
|
|
11
|
+
* Synchronous and dependency-free because it runs inside the plain-Node launcher's
|
|
12
|
+
* `runNpmSelfUpdate`, which is not async and cannot import the TypeScript liveness module.
|
|
13
|
+
* A separate Node child does the fetch so the caller keeps its straight-line control flow.
|
|
14
|
+
*
|
|
15
|
+
* Returns `"live" | "dead" | "unknown"`, and the caller treats `unknown` as a reason to
|
|
16
|
+
* stop. Fail-open was wrong here: a listener that accepts connections but withholds
|
|
17
|
+
* `/healthz`, or a probe that times out, is exactly the state where replacing package
|
|
18
|
+
* files is most dangerous, and "we could not tell" is not evidence the proxy is gone.
|
|
19
|
+
* Only a refused connection or a definitive non-OpenCodex answer earns `"dead"`.
|
|
20
|
+
*/
|
|
21
|
+
export function probeProxyLiveness(port, hostname = "127.0.0.1", timeoutMs = 1500) {
|
|
22
|
+
// An unusable port is not an ambiguous probe: there is nothing to ask.
|
|
23
|
+
if (!Number.isFinite(port) || port <= 0 || port > 65535) return "dead";
|
|
24
|
+
// Normalize HERE rather than at each call site. Leaving it to the callers put the fix in
|
|
25
|
+
// one lane and not the other, and a bracketed IPv6 literal handed to node:http answers
|
|
26
|
+
// nothing - which the tri-state correctly reports as "unknown" and the updater correctly
|
|
27
|
+
// treats as a reason to abort, turning a healthy stop into a refused update.
|
|
28
|
+
let host = typeof hostname === "string" && hostname.trim() !== "" ? hostname.trim() : "127.0.0.1";
|
|
29
|
+
// A wildcard bind answers on loopback; `node:http` cannot dial the wildcard itself.
|
|
30
|
+
if (host === "0.0.0.0" || host === "*") host = "127.0.0.1";
|
|
31
|
+
if (host === "::" ) host = "::1";
|
|
32
|
+
// `[::1]` is a URL spelling; the socket layer wants the bare address.
|
|
33
|
+
if (host.startsWith("[") && host.endsWith("]")) host = host.slice(1, -1);
|
|
34
|
+
// `node:http` rather than `fetch`: the child inherits a parent whose event loop is
|
|
35
|
+
// blocked on `spawnSync`, and an aborted-before-dispatch fetch reports the same "not
|
|
36
|
+
// live" as a genuinely dead port. A request emitted on the socket cannot be confused
|
|
37
|
+
// with one that never left.
|
|
38
|
+
const script = [
|
|
39
|
+
"const http = require('node:http');",
|
|
40
|
+
"const [host, port, timeout] = process.argv.slice(1);",
|
|
41
|
+
"const req = http.get({ host, port: Number(port), path: '/healthz', timeout: Number(timeout) }, res => {",
|
|
42
|
+
" let body = '';",
|
|
43
|
+
" res.setEncoding('utf8');",
|
|
44
|
+
" res.on('data', chunk => { body += chunk; });",
|
|
45
|
+
" res.on('end', () => {",
|
|
46
|
+
" try {",
|
|
47
|
+
" const parsed = JSON.parse(body);",
|
|
48
|
+
" // Mirrors isOpencodexHealthz in src/server/proxy-liveness.ts. A foreign server",
|
|
49
|
+
" // that happens to expose /healthz must not be read as our proxy, and a",
|
|
50
|
+
" // pre-identity build of ours must not be read as foreign.",
|
|
51
|
+
" const isOpencodex = parsed && typeof parsed === 'object'",
|
|
52
|
+
" && (parsed.service === 'opencodex'",
|
|
53
|
+
" || (parsed.service === undefined",
|
|
54
|
+
" && parsed.status === 'ok'",
|
|
55
|
+
" && typeof parsed.version === 'string'",
|
|
56
|
+
" && typeof parsed.uptime === 'number'));",
|
|
57
|
+
" // Only a clean 200 decides anything. Any other status means the endpoint is",
|
|
58
|
+
" // answering but not telling us what it is, which is not evidence of absence.",
|
|
59
|
+
" if (res.statusCode !== 200) process.stdout.write('UNKNOWN');",
|
|
60
|
+
" else process.stdout.write(isOpencodex ? 'LIVE' : 'DEAD');",
|
|
61
|
+
" } catch { process.stdout.write('UNKNOWN'); }",
|
|
62
|
+
" });",
|
|
63
|
+
"});",
|
|
64
|
+
"req.on('timeout', () => { process.stdout.write('UNKNOWN'); req.destroy(); });",
|
|
65
|
+
"// ECONNREFUSED is the one error that proves nothing is listening. Everything else -",
|
|
66
|
+
"// reset, unreachable host, TLS confusion - leaves the question open.",
|
|
67
|
+
"req.on('error', err => process.stdout.write(err && err.code === 'ECONNREFUSED' ? 'DEAD' : 'UNKNOWN'));",
|
|
68
|
+
].join("\n");
|
|
69
|
+
try {
|
|
70
|
+
const probe = spawnSync(
|
|
71
|
+
process.execPath,
|
|
72
|
+
["-e", script, host, String(port), String(timeoutMs)],
|
|
73
|
+
{ encoding: "utf8", timeout: timeoutMs + 1500, windowsHide: true },
|
|
74
|
+
);
|
|
75
|
+
const out = probe.stdout ?? "";
|
|
76
|
+
if (out.includes("LIVE")) return "live";
|
|
77
|
+
if (out.includes("DEAD")) return "dead";
|
|
78
|
+
// A child that produced nothing, was killed by its own timeout, or failed to spawn
|
|
79
|
+
// leaves the question open rather than answering it.
|
|
80
|
+
return "unknown";
|
|
81
|
+
} catch {
|
|
82
|
+
return "unknown";
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The exit code `ocx stop` uses to say "teardown succeeded, history cleanup did not".
|
|
3
|
+
*
|
|
4
|
+
* This is plain ESM rather than TypeScript because it has two consumers on opposite sides
|
|
5
|
+
* of a process boundary: `src/update/index.ts` and the Node launcher `bin/ocx.mjs`, which
|
|
6
|
+
* cannot import a `.ts` module. A TypeScript union would not survive `spawnSync` anyway —
|
|
7
|
+
* the value has to be on the wire, and an exit code is the wire.
|
|
8
|
+
*
|
|
9
|
+
* 79 is deliberate. It sits above the `sysexits.h` block (64-78), below `128 + signal`,
|
|
10
|
+
* and outside every code this CLI already uses: `src/cli/index.ts` emits 0, 1 and 130,
|
|
11
|
+
* and `src/cli/dispatch.ts` adds 2, 4 and 64. Picking one of those would have made a
|
|
12
|
+
* history-only stop indistinguishable from a config conflict, and `bin/ocx.mjs` mirrors
|
|
13
|
+
* the child's code faithfully enough to propagate the confusion.
|
|
14
|
+
*/
|
|
15
|
+
export const STOP_HISTORY_INCOMPLETE_EXIT_CODE = 79;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Declaration for the plain-ESM post-stop decision shared with `bin/ocx.mjs`. */
|
|
2
|
+
export declare function decidePostStopUpdate(input: {
|
|
3
|
+
status: number | null;
|
|
4
|
+
hasRuntimeState: boolean;
|
|
5
|
+
liveness: "live" | "dead" | "unknown";
|
|
6
|
+
teardownOutstanding?: boolean;
|
|
7
|
+
}): {
|
|
8
|
+
proceed: boolean;
|
|
9
|
+
reason: "stop-failed" | "runtime-state" | "teardown-outstanding" | "proxy-live" | "proxy-unknown" | "history-only" | "ok";
|
|
10
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { STOP_HISTORY_INCOMPLETE_EXIT_CODE } from "./stop-contract.mjs";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* May an update replace package files after `ocx stop` returned?
|
|
5
|
+
*
|
|
6
|
+
* Both updaters ask this: `src/update/index.ts` on the Bun path and `bin/ocx.mjs` on the
|
|
7
|
+
* npm path the dashboard uses. It lives here as plain ESM so the Node launcher can import
|
|
8
|
+
* it, and so the two lanes cannot drift into disagreeing about the same situation — which
|
|
9
|
+
* is how #3008 shipped in the first place, with the fix on one side only.
|
|
10
|
+
*
|
|
11
|
+
* Returns `{ proceed, reason }`. The reasons are:
|
|
12
|
+
*
|
|
13
|
+
* - `stop-failed` — a nonzero status other than the history-only code, or a signal kill.
|
|
14
|
+
* A signal kill carries no evidence the teardown finished, so it is not a maybe.
|
|
15
|
+
* - `runtime-state` — a PID or runtime-port record survived the stop.
|
|
16
|
+
* - `teardown-outstanding` — a shared-teardown obligation survived the stop. That is a
|
|
17
|
+
* quarantined receipt awaiting a human: the stop itself can succeed (there was nothing
|
|
18
|
+
* left to stop), so checking only BEFORE the stop let the retry sail straight through
|
|
19
|
+
* and install over a teardown that never ran.
|
|
20
|
+
* - `proxy-live` — something is still answering as our proxy on the captured endpoint.
|
|
21
|
+
* - `proxy-unknown` — the probe could not answer. Absence of proof is not proof of
|
|
22
|
+
* absence, and replacing files under a live server leaves it running a mix of old and
|
|
23
|
+
* new modules.
|
|
24
|
+
* - `ok` / `history-only` — proceed; the second also prints the manifest warning.
|
|
25
|
+
*/
|
|
26
|
+
export function decidePostStopUpdate({ status, hasRuntimeState, liveness, teardownOutstanding = false }) {
|
|
27
|
+
const historyOnly = status === STOP_HISTORY_INCOMPLETE_EXIT_CODE;
|
|
28
|
+
if (status !== 0 && !historyOnly) return { proceed: false, reason: "stop-failed" };
|
|
29
|
+
if (hasRuntimeState) return { proceed: false, reason: "runtime-state" };
|
|
30
|
+
if (teardownOutstanding) return { proceed: false, reason: "teardown-outstanding" };
|
|
31
|
+
if (liveness === "live") return { proceed: false, reason: "proxy-live" };
|
|
32
|
+
if (liveness !== "dead") return { proceed: false, reason: "proxy-unknown" };
|
|
33
|
+
return { proceed: true, reason: historyOnly ? "history-only" : "ok" };
|
|
34
|
+
}
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
* explicit modalities.
|
|
22
22
|
*/
|
|
23
23
|
import { modelInList, type OcxConfig, type OcxProviderConfig } from "../types";
|
|
24
|
+
import { modelRecordValue } from "../reasoning-effort";
|
|
24
25
|
import { getModelMetadataCaseInsensitive, resolveMetadataProvider } from "../generated/model-metadata";
|
|
25
26
|
import { nativeInputModalities } from "../codex/catalog/metadata";
|
|
26
27
|
import { SUPPORTED_NATIVE_OPENAI_SLUGS } from "../codex/catalog/native-models";
|
|
@@ -68,6 +69,22 @@ export interface VisionModelOption {
|
|
|
68
69
|
|
|
69
70
|
type EnrichedProviderCache = Map<string, OcxProviderConfig>;
|
|
70
71
|
|
|
72
|
+
/**
|
|
73
|
+
* Whether the proxy must describe images for this model before dispatching its main request.
|
|
74
|
+
*
|
|
75
|
+
* `noVisionModels` is an explicit override. A modality declaration is only evidence for this
|
|
76
|
+
* path when it describes a text model that excludes image input: an audio-only declaration is
|
|
77
|
+
* not a text-only model and must not be widened to image through the vision sidecar.
|
|
78
|
+
*/
|
|
79
|
+
export function isModelVisionSidecarConsumer(
|
|
80
|
+
provider: Pick<OcxProviderConfig, "noVisionModels" | "modelInputModalities">,
|
|
81
|
+
modelId: string,
|
|
82
|
+
): boolean {
|
|
83
|
+
if (modelInList(provider.noVisionModels, modelId)) return true;
|
|
84
|
+
const modalities = modelRecordValue(provider.modelInputModalities, modelId);
|
|
85
|
+
return Array.isArray(modalities) && modalities.includes("text") && !modalities.includes("image");
|
|
86
|
+
}
|
|
87
|
+
|
|
71
88
|
function advertisesImageInput(modalities: readonly string[] | undefined): boolean | undefined {
|
|
72
89
|
if (!modalities || modalities.length === 0) return undefined;
|
|
73
90
|
return modalities.includes("image");
|
|
@@ -105,7 +122,8 @@ function isVisionSidecarConsumerWithCache(
|
|
|
105
122
|
modelId: string,
|
|
106
123
|
cache: EnrichedProviderCache,
|
|
107
124
|
): boolean {
|
|
108
|
-
|
|
125
|
+
const provider = enrichedProviderForVision(config, providerName, cache);
|
|
126
|
+
return provider !== undefined && isModelVisionSidecarConsumer(provider, modelId);
|
|
109
127
|
}
|
|
110
128
|
|
|
111
129
|
/**
|
package/src/vision/index.ts
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import type { OcxConfig, OcxContentPart, OcxMessage, OcxParsedRequest, OcxProviderConfig, OcxTextContent } from "../types";
|
|
3
|
-
import { modelInList } from "../types";
|
|
4
|
-
import { modelRecordValue } from "../reasoning-effort";
|
|
5
3
|
import type { VisionReasoningEffort } from "../reasoning-effort";
|
|
6
4
|
import { describeImage, type DescribeOutcome, type VisionSettings } from "./describe";
|
|
7
5
|
import { describeImageAnthropic } from "./anthropic-describe";
|
|
8
6
|
import { describeImageRouted } from "./routed-describe";
|
|
9
|
-
import { modelAcceptsImageInput } from "./eligibility";
|
|
7
|
+
import { isModelVisionSidecarConsumer as isModelTextOnly, modelAcceptsImageInput } from "./eligibility";
|
|
10
8
|
import { normalizeVisionReasoningForModel } from "./reasoning";
|
|
11
9
|
import type { CodexAuthContext } from "../codex/auth-context";
|
|
12
10
|
import { resolveSidecarAuth } from "../sidecar/auth";
|
|
@@ -22,24 +20,12 @@ import {
|
|
|
22
20
|
|
|
23
21
|
export { describeImage } from "./describe";
|
|
24
22
|
|
|
25
|
-
/**
|
|
26
|
-
|
|
27
|
-
* `noVisionModels` or declared with `modelInputModalities` that exclude "image".
|
|
28
|
-
* Returns false for unknown models (no evidence either way) so they fall through
|
|
29
|
-
* to native image passthrough, which is the safe default for an unclassified model.
|
|
30
|
-
*/
|
|
31
|
-
export function isModelTextOnly(
|
|
32
|
-
provider: OcxProviderConfig,
|
|
33
|
-
modelId: string,
|
|
34
|
-
): boolean {
|
|
35
|
-
if (modelInList(provider.noVisionModels, modelId)) return true;
|
|
36
|
-
const modalities = modelRecordValue(provider.modelInputModalities, modelId);
|
|
37
|
-
if (Array.isArray(modalities) && modalities.length > 0 && !modalities.includes("image")) return true;
|
|
38
|
-
return false;
|
|
39
|
-
}
|
|
23
|
+
/** Backward-compatible request-time name for the shared vision-sidecar consumer predicate. */
|
|
24
|
+
export { isModelVisionSidecarConsumer as isModelTextOnly } from "./eligibility";
|
|
40
25
|
export { describeImageAnthropic, parseAnthropicVisionSSE } from "./anthropic-describe";
|
|
41
26
|
export {
|
|
42
27
|
BASELINE_VISION_MODELS,
|
|
28
|
+
isModelVisionSidecarConsumer,
|
|
43
29
|
isVisionEligibleModel,
|
|
44
30
|
isVisionSidecarConsumer,
|
|
45
31
|
modelAcceptsImageInput,
|