@yansigit/opencodex 2.31.2 → 2.32.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/bin/ocx.mjs +99 -70
- package/gui/dist/assets/index-BG43zwVe.js +102 -0
- package/gui/dist/assets/index-CiSI-jrP.css +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic.ts +6 -8
- package/src/adapters/base.ts +2 -0
- package/src/adapters/command-code-project-context.ts +377 -0
- package/src/adapters/command-code.ts +7 -4
- package/src/adapters/cursor/live-models.ts +8 -0
- package/src/adapters/cursor/live-transport.ts +21 -0
- package/src/adapters/cursor/native-exec-bridge.ts +141 -0
- package/src/adapters/cursor/native-exec-desktop.ts +16 -0
- package/src/adapters/cursor/tool-definitions.ts +1 -1
- package/src/adapters/google-http.ts +12 -2
- package/src/adapters/google-wire-compiler.ts +83 -2
- package/src/adapters/google.ts +62 -21
- package/src/adapters/kiro.ts +0 -3
- package/src/adapters/openai-responses.ts +3 -0
- package/src/adapters/tool-catalog-nudge.ts +1 -1
- package/src/adapters/xai-web-search.ts +7 -2
- package/src/bridge.ts +21 -15
- package/src/cli/dispatch.ts +50 -2
- package/src/cli/doctor.ts +24 -11
- package/src/cli/help.ts +4 -3
- package/src/cli/index.ts +11 -4
- package/src/cli/models.ts +13 -3
- package/src/cli/observe.ts +20 -5
- package/src/cli/provider.ts +2 -1
- package/src/cli/registry.ts +7 -5
- package/src/cli/status.ts +2 -1
- package/src/cli/system-restart-client.ts +1 -1
- package/src/cli/usage-report.ts +134 -0
- package/src/codex/app-server-processes.ts +3 -1
- package/src/codex/catalog/aggregation.ts +13 -1
- package/src/codex/catalog/effort.ts +24 -9
- package/src/codex/catalog/model-metadata.ts +566 -0
- package/src/codex/catalog/parsing.ts +33 -0
- package/src/codex/catalog/provider-fetch.ts +123 -32
- package/src/codex/catalog/sync.ts +5 -4
- package/src/codex/desktop-app-restart.ts +342 -0
- package/src/codex/history-job.ts +32 -3
- package/src/codex/history-manifest.ts +112 -0
- package/src/codex/history-migration-guardian.ts +5 -5
- package/src/codex/history-provider.ts +825 -247
- package/src/codex/history-worker.ts +8 -5
- package/src/codex/inject.ts +49 -21
- package/src/codex/injected-marker.ts +1 -1
- package/src/codex/internal/history-writer.ts +4 -3
- package/src/codex/native-profile-startup.ts +157 -27
- package/src/codex/native-residue.ts +26 -33
- package/src/combos/failover.ts +27 -0
- package/src/compatibility/index.ts +26 -0
- package/src/compatibility/manifest.ts +253 -0
- package/src/compatibility/openai-responses.ts +81 -0
- package/src/config/atomic-write.ts +219 -0
- package/src/config/paths.ts +40 -0
- package/src/config/process-state.ts +308 -0
- package/src/config/provider-validation.ts +177 -0
- package/src/config.ts +77 -812
- package/src/generated/compatibility-version.json +162 -94
- package/src/images/plan.ts +5 -4
- package/src/integrations/ownership-policy.ts +141 -0
- package/src/integrations/ownership.ts +10 -0
- package/src/integrations/state.ts +44 -5
- package/src/integrations/writer.ts +6 -0
- package/src/lab/subject/behavior-fingerprint.ts +1 -1
- package/src/lib/bounded-body.ts +14 -2
- package/src/lib/process-control.ts +2 -1
- package/src/lib/state-store-registrations.ts +2 -0
- package/src/lib/tool-argument-integers.ts +56 -5
- package/src/oauth/health.ts +1 -1
- package/src/oauth/index.ts +3 -0
- package/src/providers/registry.ts +1 -1
- package/src/reasoning-effort.ts +19 -2
- package/src/responses/apply-patch-envelope.ts +63 -0
- package/src/responses/custom-tool-compat.ts +132 -38
- package/src/responses/parser.ts +3 -2
- package/src/responses/reasoning-replay-cache.ts +81 -3
- package/src/routing/compatibility/behavior.ts +3 -0
- package/src/server/auth-cors.ts +9 -7
- package/src/server/index.ts +102 -21
- package/src/server/local-management-read-client.ts +1 -1
- package/src/server/local-provider-reload-client.ts +1 -1
- package/src/server/management/agent-settings-routes.ts +1 -1
- package/src/server/management/config-routes.ts +4 -1
- package/src/server/management/context.ts +1 -1
- package/src/server/management/logs-usage-routes.ts +27 -6
- package/src/server/management/model-routes.ts +8 -4
- package/src/server/management/native-integration-routes.ts +2 -1
- package/src/server/management/provider-capability-config.ts +1 -1
- package/src/server/management/system-restart.ts +1 -1
- package/src/server/port-reclaim.ts +1 -1
- package/src/server/proxy-liveness.ts +2 -1
- package/src/server/request-log-conversation.ts +30 -0
- package/src/server/responses/codex-auth-error.ts +55 -0
- package/src/server/responses/combo-stream-preflight.ts +171 -0
- package/src/server/responses/compact.ts +6 -21
- package/src/server/responses/core.ts +242 -101
- package/src/server/responses/fetch-helpers.ts +2 -97
- package/src/server/responses-custom-tool-repair.ts +41 -5
- package/src/server/responses-undeclared-tool-guard.ts +156 -15
- package/src/service.ts +8 -4
- package/src/types/provider.ts +7 -0
- package/src/types/request.ts +12 -1
- package/src/types/tools.ts +87 -11
- package/src/types.ts +1 -1
- package/src/update/index.ts +5 -4
- package/src/update/job.ts +3 -1
- package/src/update/transactional-install.mjs +8 -1
- package/src/usage/log.ts +16 -8
- package/src/usage/summary.ts +201 -8
- package/src/vision/describe.ts +18 -13
- package/src/web-search/executor.ts +10 -3
- package/src/web-search/gemini-executor.ts +35 -13
- package/src/web-search/index.ts +85 -1
- package/gui/dist/assets/index-BAMgarF9.js +0 -102
- package/gui/dist/assets/index-CGoDO3uO.css +0 -1
package/src/cli/doctor.ts
CHANGED
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
import { accessSync, constants, existsSync, readFileSync } from "node:fs";
|
|
11
11
|
import { homedir } from "node:os";
|
|
12
12
|
import { dirname, join } from "node:path";
|
|
13
|
-
import { getConfigDir, getConfigPath, readConfigDiagnostics,
|
|
13
|
+
import { getConfigDir, getConfigPath, readConfigDiagnostics, resolveEnvValue } from "../config";
|
|
14
|
+
import { readPid } from "../config/process-state";
|
|
14
15
|
import { findLiveProxy, type LiveProxy } from "../server/proxy-liveness";
|
|
15
16
|
import { BUN_RUNTIME_SOURCES } from "../lib/bun-runtime";
|
|
16
17
|
import type { BunRuntimeSource } from "../lib/bun-runtime";
|
|
@@ -1072,10 +1073,10 @@ export async function runDoctor(args: string[] = []): Promise<void> {
|
|
|
1072
1073
|
console.log(` ${probe.ok ? "ok " : "-- "} ${WHAM_USAGE_URL}`);
|
|
1073
1074
|
console.log(` ${detail}, ${probe.durationMs}ms, ${probe.authenticated ? "authenticated" : "unauthenticated"}`);
|
|
1074
1075
|
|
|
1075
|
-
// Design B upgrade visibility:
|
|
1076
|
-
//
|
|
1077
|
-
//
|
|
1078
|
-
console.log("\nCodex history
|
|
1076
|
+
// Design B upgrade visibility: only the backup manifest authorizes restoring provider
|
|
1077
|
+
// metadata. Bare routed rows have unknown provenance and remain unchanged. This read-only
|
|
1078
|
+
// probe reports manifest work and database readability; it never mutates.
|
|
1079
|
+
console.log("\nCodex history metadata restore");
|
|
1079
1080
|
// The history failure messages point here; make the visit worthwhile by
|
|
1080
1081
|
// probing the coordinator namespace the locks live in. The probe exercises
|
|
1081
1082
|
// identity, runtime-root, and permission checks without taking any lock or
|
|
@@ -1096,11 +1097,17 @@ export async function runDoctor(args: string[] = []): Promise<void> {
|
|
|
1096
1097
|
for (const line of formatCoordinatorDoctorLines(inspectCodexCoordinator())) console.log(line);
|
|
1097
1098
|
const pending = countPendingOpencodexHistory();
|
|
1098
1099
|
if (pending.failed) {
|
|
1099
|
-
|
|
1100
|
+
if (pending.failureReason === "busy") {
|
|
1101
|
+
console.log(" -- history database, backup manifest, or rollout file is busy — exact metadata restore is pending");
|
|
1102
|
+
} else if (pending.failureReason === "permission") {
|
|
1103
|
+
console.log(" -- state DB or backup manifest access was denied — restore state unknown");
|
|
1104
|
+
} else {
|
|
1105
|
+
console.log(" -- backup manifest or restore target failed integrity checks — manual review required");
|
|
1106
|
+
}
|
|
1100
1107
|
} else if (pending.pendingRows === 0 && pending.backupEntries === 0) {
|
|
1101
|
-
console.log(" ok no
|
|
1108
|
+
console.log(" ok no manifest-backed provider metadata pending; untracked routed history is unchanged");
|
|
1102
1109
|
} else {
|
|
1103
|
-
console.log(` -- ${pending.
|
|
1110
|
+
console.log(` -- ${pending.backupEntries} backup manifest entr${pending.backupEntries === 1 ? "y" : "ies"} pending exact metadata restore`);
|
|
1104
1111
|
}
|
|
1105
1112
|
|
|
1106
1113
|
console.log("\nProject Codex configs");
|
|
@@ -1148,7 +1155,7 @@ export async function runDoctor(args: string[] = []): Promise<void> {
|
|
|
1148
1155
|
const { collectCodexAppServerCatalogState } = await import("../codex/app-server-processes");
|
|
1149
1156
|
const catalogState = collectCodexAppServerCatalogState();
|
|
1150
1157
|
if (catalogState.state === "stale") {
|
|
1151
|
-
console.log(` [WARN] Codex app-server (PID(s): ${catalogState.processes.map(p => p.pid).join(", ")}) started before the on-disk catalog changed; its in-memory model list disagrees with ocx. Action: restart Codex (or run \`ocx sync --restart-codex\`)`);
|
|
1158
|
+
console.log(` [WARN] Codex app-server (PID(s): ${catalogState.processes.map(p => p.pid).join(", ")}) started before the on-disk catalog changed; its in-memory model list disagrees with ocx. Action: restart Codex (or run \`ocx sync --restart-codex\`; on Windows the desktop app may need \`ocx sync --restart-desktop-app\`)`);
|
|
1152
1159
|
} else if (catalogState.state === "unknown") {
|
|
1153
1160
|
console.log(" [WARN] Could not verify whether the running Codex app-server's model catalog is current (start time or catalog unreadable). Action: if the model list looks stale, restart Codex");
|
|
1154
1161
|
} else if (catalogState.state === "fresh") {
|
|
@@ -1185,8 +1192,14 @@ export async function runDoctor(args: string[] = []): Promise<void> {
|
|
|
1185
1192
|
}
|
|
1186
1193
|
}
|
|
1187
1194
|
}
|
|
1188
|
-
if (pending.failed
|
|
1189
|
-
hints.push("
|
|
1195
|
+
if (pending.failed && pending.failureReason === "busy") {
|
|
1196
|
+
hints.push("Backed-up history metadata is pending or its state is unreadable. The running proxy retries exact restoration automatically; to force it now, close the Codex app and run 'ocx sync'. Untracked routed history is not relabeled.");
|
|
1197
|
+
} else if (pending.failed && pending.failureReason === "permission") {
|
|
1198
|
+
hints.push("Backed-up history metadata could not be inspected because access was denied. Fix access to the reported Codex history paths, then run 'ocx sync'; repeated retries do not repair permissions.");
|
|
1199
|
+
} else if (pending.failed) {
|
|
1200
|
+
hints.push("The history manifest or its target is invalid or changed. Preserve both, inspect the manifest/database/rollout identity, and do not repeatedly run 'ocx sync' until the mismatch is understood. Untracked routed history is not relabeled.");
|
|
1201
|
+
} else if (pending.backupEntries > 0) {
|
|
1202
|
+
hints.push("Backed-up history metadata is pending. The running proxy retries exact restoration automatically; to force it now, close the Codex app and run 'ocx sync'. Untracked routed history is not relabeled.");
|
|
1190
1203
|
}
|
|
1191
1204
|
if (dual.dualInstall && !dual.effectiveIsWindowsMount) {
|
|
1192
1205
|
hints.push(`Codex is installed on BOTH WSL and Windows. Each side keeps its own ~/.codex (logins, config, catalog are separate); ocx here manages the Linux one. To share a single home, set CODEX_HOME=${dual.windowsCodexHomes[0] ?? `${dual.automountRoot}/c/Users/<you>/.codex`} in WSL (drvfs file locking is less reliable).`);
|
package/src/cli/help.ts
CHANGED
|
@@ -24,8 +24,8 @@ Usage:
|
|
|
24
24
|
ocx stop Stop the proxy AND restore native Codex (plain codex works again)
|
|
25
25
|
ocx restore Restore native Codex without stopping (alias: eject)
|
|
26
26
|
ocx restore back Re-point codex at the running proxy (undo restore)
|
|
27
|
-
ocx recover-history --legacy-openai
|
|
28
|
-
|
|
27
|
+
ocx recover-history --legacy-openai --yes
|
|
28
|
+
Force all user-message opencodex rows to OpenAI (legacy recovery)
|
|
29
29
|
ocx uninstall Remove service/shim/config and restore native Codex (alias: remove)
|
|
30
30
|
ocx service [sub] Run as a background service (default: install/update/start)
|
|
31
31
|
ocx codex-shim <sub> Auto-start proxy when \`codex\` launches (install|status|uninstall|remove)
|
|
@@ -57,7 +57,8 @@ Usage:
|
|
|
57
57
|
ocx observe <sub> Logs, usage, storage, memory, and debug data
|
|
58
58
|
ocx route <sub> Routing features (combo, policy)
|
|
59
59
|
ocx logs [filters] Alias of ocx observe logs
|
|
60
|
-
ocx usage [--range <7d|30d|all>]
|
|
60
|
+
ocx usage [--range <today|1d|7d|30d|all>] [--provider <name>] [--model <id>]
|
|
61
|
+
Token and estimated-cost report (alias of ocx observe usage)
|
|
61
62
|
ocx storage [--json] Alias of ocx observe storage
|
|
62
63
|
ocx memory [--json] Alias of ocx observe memory
|
|
63
64
|
ocx api-key <sub> Alias of ocx access key
|
package/src/cli/index.ts
CHANGED
|
@@ -12,6 +12,9 @@ import {
|
|
|
12
12
|
codexAutoStartEnabled,
|
|
13
13
|
getConfigDir,
|
|
14
14
|
loadConfig,
|
|
15
|
+
saveConfig,
|
|
16
|
+
} from "../config";
|
|
17
|
+
import {
|
|
15
18
|
readPid,
|
|
16
19
|
readPidFileValue,
|
|
17
20
|
readRuntimePort,
|
|
@@ -19,10 +22,9 @@ import {
|
|
|
19
22
|
removePidIfValueIs,
|
|
20
23
|
removeRuntimePort,
|
|
21
24
|
removeRuntimePortIfPidIs,
|
|
22
|
-
saveConfig,
|
|
23
25
|
writePid,
|
|
24
26
|
writeRuntimePort,
|
|
25
|
-
} from "../config";
|
|
27
|
+
} from "../config/process-state";
|
|
26
28
|
import { collectStatus } from "./status";
|
|
27
29
|
|
|
28
30
|
import {
|
|
@@ -906,8 +908,13 @@ async function handleStatus() {
|
|
|
906
908
|
|
|
907
909
|
async function handleRecoverHistory() {
|
|
908
910
|
if (args[1] !== "--legacy-openai") {
|
|
909
|
-
console.error("Usage: ocx recover-history --legacy-openai");
|
|
910
|
-
console.error("
|
|
911
|
+
console.error("Usage: ocx recover-history --legacy-openai --yes");
|
|
912
|
+
console.error("This force-relabels every user-message opencodex row to OpenAI, including legitimate dedicated-provider history. Back up first and use it only for pre-backup legacy recovery.");
|
|
913
|
+
process.exit(1);
|
|
914
|
+
}
|
|
915
|
+
console.error("WARNING: this force-relabels every user-message opencodex row to OpenAI, normalizes exec to cli, and includes legitimate dedicated-provider history.");
|
|
916
|
+
if (args.length !== 3 || args[2] !== "--yes") {
|
|
917
|
+
console.error("Re-run with explicit confirmation: ocx recover-history --legacy-openai --yes");
|
|
911
918
|
process.exit(1);
|
|
912
919
|
}
|
|
913
920
|
// Manifest-independent legacy ejection, serialized like every other history
|
package/src/cli/models.ts
CHANGED
|
@@ -5,7 +5,12 @@ import { randomUUID } from "node:crypto";
|
|
|
5
5
|
import { createInterface } from "node:readline/promises";
|
|
6
6
|
import { syncModelsToCodex } from "../codex/sync";
|
|
7
7
|
import { hasOwnProvider, isValidProviderName, loadConfig, saveConfig } from "../config";
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
canonicalizeReasoningEfforts,
|
|
10
|
+
configuredReasoningEfforts,
|
|
11
|
+
isDeclaredReasoningEffort,
|
|
12
|
+
modelRecordValue,
|
|
13
|
+
} from "../reasoning-effort";
|
|
9
14
|
import { encodedModelIdCollides, routedSlug, slugEquals } from "../providers/slug-codec";
|
|
10
15
|
import { knownModelIdsForProvider } from "../router";
|
|
11
16
|
import { findLiveProxy } from "../server/proxy-liveness";
|
|
@@ -91,7 +96,6 @@ function collectModels(config: OcxConfig, providerFilter?: string): ModelEntry[]
|
|
|
91
96
|
const seen = new Set<string>();
|
|
92
97
|
const contextWindows = prov.modelContextWindows ?? {};
|
|
93
98
|
const inputModalities = prov.modelInputModalities ?? {};
|
|
94
|
-
const reasoningEfforts = prov.modelReasoningEfforts ?? {};
|
|
95
99
|
const globalContext = prov.contextWindow ?? null;
|
|
96
100
|
|
|
97
101
|
const addModel = (model: string, isDefault: boolean) => {
|
|
@@ -107,7 +111,13 @@ function collectModels(config: OcxConfig, providerFilter?: string): ModelEntry[]
|
|
|
107
111
|
// an exact `gpt-oss:120b` entry that lists "image", and the proxy rejects the image.
|
|
108
112
|
const noVision = modelInList(prov.noVisionModels, model);
|
|
109
113
|
const modalities = noVision ? ["text"] : (modelRecordValue(inputModalities, model) ?? null);
|
|
110
|
-
|
|
114
|
+
// Same reason, for the ladder: `configuredReasoningEfforts` is what the catalog
|
|
115
|
+
// (`provider-fetch`) and the effort cap (`effort-policy`) resolve through, and it
|
|
116
|
+
// does three things this expression did not — it returns [] for a noReasoningModels
|
|
117
|
+
// match, drops levels Codex does not declare, and re-adds tiers the wire map proves
|
|
118
|
+
// the model emits. Restating two of its five lines here reported a ladder the proxy
|
|
119
|
+
// strips, and unsanitized junk as a supported level.
|
|
120
|
+
const efforts = configuredReasoningEfforts(prov, model) ?? null;
|
|
111
121
|
|
|
112
122
|
entries.push({
|
|
113
123
|
provider: provName,
|
package/src/cli/observe.ts
CHANGED
|
@@ -10,6 +10,8 @@ import {
|
|
|
10
10
|
takeOption,
|
|
11
11
|
type RuntimeApiDeps,
|
|
12
12
|
} from "./runtime-api";
|
|
13
|
+
import { formatUsageReport } from "./usage-report";
|
|
14
|
+
import { USAGE_RANGES, USAGE_SURFACES } from "../usage/summary";
|
|
13
15
|
|
|
14
16
|
const USAGE = `Usage:
|
|
15
17
|
ocx observe logs [--provider <name>] [--model <id>] [--status <code>]
|
|
@@ -17,7 +19,8 @@ const USAGE = `Usage:
|
|
|
17
19
|
ocx logs explain <request-id> [--json]
|
|
18
20
|
ocx logs rebuild-index
|
|
19
21
|
ocx logs index-status
|
|
20
|
-
ocx observe usage [--range <7d|30d|all>] [--surface <all|codex|claude|grok>]
|
|
22
|
+
ocx observe usage [--range <today|1d|7d|30d|all>] [--surface <all|codex|claude|grok>]
|
|
23
|
+
[--provider <name>] [--model <id>] [--json]
|
|
21
24
|
ocx observe storage [codex-logs [status|protect|unprotect|repair|compact] [--mode <compat|quiet>]] [--json]
|
|
22
25
|
ocx observe memory [--json]
|
|
23
26
|
ocx observe debug [--json]
|
|
@@ -131,11 +134,23 @@ async function usage(argv: string[], deps: RuntimeApiDeps): Promise<void> {
|
|
|
131
134
|
const wantsJson = takeFlag(args, "--json");
|
|
132
135
|
const range = takeOption(args, "--range") ?? "30d";
|
|
133
136
|
const surface = takeOption(args, "--surface") ?? "all";
|
|
134
|
-
|
|
135
|
-
|
|
137
|
+
const provider = takeOption(args, "--provider");
|
|
138
|
+
const model = takeOption(args, "--model");
|
|
139
|
+
// `1d` is accepted here as well as server-side so the CLI does not reject an
|
|
140
|
+
// alias the API would have understood.
|
|
141
|
+
const ranges = [...USAGE_RANGES, "1d"];
|
|
142
|
+
if (!ranges.includes(range)) throw new CliUsageError(`--range must be one of ${USAGE_RANGES.join(", ")} (1d aliases today)`, USAGE);
|
|
143
|
+
if (!USAGE_SURFACES.includes(surface as (typeof USAGE_SURFACES)[number])) {
|
|
144
|
+
throw new CliUsageError(`--surface must be one of ${USAGE_SURFACES.join(", ")}`, USAGE);
|
|
145
|
+
}
|
|
136
146
|
rejectArgs(args, USAGE);
|
|
137
|
-
const result = await runtimeRequest(`/api/usage${query({ range, surface })}`, {}, deps);
|
|
138
|
-
|
|
147
|
+
const result = await runtimeRequest(`/api/usage${query({ range, surface, provider, model })}`, {}, deps);
|
|
148
|
+
// Built only when it will be printed: JavaScript evaluates arguments before
|
|
149
|
+
// the call, so passing formatUsageReport(...) inline would run the human
|
|
150
|
+
// renderer during --json and let its assumptions affect a path that is meant
|
|
151
|
+
// to bypass it entirely.
|
|
152
|
+
if (wantsJson) printData(result, true);
|
|
153
|
+
else printData(result, false, formatUsageReport(result as Parameters<typeof formatUsageReport>[0]));
|
|
139
154
|
}
|
|
140
155
|
|
|
141
156
|
async function simple(path: string, argv: string[], deps: RuntimeApiDeps): Promise<void> {
|
package/src/cli/provider.ts
CHANGED
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
* show <name> Show provider config details (secrets masked)
|
|
9
9
|
* set-default <name> Change the default provider
|
|
10
10
|
*/
|
|
11
|
-
import {
|
|
11
|
+
import { hasOwnProvider, isValidProviderName, loadConfig, sanitizeModelCostsForDisplay, saveConfig } from "../config";
|
|
12
|
+
import { apiKeyTransportConfigError } from "../config/provider-validation";
|
|
12
13
|
import { hasHelpFlag } from "./help";
|
|
13
14
|
import { getProviderRegistryEntry, PROVIDER_REGISTRY } from "../providers/registry";
|
|
14
15
|
import { providerConfigSeed } from "../providers/derive";
|
package/src/cli/registry.ts
CHANGED
|
@@ -36,8 +36,8 @@ export const CLI_COMMANDS: CliCommandEntry[] = [
|
|
|
36
36
|
},
|
|
37
37
|
{
|
|
38
38
|
name: "recover-history",
|
|
39
|
-
usage: "ocx recover-history --legacy-openai",
|
|
40
|
-
summary: "
|
|
39
|
+
usage: "ocx recover-history --legacy-openai --yes",
|
|
40
|
+
summary: "Force all user-message opencodex rows to OpenAI for legacy recovery.",
|
|
41
41
|
},
|
|
42
42
|
{
|
|
43
43
|
name: "uninstall",
|
|
@@ -88,20 +88,22 @@ export const CLI_COMMANDS: CliCommandEntry[] = [
|
|
|
88
88
|
{ name: "ensure", usage: "ocx ensure", summary: "Ensure the proxy is running and Codex config/cache are current." },
|
|
89
89
|
{
|
|
90
90
|
name: "sync",
|
|
91
|
-
usage: "ocx sync [--restart-codex]",
|
|
91
|
+
usage: "ocx sync [--restart-codex] [--restart-desktop-app]",
|
|
92
92
|
summary: "Fetch provider models and inject them into Codex config.",
|
|
93
93
|
details: [
|
|
94
94
|
"After writing the catalog, warns if long-lived Codex app-server processes are still running.",
|
|
95
95
|
"--restart-codex sends SIGTERM only to matching app-server / code-mode-host processes (may interrupt active turns).",
|
|
96
|
+
"--restart-desktop-app (Windows only, opt-in) fully restarts the Codex desktop app so its model picker re-reads the catalog. Never implied by --restart-codex: it ends live conversations.",
|
|
96
97
|
],
|
|
97
98
|
},
|
|
98
99
|
{
|
|
99
100
|
name: "sync-cache",
|
|
100
|
-
usage: "ocx sync-cache [--restart-codex]",
|
|
101
|
+
usage: "ocx sync-cache [--restart-codex] [--restart-desktop-app]",
|
|
101
102
|
summary: "Refresh Codex's model cache from the active catalog.",
|
|
102
103
|
details: [
|
|
103
104
|
"Warns when Codex app-server processes still hold an in-memory model list.",
|
|
104
105
|
"--restart-codex sends SIGTERM only to matching app-server / code-mode-host processes (may interrupt active turns).",
|
|
106
|
+
"--restart-desktop-app (Windows only, opt-in) fully restarts the Codex desktop app so its model picker re-reads the catalog. Never implied by --restart-codex: it ends live conversations.",
|
|
105
107
|
],
|
|
106
108
|
},
|
|
107
109
|
{ name: "status", usage: "ocx status", summary: "Check proxy server status." },
|
|
@@ -209,7 +211,7 @@ export const CLI_COMMANDS: CliCommandEntry[] = [
|
|
|
209
211
|
{ name: "logs", usage: "ocx logs [filters] [--follow] [--json|--jsonl]", summary: "Alias of ocx observe logs." },
|
|
210
212
|
{
|
|
211
213
|
name: "usage",
|
|
212
|
-
usage: "ocx usage [--range <7d|30d|all>] [--surface <all|codex|claude|grok>] [--json]",
|
|
214
|
+
usage: "ocx usage [--range <today|1d|7d|30d|all>] [--surface <all|codex|claude|grok>] [--provider <name>] [--model <id>] [--json]",
|
|
213
215
|
summary: "Alias of ocx observe usage.",
|
|
214
216
|
},
|
|
215
217
|
{ name: "storage", usage: "ocx storage [--json]", summary: "Alias of ocx observe storage." },
|
package/src/cli/status.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { durableBunRuntime } from "../lib/bun-runtime";
|
|
2
|
-
import { codexAutoStartEnabled, getConfigPath,
|
|
2
|
+
import { codexAutoStartEnabled, getConfigPath, readConfigDiagnostics } from "../config";
|
|
3
|
+
import { getPidPath, readPid, readRuntimePort, type RuntimePortState } from "../config/process-state";
|
|
3
4
|
import { diagnoseCodexBundledPlugins, type CodexPluginsDiagnostic } from "../codex/plugins-doctor";
|
|
4
5
|
import { findLiveProxy, isOpencodexHealthz, probeHostname } from "../server/proxy-liveness";
|
|
5
6
|
import { directLocalHttpFetch } from "../server/direct-local-http";
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Human rendering for `ocx usage`.
|
|
3
|
+
*
|
|
4
|
+
* Kept out of `observe.ts` and away from the shared `summaryLines()` helper on
|
|
5
|
+
* purpose. `summaryLines()` is a generic depth-1 flattener shared with
|
|
6
|
+
* storage/memory/debug/claude-inbound/injection; it renders any array as
|
|
7
|
+
* "N item(s)", which is why every per-model and per-provider cost the server
|
|
8
|
+
* computes used to vanish before reaching the terminal. Deepening it would
|
|
9
|
+
* change five unrelated commands.
|
|
10
|
+
*
|
|
11
|
+
* Formatting follows the existing CLI house style: dynamic `padEnd` columns
|
|
12
|
+
* (as in `formatAccountTable`), plain text, no ANSI colour.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
interface CostRow {
|
|
16
|
+
provider: string;
|
|
17
|
+
model?: string;
|
|
18
|
+
requests: number;
|
|
19
|
+
totalTokens: number;
|
|
20
|
+
estimatedCostUsd?: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface UsageReportInput {
|
|
24
|
+
range?: string;
|
|
25
|
+
surface?: string;
|
|
26
|
+
since?: number | null;
|
|
27
|
+
summary?: {
|
|
28
|
+
requests?: number;
|
|
29
|
+
totalTokens?: number;
|
|
30
|
+
inputTokens?: number;
|
|
31
|
+
outputTokens?: number;
|
|
32
|
+
cachedInputTokens?: number;
|
|
33
|
+
estimatedCostUsd?: number;
|
|
34
|
+
unpricedRequests?: number;
|
|
35
|
+
unmeteredRequests?: number;
|
|
36
|
+
};
|
|
37
|
+
models?: CostRow[];
|
|
38
|
+
providers?: CostRow[];
|
|
39
|
+
days?: { date: string; requests: number; totalTokens: number; estimatedCostUsd?: number }[];
|
|
40
|
+
filter?: { provider: string | null; model: string | null; matched: boolean; comboOverlap: boolean };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const MAX_MODEL_ROWS = 10;
|
|
44
|
+
|
|
45
|
+
function count(value: number | undefined): string {
|
|
46
|
+
return (value ?? 0).toLocaleString("en-US");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Matches the dashboard's `~$` with four fraction digits. Estimates below a
|
|
51
|
+
* hundredth of a cent still read as a number rather than collapsing to $0.00,
|
|
52
|
+
* which matters when a single request is being inspected.
|
|
53
|
+
*/
|
|
54
|
+
function usd(value: number | undefined): string {
|
|
55
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return "—";
|
|
56
|
+
return `~$${value.toFixed(4)}`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function table(header: string[], rows: string[][]): string[] {
|
|
60
|
+
if (rows.length === 0) return [];
|
|
61
|
+
const widths = header.map((h, i) => Math.max(h.length, ...rows.map(r => (r[i] ?? "").length)));
|
|
62
|
+
const line = (cols: string[]): string => cols.map((c, i) => (c ?? "").padEnd(widths[i]!)).join(" ").trimEnd();
|
|
63
|
+
return [line(header), ...rows.map(line)];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function describeScope(data: UsageReportInput): string {
|
|
67
|
+
const parts = [`Usage — ${data.range ?? "?"}`];
|
|
68
|
+
if (data.surface && data.surface !== "all") parts.push(`surface=${data.surface}`);
|
|
69
|
+
if (data.filter?.provider) parts.push(`provider=${data.filter.provider}`);
|
|
70
|
+
if (data.filter?.model) parts.push(`model=${data.filter.model}`);
|
|
71
|
+
return parts.join(", ");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function formatUsageReport(data: UsageReportInput): string[] {
|
|
75
|
+
const summary = data.summary ?? {};
|
|
76
|
+
const lines: string[] = [describeScope(data), ""];
|
|
77
|
+
|
|
78
|
+
if (data.filter && !data.filter.matched) {
|
|
79
|
+
const what = [data.filter.provider && `provider "${data.filter.provider}"`, data.filter.model && `model "${data.filter.model}"`]
|
|
80
|
+
.filter(Boolean).join(" and ");
|
|
81
|
+
lines.push(`No usage recorded for ${what} in this range.`);
|
|
82
|
+
lines.push("Check the spelling against `ocx usage --json`, or widen --range.");
|
|
83
|
+
return lines;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const tokenSplit = [
|
|
87
|
+
summary.inputTokens !== undefined ? `in ${count(summary.inputTokens)}` : null,
|
|
88
|
+
summary.outputTokens !== undefined ? `out ${count(summary.outputTokens)}` : null,
|
|
89
|
+
summary.cachedInputTokens ? `cached ${count(summary.cachedInputTokens)}` : null,
|
|
90
|
+
].filter(Boolean).join(" / ");
|
|
91
|
+
|
|
92
|
+
lines.push(`Requests ${count(summary.requests)}`);
|
|
93
|
+
lines.push(`Tokens ${count(summary.totalTokens)}${tokenSplit ? ` (${tokenSplit})` : ""}`);
|
|
94
|
+
lines.push(`Est. cost ${usd(summary.estimatedCostUsd)} API list-price equivalent (this range)`);
|
|
95
|
+
|
|
96
|
+
const unpriced = summary.unpricedRequests ?? 0;
|
|
97
|
+
const unmetered = summary.unmeteredRequests ?? 0;
|
|
98
|
+
if (unpriced > 0 || unmetered > 0) {
|
|
99
|
+
// Spelled out because a $0 total is ambiguous otherwise: it can mean "no
|
|
100
|
+
// spend" or "no price row matched", and those are very different answers.
|
|
101
|
+
lines.push(` ${count(unpriced)} unpriced, ${count(unmetered)} unmetered excluded from ~$`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const providers = (data.providers ?? []).filter(row => row.requests > 0);
|
|
105
|
+
if (providers.length > 0) {
|
|
106
|
+
lines.push("");
|
|
107
|
+
lines.push(...table(
|
|
108
|
+
["PROVIDER", "REQUESTS", "TOKENS", "EST. COST"],
|
|
109
|
+
providers.map(row => [row.provider, count(row.requests), count(row.totalTokens), usd(row.estimatedCostUsd)]),
|
|
110
|
+
));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const models = (data.models ?? []).filter(row => row.requests > 0);
|
|
114
|
+
if (models.length > 0) {
|
|
115
|
+
lines.push("");
|
|
116
|
+
const shown = models.slice(0, MAX_MODEL_ROWS);
|
|
117
|
+
lines.push(...table(
|
|
118
|
+
["MODEL", "PROVIDER", "REQUESTS", "TOKENS", "EST. COST"],
|
|
119
|
+
shown.map(row => [row.model ?? "-", row.provider, count(row.requests), count(row.totalTokens), usd(row.estimatedCostUsd)]),
|
|
120
|
+
));
|
|
121
|
+
if (models.length > shown.length) {
|
|
122
|
+
lines.push(`... ${models.length - shown.length} more (use --json)`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (data.filter?.comboOverlap) {
|
|
127
|
+
lines.push("");
|
|
128
|
+
lines.push("Some requests ran as combos, so per-model request counts can overlap. Cost does not.");
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
lines.push("");
|
|
132
|
+
lines.push("Not a billing receipt. Subscription usage or provider credits may apply instead.");
|
|
133
|
+
return lines;
|
|
134
|
+
}
|
|
@@ -17,7 +17,8 @@ import {
|
|
|
17
17
|
import { readCodexCatalogPath } from "./catalog/parsing";
|
|
18
18
|
|
|
19
19
|
export const STALE_CODEX_APP_SERVER_HINT =
|
|
20
|
-
"If Codex still shows an older model list, restart its long-lived app-server process after sync (ocx sync --restart-codex)."
|
|
20
|
+
"If Codex still shows an older model list, restart its long-lived app-server process after sync (ocx sync --restart-codex). "
|
|
21
|
+
+ "On Windows the desktop app itself may also need a full restart (ocx sync --restart-desktop-app).";
|
|
21
22
|
|
|
22
23
|
/** Attach the shared dashboard hint only after a catalog or models_cache write. */
|
|
23
24
|
export function attachStaleAppServerHint<T extends {
|
|
@@ -505,6 +506,7 @@ export function formatStaleCodexAppServerWarning(
|
|
|
505
506
|
`WARNING: ${processes.length} Codex app-server process(es) still running (PID${processes.length === 1 ? "" : "s"}: ${pids}). `
|
|
506
507
|
+ "Disk catalog/cache were updated, but Codex may keep showing the old model list until those processes restart. "
|
|
507
508
|
+ "Re-run with `ocx sync --restart-codex` (or `ocx sync-cache --restart-codex`) to send SIGTERM only to matching app-server processes. "
|
|
509
|
+
+ "On Windows the desktop app itself may also need a full restart (`ocx sync --restart-desktop-app`). "
|
|
508
510
|
+ "Active turns may be interrupted."
|
|
509
511
|
);
|
|
510
512
|
}
|
|
@@ -152,10 +152,20 @@ export function deriveComboCatalogModel(
|
|
|
152
152
|
// A combo is cap-limited only when every member defining its effective minimum was
|
|
153
153
|
// itself reduced by a provider cap. An uncapped member at the same minimum means the
|
|
154
154
|
// combo would have the same window even without the cap.
|
|
155
|
-
const contextCapped = limitingMembers.every(member =>
|
|
155
|
+
const contextCapped = limitingMembers.every(member => (
|
|
156
|
+
member.contextCapped === true
|
|
157
|
+
|| (
|
|
158
|
+
typeof member.detectedContextWindow === "number"
|
|
159
|
+
&& typeof member.contextWindow === "number"
|
|
160
|
+
&& member.detectedContextWindow > member.contextWindow
|
|
161
|
+
)
|
|
162
|
+
));
|
|
156
163
|
const maxInputTokens = Math.min(
|
|
157
164
|
...members.map(member => member.maxInputTokens ?? member.contextWindow!),
|
|
158
165
|
);
|
|
166
|
+
const detectedContextWindow = Math.min(
|
|
167
|
+
...limitingMembers.map(member => member.detectedContextWindow ?? member.contextWindow!),
|
|
168
|
+
);
|
|
159
169
|
const defaultReasoningEffort = effectiveComboDefault(
|
|
160
170
|
combo.defaultEffort,
|
|
161
171
|
reasoningEfforts,
|
|
@@ -167,6 +177,8 @@ export function deriveComboCatalogModel(
|
|
|
167
177
|
owned_by: COMBO_NAMESPACE,
|
|
168
178
|
contextWindow,
|
|
169
179
|
maxInputTokens,
|
|
180
|
+
metadataSource: "derived",
|
|
181
|
+
detectedContextWindow,
|
|
170
182
|
...(hasLimitingContextCapMetadata ? { contextCapped } : {}),
|
|
171
183
|
inputModalities,
|
|
172
184
|
reasoningEfforts,
|
|
@@ -33,6 +33,7 @@ import upstreamModelsSnapshot from "../data/upstream-models.json";
|
|
|
33
33
|
|
|
34
34
|
import { generatedModelMetadata, readCatalog, readCodexCatalogPath } from "./parsing";
|
|
35
35
|
import type { CatalogModel, RawEntry } from "./parsing";
|
|
36
|
+
import { shouldStampContextProvenance } from "./model-metadata";
|
|
36
37
|
import { UPSTREAM_NATIVE_ENTRIES } from "./metadata";
|
|
37
38
|
import { nativeOpenAiCapabilitySourceSlug } from "./native-models";
|
|
38
39
|
import { loadBundledCodexCatalog } from "./bundled";
|
|
@@ -171,33 +172,47 @@ export function applyCatalogModelMetadata(entry: RawEntry, model?: CatalogModel)
|
|
|
171
172
|
* live in that table.
|
|
172
173
|
*/
|
|
173
174
|
function stampCapabilityProvenance(entry: RawEntry, model: CatalogModel): void {
|
|
174
|
-
// Virtual combo rows
|
|
175
|
-
//
|
|
176
|
-
//
|
|
177
|
-
//
|
|
178
|
-
if (model.provider === COMBO_NAMESPACE) return;
|
|
175
|
+
// Virtual combo rows without derived member evidence are placeholders
|
|
176
|
+
// (generic 128k / `["text"]`). Stamping those would reintroduce the
|
|
177
|
+
// false-evidence defect this block exists to prevent. Combos that actually
|
|
178
|
+
// min() real member windows carry `metadataSource: "derived"`.
|
|
179
|
+
if (model.provider === COMBO_NAMESPACE && model.metadataSource !== "derived") return;
|
|
179
180
|
|
|
180
181
|
const meta = generatedModelMetadata(model.provider, model.id);
|
|
181
182
|
const metaContext = typeof meta?.contextWindow === "number" && meta.contextWindow > 0
|
|
182
|
-
// The generated context is capped before it reaches the entry, so provenance
|
|
183
|
-
// must apply the same cap or routing would advertise a window the cap refused.
|
|
184
183
|
? applyProviderContextCap(meta.contextWindow, model.contextCap) ?? meta.contextWindow
|
|
185
184
|
: undefined;
|
|
186
|
-
const
|
|
185
|
+
const stampModelContext = shouldStampContextProvenance(model);
|
|
186
|
+
const contextWindow = stampModelContext
|
|
187
187
|
? model.contextWindow
|
|
188
|
-
: metaContext;
|
|
188
|
+
: (model.metadataSource === undefined ? metaContext : undefined);
|
|
189
189
|
const inputModalities = Array.isArray(model.inputModalities) && model.inputModalities.length > 0
|
|
190
190
|
? model.inputModalities
|
|
191
191
|
: (Array.isArray(meta?.input) && meta.input.length > 0 ? meta.input : undefined);
|
|
192
|
+
const maxInputTokens = typeof model.maxInputTokens === "number" && model.maxInputTokens > 0
|
|
193
|
+
? model.maxInputTokens
|
|
194
|
+
: undefined;
|
|
195
|
+
const source = model.metadataSource;
|
|
196
|
+
const stampSource = source === "live" || source === "registry" || source === "snapshot" || source === "derived";
|
|
197
|
+
|
|
198
|
+
if (contextWindow === undefined && inputModalities === undefined
|
|
199
|
+
&& !(Array.isArray(model.capabilities) && model.capabilities.length > 0)) {
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
192
202
|
|
|
193
203
|
entry.opencodex_capability_provenance = {
|
|
194
204
|
provider: model.provider,
|
|
195
205
|
model_id: model.id,
|
|
196
206
|
...(contextWindow !== undefined ? { context_window: contextWindow } : {}),
|
|
207
|
+
...(maxInputTokens !== undefined && stampModelContext ? { max_input_tokens: maxInputTokens } : {}),
|
|
197
208
|
...(inputModalities !== undefined ? { input_modalities: [...inputModalities] } : {}),
|
|
198
209
|
...(Array.isArray(model.capabilities) && model.capabilities.length > 0
|
|
199
210
|
? { capabilities: [...model.capabilities] }
|
|
200
211
|
: {}),
|
|
212
|
+
...(stampSource ? { source } : {}),
|
|
213
|
+
...(typeof model.metadataObservedAt === "string" && stampSource
|
|
214
|
+
? { observed_at: model.metadataObservedAt }
|
|
215
|
+
: {}),
|
|
201
216
|
};
|
|
202
217
|
}
|
|
203
218
|
|