@bitkyc08/opencodex 2.68.0-preview.20260927 → 2.69.0-preview.20260928
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 +25 -17
- package/gui/dist/assets/App-cDPyCb_V.js +51 -0
- package/gui/dist/assets/{Tray-DFnMiyD0.js → Tray-H1IAB8mN.js} +1 -1
- package/gui/dist/assets/index-BF9QnKO_.css +1 -0
- package/gui/dist/assets/index-CI6YTp8z.js +86 -0
- package/gui/dist/assets/{quota-summary-i84eOU3o.js → quota-summary-CutOlPu2.js} +1 -1
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/factory-droid.svg +8 -0
- package/package.json +1 -1
- package/src/adapters/anthropic-output-schema.ts +109 -11
- package/src/adapters/coding-agent/protocol.ts +106 -11
- package/src/adapters/coding-agent/turn.ts +23 -18
- package/src/adapters/google-errors.ts +16 -1
- package/src/adapters/google-http.ts +14 -0
- package/src/adapters/openai-chat/passthrough.ts +2 -0
- package/src/adapters/openai-chat/summary-budget.ts +72 -0
- package/src/adapters/openai-chat/tool-call-id-remint.ts +11 -2
- package/src/adapters/openai-chat.ts +6 -9
- package/src/bridge/response-json.ts +3 -1
- package/src/bridge/sse.ts +3 -1
- package/src/claude/desktop-picker.ts +6 -2
- package/src/claude/desktop-system-proxy.ts +155 -0
- package/src/claude/inbound-model-options.ts +10 -2
- package/src/claude/inbound.ts +1 -1
- package/src/claude/intercept/picker-ca-cleanup.ts +31 -0
- package/src/claude/intercept/picker-ca.ts +283 -83
- package/src/claude/intercept/picker-models.ts +4 -1
- package/src/claude/intercept/picker-trust.ts +15 -2
- package/src/claude/intercept/runtime.ts +99 -32
- package/src/cli/account-api.ts +5 -2
- package/src/cli/account-extended.ts +47 -11
- package/src/cli/account-target.ts +12 -7
- package/src/cli/account.ts +47 -3
- package/src/cli/capabilities.ts +33 -7
- package/src/cli/claude-agent-startup-sync.ts +2 -0
- package/src/cli/claude-desktop.ts +37 -13
- package/src/cli/cross-home-owner.ts +138 -0
- package/src/cli/dispatch.ts +1 -1
- package/src/cli/doctor.ts +18 -8
- package/src/cli/ensure-desired-integrations.ts +5 -0
- package/src/cli/export-command.ts +6 -6
- package/src/cli/help.ts +1 -1
- package/src/cli/index.ts +15 -18
- package/src/cli/integrations.ts +8 -1
- package/src/cli/models-runtime-subcommands.ts +1 -0
- package/src/cli/models-runtime.ts +106 -4
- package/src/cli/models.ts +6 -1
- package/src/cli/opencode.ts +2 -83
- package/src/cli/registry.ts +3 -2
- package/src/cli/status.ts +86 -7
- package/src/client/connect.ts +24 -10
- package/src/client/link-ingress.ts +3 -1
- package/src/client/link-join.ts +76 -9
- package/src/client/link-relay-transport.ts +184 -0
- package/src/client/link-relay.ts +16 -4
- package/src/client/machine-listener.ts +2 -0
- package/src/clients/config-export/constants.ts +8 -0
- package/src/clients/config-export/contracts.ts +13 -1
- package/src/clients/config-export/droid.ts +71 -0
- package/src/clients/config-export/kilo.ts +116 -0
- package/src/clients/config-export.ts +33 -2
- package/src/codex/auth-api/routes.ts +1 -1
- package/src/codex/catalog/access-programs.ts +14 -2
- package/src/codex/catalog/provider-models.ts +1 -1
- package/src/codex/catalog-auto-refresh.ts +122 -0
- package/src/codex/config-drift-heal.ts +80 -0
- package/src/codex/desktop-switches.ts +7 -5
- package/src/codex/inject/bounded-config-reader.ts +69 -0
- package/src/codex/inject/config-toml.ts +31 -3
- package/src/codex/inject/routing-target.ts +6 -1
- package/src/codex/inject.ts +4 -0
- package/src/codex/model-entitlements.ts +32 -5
- package/src/codex/native-profile-startup.ts +17 -5
- package/src/codex/project-config-warnings.ts +72 -21
- package/src/codex/quota-auto-refresh-state.ts +8 -2
- package/src/codex/quota-auto-refresh.ts +80 -9
- package/src/codex/sync.ts +10 -0
- package/src/combos/failover.ts +4 -1
- package/src/combos/jev.ts +15 -4
- package/src/combos/types.ts +16 -0
- package/src/config/diagnostics.ts +19 -1
- package/src/config/load-degrade.ts +29 -0
- package/src/config/macos-system-proxy.ts +114 -0
- package/src/config/proxy-env.ts +67 -20
- package/src/config/schema/config-schema.ts +15 -0
- package/src/config/schema/leaf-validators.ts +23 -0
- package/src/config.ts +2 -0
- package/src/generated/compatibility-version.json +220 -144
- package/src/integrations/catalog-refresh.ts +1 -1
- package/src/integrations/config-io.ts +12 -3
- package/src/integrations/droid-settings.ts +65 -0
- package/src/integrations/kilo-candidates.ts +30 -0
- package/src/integrations/mutation-plan.ts +62 -7
- package/src/integrations/native/ownership-preflight.ts +46 -12
- package/src/integrations/registry.ts +136 -7
- package/src/integrations/state.ts +52 -4
- package/src/integrations/target.ts +3 -0
- package/src/integrations/writer.ts +55 -14
- package/src/lib/account-selection-events.ts +14 -0
- package/src/lib/jsonc.ts +110 -0
- package/src/lib/standalone.ts +9 -1
- package/src/lib/windows-system-proxy.ts +101 -1
- package/src/link/relay-auth.ts +29 -0
- package/src/link/ssh-argv.ts +6 -3
- package/src/link/ssh-runner.ts +5 -5
- package/src/oauth/generic-account-failover.ts +52 -8
- package/src/oauth/index.ts +37 -14
- package/src/oauth/store.ts +96 -11
- package/src/oauth/token-guardian.ts +7 -3
- package/src/oauth/types.ts +2 -0
- package/src/plugins/loader.ts +5 -3
- package/src/providers/fastwire.ts +3 -0
- package/src/providers/key-failover.ts +13 -5
- package/src/providers/kiro-model-catalog.ts +4 -3
- package/src/providers/model-rename-fields.ts +1 -0
- package/src/providers/openai-tiers-destination.ts +9 -0
- package/src/providers/quota/account-cache.ts +17 -3
- package/src/providers/quota/vendor-probes-oauth.ts +2 -0
- package/src/providers/quota.ts +2 -2
- package/src/responses/code-mode-helper-compat.ts +14 -1
- package/src/responses/custom-tool-compat.ts +1 -1
- package/src/server/auth-cors.ts +1 -0
- package/src/server/images.ts +70 -20
- package/src/server/index/link-listener.ts +20 -3
- package/src/server/index/link-relay-sessions.ts +138 -0
- package/src/server/index/optional-listeners.ts +11 -1
- package/src/server/index/serve-options.ts +12 -0
- package/src/server/inference/client-encoder-delivery.ts +1 -0
- package/src/server/local-management-read-client.ts +21 -2
- package/src/server/management/agent-settings-routes.ts +19 -5
- package/src/server/management/combo-routes.ts +5 -3
- package/src/server/management/config-routes.ts +32 -6
- package/src/server/management/model-routes.ts +232 -7
- package/src/server/management/model-rows.ts +199 -1
- package/src/server/management/oauth-account-routes.ts +63 -3
- package/src/server/management/route-registry.ts +2 -0
- package/src/server/port-reclaim.ts +170 -32
- package/src/server/responses/adapter-delivery.ts +6 -4
- package/src/server/responses/adapter-dispatch.ts +72 -2
- package/src/server/responses/core-combo.ts +6 -1
- package/src/server/responses/core-normalize.ts +19 -4
- package/src/server/responses/core-options.ts +2 -0
- package/src/server/responses/memory-models.ts +169 -0
- package/src/server/responses/passthrough-delivery.ts +4 -0
- package/src/server/responses/passthrough-dispatch.ts +10 -0
- package/src/server/responses/request-prepare.ts +86 -4
- package/src/server/responses/request-transport.ts +4 -0
- package/src/server/responses/run-turn-execution.ts +5 -2
- package/src/server/responses/shadow-target-availability.ts +17 -4
- package/src/server/responses/sidecar-execution.ts +18 -2
- package/src/server/responses/skills-snapshot.ts +239 -0
- package/src/server/responses-undeclared-tool-guard.ts +43 -6
- package/src/server/search.ts +1 -1
- package/src/service/guards.ts +4 -5
- package/src/service/state.ts +39 -2
- package/src/service.ts +2 -2
- package/src/types/config.ts +46 -4
- package/src/types/provider.ts +9 -0
- package/src/types/request.ts +6 -0
- package/src/types/tools.ts +38 -8
- package/src/types.ts +3 -0
- package/src/update/async-check.ts +5 -1
- package/src/update/index.ts +30 -12
- package/src/update/job.ts +3 -1
- package/src/update/pnpm-read-policy.d.mts +4 -0
- package/src/update/pnpm-read-policy.mjs +44 -0
- package/src/update/worker-launch.ts +152 -11
- package/src/usage/cost.ts +6 -3
- package/src/usage/log.ts +4 -0
- package/src/web-search/backends.ts +2 -2
- package/src/web-search/devin-executor.ts +1 -1
- package/src/web-search/sidecar-providers.ts +5 -5
- package/gui/dist/assets/App-B5-cAEd2.js +0 -51
- package/gui/dist/assets/index-BDUBS8PW.css +0 -1
- package/gui/dist/assets/index-BcXblvem.js +0 -86
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/** Best-effort discovery of a live proxy named by shared, OpenCodex-managed client state. */
|
|
2
|
+
import { closeSync, constants, fstatSync, openSync, readSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { join, resolve } from "node:path";
|
|
5
|
+
import { getConfigDir } from "../config/paths";
|
|
6
|
+
import { readRuntimePort } from "../config/process-state";
|
|
7
|
+
import { getCodexHome } from "../codex/paths";
|
|
8
|
+
import { detectCodexRoutingDrift } from "../codex/routing-drift";
|
|
9
|
+
import { readBoundedCodexConfig } from "../codex/inject/bounded-config-reader";
|
|
10
|
+
import { currentExternalCodexModelProvider } from "../codex/inject";
|
|
11
|
+
import { reconcileJournal } from "../codex/journal";
|
|
12
|
+
import { markSiblingStart, siblingOfLivePort } from "../codex/sibling-start";
|
|
13
|
+
import { readClientConnectionState } from "../client/state";
|
|
14
|
+
import { findManagedRegion, resolveGrokHome } from "../grok/inject";
|
|
15
|
+
import { providerTableString } from "../codex/injected-marker";
|
|
16
|
+
import { probePortOwner, START_OWNERSHIP_LIVENESS } from "../server/proxy-liveness";
|
|
17
|
+
|
|
18
|
+
const MAX_HINT_BYTES = 256 * 1024;
|
|
19
|
+
// Far above any real Grok config; discovery must not stall startup when Grok sync is off.
|
|
20
|
+
const MAX_GROK_CONFIG_BYTES = 16 * 1024 * 1024;
|
|
21
|
+
|
|
22
|
+
/** Nonblocking open (a FIFO cannot stall startup), regular files only, capped at maxBytes. */
|
|
23
|
+
function readBoundedRegularFile(path: string, maxBytes: number): string | null {
|
|
24
|
+
let fd: number | undefined;
|
|
25
|
+
try {
|
|
26
|
+
fd = openSync(path, constants.O_RDONLY | constants.O_NONBLOCK);
|
|
27
|
+
const stat = fstatSync(fd);
|
|
28
|
+
if (!stat.isFile() || stat.size > maxBytes) return null;
|
|
29
|
+
const bytes = Buffer.alloc(stat.size + 1);
|
|
30
|
+
let count = 0;
|
|
31
|
+
while (count < bytes.length) {
|
|
32
|
+
const next = readSync(fd, bytes, count, bytes.length - count, count);
|
|
33
|
+
if (next === 0) break;
|
|
34
|
+
count += next;
|
|
35
|
+
}
|
|
36
|
+
return count > maxBytes || count > stat.size ? null : bytes.toString("utf8", 0, count);
|
|
37
|
+
} catch {
|
|
38
|
+
return null;
|
|
39
|
+
} finally {
|
|
40
|
+
if (fd !== undefined) closeSync(fd);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function validPort(value: unknown): value is number {
|
|
45
|
+
return Number.isInteger(value) && Number(value) > 0 && Number(value) <= 65535;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function loopbackPort(raw: string | null): number | null {
|
|
49
|
+
if (!raw) return null;
|
|
50
|
+
try {
|
|
51
|
+
const url = new URL(raw);
|
|
52
|
+
if (!url.port || !["http:", "https:"].includes(url.protocol)) return null;
|
|
53
|
+
const host = url.hostname.replace(/^\[|\]$/g, "").toLowerCase();
|
|
54
|
+
if (!["127.0.0.1", "localhost", "::1"].includes(host)) return null;
|
|
55
|
+
const port = Number(url.port);
|
|
56
|
+
return validPort(port) ? port : null;
|
|
57
|
+
} catch {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Returns only a different process with an identity-checked /healthz response. */
|
|
63
|
+
export async function findCrossHomeOwner(options: { homeDir?: string } = {}): Promise<number | null> {
|
|
64
|
+
const candidates = new Set<number>();
|
|
65
|
+
const defaultHome = join(options.homeDir ?? homedir(), ".opencodex");
|
|
66
|
+
if (resolve(getConfigDir()) !== resolve(defaultHome)) {
|
|
67
|
+
const raw = readBoundedRegularFile(join(defaultHome, "runtime-port.json"), MAX_HINT_BYTES);
|
|
68
|
+
if (raw) {
|
|
69
|
+
try {
|
|
70
|
+
const record: unknown = JSON.parse(raw);
|
|
71
|
+
if (record && typeof record === "object" && validPort((record as { port?: unknown }).port)) {
|
|
72
|
+
candidates.add((record as { port: number }).port);
|
|
73
|
+
}
|
|
74
|
+
} catch { /* stale or malformed hint */ }
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Grok's writer reads its config in full; cap discovery separately so startup stays bounded.
|
|
79
|
+
const grok = readBoundedRegularFile(join(resolveGrokHome(), "config.toml"), MAX_GROK_CONFIG_BYTES);
|
|
80
|
+
const region = grok === null ? null : findManagedRegion(grok);
|
|
81
|
+
if (grok && region && !region.orphaned) {
|
|
82
|
+
const port = loopbackPort(providerTableString(grok.slice(region.start, region.end), "opencodex", "base_url"));
|
|
83
|
+
if (port !== null) candidates.add(port);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
const codex = readBoundedCodexConfig(join(getCodexHome(), "config.toml"));
|
|
88
|
+
if (codex) {
|
|
89
|
+
const drift = detectCodexRoutingDrift(codex, { ownPorts: [] });
|
|
90
|
+
if (drift.kind === "foreign") {
|
|
91
|
+
for (const target of drift.targets) {
|
|
92
|
+
// Every drift target is owned, loopback, and has an explicit port (including Design B roots).
|
|
93
|
+
candidates.add(target.port);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
} catch { /* an absent or invalid client home is not owner evidence */ }
|
|
98
|
+
|
|
99
|
+
for (const port of candidates) {
|
|
100
|
+
const owner = await probePortOwner(port, {}, START_OWNERSHIP_LIVENESS);
|
|
101
|
+
if (owner && Number.isSafeInteger(owner.pid) && owner.pid! > 0 && owner.pid !== process.pid) return port;
|
|
102
|
+
}
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Mark this process before any shared-client write when another home owns the clients. */
|
|
107
|
+
export async function markCrossHomeSibling(): Promise<boolean> {
|
|
108
|
+
const port = await findCrossHomeOwner();
|
|
109
|
+
if (port === null) return false;
|
|
110
|
+
markSiblingStart(port);
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** A live proxy in this home can publish its sibling owner even while that owner is down. */
|
|
115
|
+
export async function markLiveHomeSibling(live: { pid: number | null; port: number }): Promise<boolean> {
|
|
116
|
+
const runtime = readRuntimePort();
|
|
117
|
+
if (live.pid !== null && runtime?.pid === live.pid && runtime.port === live.port
|
|
118
|
+
&& runtime.siblingOfPort !== undefined && runtime.siblingOfPort !== live.port) {
|
|
119
|
+
markSiblingStart(runtime.siblingOfPort);
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
122
|
+
const otherPort = await findCrossHomeOwner();
|
|
123
|
+
if (otherPort === null || otherPort === live.port) return false;
|
|
124
|
+
markSiblingStart(otherPort);
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Recovery follows the full owner decision; a sibling never replays another home's journal.
|
|
130
|
+
* A marked sibling's owner can be down mid-restart; its journal is still not ours to replay.
|
|
131
|
+
*/
|
|
132
|
+
export function reconcileStartupJournal(): void {
|
|
133
|
+
if (currentExternalCodexModelProvider() || siblingOfLivePort() !== null) return;
|
|
134
|
+
const clientState = readClientConnectionState();
|
|
135
|
+
reconcileJournal(clientState.kind === "connected"
|
|
136
|
+
? { activeClientApiKeyId: clientState.value.apiKeyId }
|
|
137
|
+
: undefined);
|
|
138
|
+
}
|
package/src/cli/dispatch.ts
CHANGED
|
@@ -512,7 +512,7 @@ const commandRunners: Record<string, CommandRunner> = {
|
|
|
512
512
|
},
|
|
513
513
|
config,
|
|
514
514
|
port: live.port,
|
|
515
|
-
}, ["mcode", "pi", "raycast", "omo", "cline"]));
|
|
515
|
+
}, ["mcode", "pi", "raycast", "omo", "cline", "droid"]));
|
|
516
516
|
} catch (error) {
|
|
517
517
|
console.warn(`Client integrations were not refreshed: ${error instanceof Error ? error.message : String(error)}`);
|
|
518
518
|
}
|
package/src/cli/doctor.ts
CHANGED
|
@@ -12,7 +12,7 @@ import { homedir } from "node:os";
|
|
|
12
12
|
import { dirname, join } from "node:path";
|
|
13
13
|
import { getConfigDir, getConfigPath, readConfigDiagnostics } from "../config";
|
|
14
14
|
import { readPid } from "../config/process-state";
|
|
15
|
-
import { probeUncleanExitState } from "./status";
|
|
15
|
+
import { fetchLiveStartupHealth, probeUncleanExitState, selectStatusStartupHealth } from "./status";
|
|
16
16
|
import { findLiveProxy, probeHostname, type LiveProxy } from "../server/proxy-liveness";
|
|
17
17
|
import { directLocalHttpFetch } from "../server/direct-local-http";
|
|
18
18
|
import { BUN_RUNTIME_SOURCES } from "../lib/bun-runtime";
|
|
@@ -61,6 +61,7 @@ import {
|
|
|
61
61
|
formatLegacyCodexConfigKeyDiagnosticsForDoctor,
|
|
62
62
|
} from "../codex/legacy-config-keys";
|
|
63
63
|
import { collectStartupHealth, formatStartupRoutingDetail, startupHealthSummary } from "../codex/autostart-health";
|
|
64
|
+
import { collectDesktopSystemProxy, formatDesktopSystemProxyLines } from "../claude/desktop-system-proxy";
|
|
64
65
|
import {
|
|
65
66
|
displayCodexRuntimePath,
|
|
66
67
|
effortClampAppliesToRuntime,
|
|
@@ -1354,7 +1355,14 @@ export async function runDoctor(args: string[] = []): Promise<void> {
|
|
|
1354
1355
|
diagnoseCodexShim(),
|
|
1355
1356
|
serviceTokenPresent,
|
|
1356
1357
|
);
|
|
1357
|
-
|
|
1358
|
+
// Use the same attested live startup verdict as `ocx status` when the proxy is already
|
|
1359
|
+
// identity-verified. A shell-local systemd probe can be a false negative for a system-wide
|
|
1360
|
+
// service because the shell does not inherit the manager-owned environment.
|
|
1361
|
+
const live = await findLiveProxy({
|
|
1362
|
+
configFn: () => ({ port: doctorConfig.port, hostname: doctorConfig.hostname }),
|
|
1363
|
+
});
|
|
1364
|
+
const liveStartup = live ? await fetchLiveStartupHealth(live) : null;
|
|
1365
|
+
const startup = selectStatusStartupHealth(liveStartup, () => collectStartupHealth(doctorConfig));
|
|
1358
1366
|
console.log("\nCodex restart safety");
|
|
1359
1367
|
console.log(` ${startup.rebootSafe ? "ok " : "!! "} ${startupHealthSummary(startup)}`);
|
|
1360
1368
|
console.log(` ${formatStartupRoutingDetail(startup)}`);
|
|
@@ -1393,12 +1401,6 @@ export async function runDoctor(args: string[] = []): Promise<void> {
|
|
|
1393
1401
|
}
|
|
1394
1402
|
}
|
|
1395
1403
|
|
|
1396
|
-
// #618: identity-verified liveness first so pid-file absence does not hide a live service.
|
|
1397
|
-
// Reuse the diagnostics config already loaded above so doctor stays read-only on malformed JSON.
|
|
1398
|
-
const live = await findLiveProxy({
|
|
1399
|
-
configFn: () => ({ port: doctorConfig.port, hostname: doctorConfig.hostname }),
|
|
1400
|
-
});
|
|
1401
|
-
|
|
1402
1404
|
// Mirrors `ocx status` through the same comparison rather than a second implementation:
|
|
1403
1405
|
// two diagnostics disagreeing about whether an install is stale is worse than one (#2701).
|
|
1404
1406
|
// No extra probe -- findLiveProxy already carried the version back.
|
|
@@ -1427,6 +1429,14 @@ export async function runDoctor(args: string[] = []): Promise<void> {
|
|
|
1427
1429
|
console.log("\nConfigured proxy (value hidden)");
|
|
1428
1430
|
console.log(` ${configuredProxy.present ? "set " : "unset "} ${configuredProxy.key} (${configuredProxy.source}; ${configuredProxy.detail})`);
|
|
1429
1431
|
|
|
1432
|
+
// Observe-only and warning-level: a bypassed Desktop first-party install still serves the
|
|
1433
|
+
// CLI and every non-Desktop client, so this never records a doctor failure.
|
|
1434
|
+
const desktopSystemProxyLines = formatDesktopSystemProxyLines(collectDesktopSystemProxy(doctorConfig));
|
|
1435
|
+
if (desktopSystemProxyLines.length > 0) {
|
|
1436
|
+
console.log("\nClaude Desktop first-party vs Windows system proxy");
|
|
1437
|
+
for (const line of desktopSystemProxyLines) console.log(line);
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1430
1440
|
const providerApiKeys = collectProviderApiKeyDiagnostics(doctorConfig.providers);
|
|
1431
1441
|
console.log("\nProvider API keys (value hidden)");
|
|
1432
1442
|
if (providerApiKeys.length === 0) {
|
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
shouldSyncGrokOnStart,
|
|
31
31
|
} from "../codex/desired-state";
|
|
32
32
|
import type { OcxConfig } from "../types";
|
|
33
|
+
import { siblingOfLivePort, siblingSkipMessage } from "../codex/sibling-start";
|
|
33
34
|
|
|
34
35
|
export function grokSyncFailureMessage(err: unknown): string {
|
|
35
36
|
const detail = err instanceof Error ? err.message : String(err);
|
|
@@ -101,6 +102,10 @@ export async function ensureGrokFenceMatchesDesired(
|
|
|
101
102
|
): Promise<void> {
|
|
102
103
|
const config = deps.loadConfig();
|
|
103
104
|
const { log, error } = io(deps);
|
|
105
|
+
if (siblingOfLivePort() !== null) {
|
|
106
|
+
log(` ${siblingSkipMessage()} ~/.grok/config.toml was left exactly as it is.`);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
104
109
|
// A hub-gated skip is NOT "the user turned Grok off" (#4236). Stripping the managed block
|
|
105
110
|
// there deleted a fence the operator still wants — and `ocx ensure` reported it as the
|
|
106
111
|
// Grok toggle doing its job. Only an explicit OFF authorizes the strip; the gate just
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `ocx export --client <id>` — print a client config for the live proxy.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* this comment: OpenCode, Pi, Prime, Aside, ZCode and
|
|
4
|
+
* The accepted clients span five formats. The accepted list is `EXPORT_CLIENT_IDS`, not
|
|
5
|
+
* this comment: OpenCode, Pi, Prime, Aside, ZCode, omo and Kilo are JSON; OMP,
|
|
6
6
|
* Hermes, gjc, DSH, MiniMax Code and Raycast are YAML; OpenClaw is JSON5; Kimi
|
|
7
7
|
* is TOML.
|
|
8
8
|
*
|
|
@@ -159,16 +159,16 @@ export async function handleExportCommand(argv: string[], deps: ExportCommandDep
|
|
|
159
159
|
const spec = EXPORT_CLIENTS[client];
|
|
160
160
|
const root = await runtimeBaseUrl(deps);
|
|
161
161
|
let built: { document: unknown; text: string };
|
|
162
|
-
if (client === "raycast") {
|
|
162
|
+
if (client === "raycast" || client === "droid") {
|
|
163
163
|
// The dial address alone cannot distinguish a wildcard authenticated bind
|
|
164
164
|
// from loopback. Let the live server resolve its admission/listener policy;
|
|
165
165
|
// saved config can differ from the process serving this request.
|
|
166
166
|
const exported = await runtimeRequest<{
|
|
167
167
|
client: string; format: string; config: unknown; text: string;
|
|
168
|
-
}>(
|
|
169
|
-
if (!exported || exported.client !==
|
|
168
|
+
}>(`/api/client-config?client=${client}`, {}, { ...deps, baseUrl: root });
|
|
169
|
+
if (!exported || exported.client !== client || exported.format !== spec.format
|
|
170
170
|
|| typeof exported.text !== "string" || exported.config === undefined) {
|
|
171
|
-
throw new RuntimeApiError(
|
|
171
|
+
throw new RuntimeApiError(`Management API returned an unexpected ${client} export payload.`, 502, null);
|
|
172
172
|
}
|
|
173
173
|
built = { document: exported.config, text: exported.text };
|
|
174
174
|
} else {
|
package/src/cli/help.ts
CHANGED
|
@@ -83,7 +83,7 @@ Usage:
|
|
|
83
83
|
ocx api-key <sub> Alias of ocx access key
|
|
84
84
|
ocx access <sub> External API keys and endpoint information
|
|
85
85
|
ocx api <sub> Protocol paths: vocabulary, request-path preview, and policy
|
|
86
|
-
ocx export --client <id> Print a client config wired to the running proxy (
|
|
86
|
+
ocx export --client <id> Print a client config wired to the running proxy (17 clients)
|
|
87
87
|
ocx integration client <sub> Enable, disable, inspect or roll back a client integration
|
|
88
88
|
ocx grok <sub> Grok Build model selection and apply
|
|
89
89
|
ocx system <sub> Runtime settings, startup, sync, OpenCodex updates, and Codex CLI inspection
|
package/src/cli/index.ts
CHANGED
|
@@ -38,7 +38,7 @@ import {
|
|
|
38
38
|
resolveCodexHistoryJobTarget,
|
|
39
39
|
runCodexHistoryJob,
|
|
40
40
|
} from "../codex/history-job";
|
|
41
|
-
import {
|
|
41
|
+
import { findCrossHomeOwner, markCrossHomeSibling, markLiveHomeSibling, reconcileStartupJournal } from "./cross-home-owner";
|
|
42
42
|
import { inspectClientRotationRecoveryGate, readClientConnectionState } from "../client/state";
|
|
43
43
|
import {
|
|
44
44
|
codexAutoStartEnabled,
|
|
@@ -355,24 +355,15 @@ async function findProxyOwnerBeforeJournalRecovery(
|
|
|
355
355
|
const pidSnapshot = readPidFileValue();
|
|
356
356
|
const hasRuntimeOwner = readRuntimePort() !== null;
|
|
357
357
|
const shouldProbe = pidSnapshot !== null || hasRuntimeOwner || options.probeConfiguredPort === true;
|
|
358
|
-
// A negative answer
|
|
359
|
-
//
|
|
360
|
-
//
|
|
358
|
+
// A negative answer lets the caller walk past a proxy it was supposed to find and
|
|
359
|
+
// deletes this home's stale pid record. Journal recovery follows cross-home discovery.
|
|
360
|
+
// One 750ms probe is not enough evidence for that (#5004) — a transport
|
|
361
361
|
// failure is indistinguishable from an empty port, and the reported Windows duplicate
|
|
362
362
|
// came from exactly that answer on a proxy the previous command had just found healthy.
|
|
363
363
|
const live = shouldProbe ? await findLiveProxy(START_OWNERSHIP_LIVENESS) : null;
|
|
364
364
|
if (live) return { live, pidSnapshot };
|
|
365
365
|
|
|
366
|
-
// The probe established that the snapshotted owner is stale. Compare before
|
|
367
|
-
// deleting so a concurrent start that rewrote the PID file keeps its state.
|
|
368
366
|
removePidIfValueIs(pidSnapshot);
|
|
369
|
-
// A marked sibling's owner can be down mid-restart; its journal is still not ours to replay.
|
|
370
|
-
if (!currentExternalCodexModelProvider() && siblingOfLivePort() === null) {
|
|
371
|
-
const clientState = readClientConnectionState();
|
|
372
|
-
reconcileJournal(clientState.kind === "connected"
|
|
373
|
-
? { activeClientApiKeyId: clientState.value.apiKeyId }
|
|
374
|
-
: undefined);
|
|
375
|
-
}
|
|
376
367
|
return { live: null, pidSnapshot };
|
|
377
368
|
}
|
|
378
369
|
|
|
@@ -413,8 +404,7 @@ async function handleStart(options: { block?: boolean } = {}) {
|
|
|
413
404
|
}
|
|
414
405
|
}
|
|
415
406
|
const requestedPort = startOpts.port;
|
|
416
|
-
// Probe the configured port even without state files: a fallback sibling can remove them
|
|
417
|
-
// and an unprobed start could shadow the owner and reroute Codex to a short-lived port.
|
|
407
|
+
// Probe the configured port even without state files: a fallback sibling can remove them.
|
|
418
408
|
// Consume a sibling replacement's handoff before probing, even if its owner is momentarily down.
|
|
419
409
|
let siblingStart = honorSiblingMarker(process.env, consumeSiblingHandoff) !== null;
|
|
420
410
|
// A restart replacement waits out its draining parent instead of refusing it, and bounds its handoff log (restart-handoff.ts).
|
|
@@ -448,6 +438,8 @@ async function handleStart(options: { block?: boolean } = {}) {
|
|
|
448
438
|
+ `Startup continues only for an independent OPENCODEX_HOME; one state directory has one spend-ledger writer.`,
|
|
449
439
|
);
|
|
450
440
|
}
|
|
441
|
+
if (!owner.live && !siblingStart) siblingStart = await markCrossHomeSibling();
|
|
442
|
+
if (!siblingStart) reconcileStartupJournal();
|
|
451
443
|
|
|
452
444
|
const clientState = readClientConnectionState();
|
|
453
445
|
if (clientState.kind === "invalid" || clientState.kind === "mismatched") {
|
|
@@ -504,6 +496,7 @@ async function handleStart(options: { block?: boolean } = {}) {
|
|
|
504
496
|
siblingStart = true;
|
|
505
497
|
markSiblingStart(fencedLive.port);
|
|
506
498
|
}
|
|
499
|
+
if (!fencedLive && !siblingStart) siblingStart = await markCrossHomeSibling();
|
|
507
500
|
|
|
508
501
|
// Port selection is check-then-bind. The lease prevents every cooperating start or
|
|
509
502
|
// updater from turning that check into a different ownership decision.
|
|
@@ -733,6 +726,7 @@ async function handleStart(options: { block?: boolean } = {}) {
|
|
|
733
726
|
// to explain it. Name the failure and the one command that repairs it.
|
|
734
727
|
console.error(`⚠️ ${grokSyncFailureMessage(err)}`);
|
|
735
728
|
}
|
|
729
|
+
console.log("Client startup work complete.");
|
|
736
730
|
if (options.block ?? true) {
|
|
737
731
|
setInterval(() => {}, 60_000);
|
|
738
732
|
await new Promise<void>(() => {});
|
|
@@ -749,6 +743,9 @@ function detachedStartEnvironment(): NodeJS.ProcessEnv {
|
|
|
749
743
|
|
|
750
744
|
async function handleEnsure(options: { existingIsSuccess?: boolean } = {}): Promise<boolean> {
|
|
751
745
|
const owner = await findProxyOwnerBeforeJournalRecovery({ probeConfiguredPort: true });
|
|
746
|
+
if (!owner.live && !(await markCrossHomeSibling())) reconcileStartupJournal();
|
|
747
|
+
// A later ensure can find this home's sibling alive; the other port still owns shared clients.
|
|
748
|
+
if (owner.live) await markLiveHomeSibling(owner.live);
|
|
752
749
|
const config = loadConfig();
|
|
753
750
|
if (!codexAutoStartEnabled(config)) {
|
|
754
751
|
console.log("Codex autostart is disabled.");
|
|
@@ -772,13 +769,13 @@ async function handleEnsure(options: { existingIsSuccess?: boolean } = {}): Prom
|
|
|
772
769
|
// owns catalog refresh; ensure must not overwrite a working destination.
|
|
773
770
|
// Ensure env file exists for already-running proxy (may have been deleted or pre-dates this feature).
|
|
774
771
|
const systemEnv = await injectSystemEnv(live.port, config).catch(() => ({ injected: false }));
|
|
775
|
-
reportShellHookFailure(reconcileShellHook(systemEnv.injected));
|
|
772
|
+
if (siblingOfLivePort() === null) reportShellHookFailure(reconcileShellHook(systemEnv.injected));
|
|
776
773
|
if (!systemEnv.injected) await syncClaudeAgentDefsAtProxyStartup(config, live.port);
|
|
777
774
|
// Refresh the Grok Build fence too (same contract as start). live.hostname is the
|
|
778
775
|
// hostname the running proxy actually bound — config.hostname may have drifted.
|
|
779
776
|
// The reconciler re-reads immediately before each client-file mutation; only
|
|
780
777
|
// the live proxy's observed bind host is safe to carry across this boundary.
|
|
781
|
-
await reconcileEnsureDesiredIntegrations(
|
|
778
|
+
if (siblingOfLivePort() === null) await reconcileEnsureDesiredIntegrations(
|
|
782
779
|
live.port,
|
|
783
780
|
{ kind: "live", hostname: live.hostname },
|
|
784
781
|
);
|
|
@@ -806,7 +803,7 @@ async function handleEnsure(options: { existingIsSuccess?: boolean } = {}): Prom
|
|
|
806
803
|
// responds — align here too so `ocx ensure` never returns with a stale ON/OFF mismatch.
|
|
807
804
|
// Persisted state is loaded inside each mutation after waitForProxy, so a
|
|
808
805
|
// toggle while the child starts wins over the pre-spawn snapshot.
|
|
809
|
-
await reconcileEnsureDesiredIntegrations(port, { kind: "spawned" });
|
|
806
|
+
if (siblingOfLivePort() === null) await reconcileEnsureDesiredIntegrations(port, { kind: "spawned" });
|
|
810
807
|
// Always sync the LIVE port: after a fallback-port start, config.port still names the
|
|
811
808
|
// busy preferred port — syncing that would point Codex at a dead listener.
|
|
812
809
|
const synced = await syncModelsToCodex(port).catch(e => {
|
package/src/cli/integrations.ts
CHANGED
|
@@ -126,7 +126,14 @@ export async function handleClaudeConfigCommand(argv: string[], deps: RuntimeApi
|
|
|
126
126
|
}
|
|
127
127
|
if (Object.keys(body).length === 0) throw new CliUsageError("at least one Claude setting is required", CLAUDE_USAGE);
|
|
128
128
|
const result = await runtimeRequest("/api/claude-code", { method: "PUT", body: JSON.stringify(body) }, deps);
|
|
129
|
-
|
|
129
|
+
const warnings = (result as { warnings?: unknown }).warnings;
|
|
130
|
+
const retained = Array.isArray(warnings) && warnings.includes("shared_proxy_retained");
|
|
131
|
+
printData(result, wantsJson, [
|
|
132
|
+
"Claude Code settings updated.",
|
|
133
|
+
// The route kept the shared proxy env because Claude Desktop may still rely on it. Say so,
|
|
134
|
+
// or an operator who turned first-party off believes the local interception is gone.
|
|
135
|
+
...(retained ? ["Warning: Claude Desktop still uses the shared proxy settings. Run `ocx claude desktop apply --gateway` to release them."] : []),
|
|
136
|
+
]);
|
|
130
137
|
});
|
|
131
138
|
}
|
|
132
139
|
|
|
@@ -18,6 +18,7 @@ import { isValidProviderName } from "../config/provider-name";
|
|
|
18
18
|
import { isValidModelDiscoveryModelId } from "../providers/model-discovery-limits";
|
|
19
19
|
import { redactSecretString } from "../lib/redact";
|
|
20
20
|
import type { ProviderCostOverlay } from "../types";
|
|
21
|
+
import { resolveMatchedPrice } from "../usage/cost";
|
|
21
22
|
import { MAX_COST4_RATE } from "../usage/expected-prices";
|
|
22
23
|
import { isValidCost4Rate } from "../usage/user-cost-overlays";
|
|
23
24
|
|
|
@@ -30,6 +31,10 @@ const USAGE = `Usage:
|
|
|
30
31
|
[--context-window <tokens|0>] [--modalities <text,image,audio|->]
|
|
31
32
|
[--reasoning-efforts <none,minimal,low,medium,high,xhigh,max,ultra|->]
|
|
32
33
|
[--default-reasoning-effort <level|->] [--json]
|
|
34
|
+
ocx models set <provider/model> [--context-window <tokens|0|->]
|
|
35
|
+
[--modalities <text,image,audio|->]
|
|
36
|
+
[--reasoning-efforts <none,minimal,low,medium,high,xhigh,max,ultra|"">|->]
|
|
37
|
+
[--default-reasoning-effort <level|->] [--reset] [--json]
|
|
33
38
|
ocx models <enable|disable> <provider/model|native-model> [--native] [--json]
|
|
34
39
|
ocx models provider <name> <on|off> [--json]
|
|
35
40
|
ocx models selected <provider> [--set <id,id...>|--clear] [--json]
|
|
@@ -124,8 +129,12 @@ async function priceRequest(write: boolean, argv: string[], deps: RuntimeApiDeps
|
|
|
124
129
|
if (!validPriceCost(stored)) throw new Error("Invalid model price response");
|
|
125
130
|
cost = { ...stored };
|
|
126
131
|
}
|
|
127
|
-
|
|
128
|
-
|
|
132
|
+
// The API map owns manual overrides; bundled defaults remain derived rather
|
|
133
|
+
// than being persisted as overrides that would mask later catalog updates.
|
|
134
|
+
const effectiveCost = cost ?? resolveMatchedPrice(provider, modelId, undefined, [])?.cost4 ?? null;
|
|
135
|
+
printData({ provider, modelId, cost, effectiveCost }, wantsJson, [
|
|
136
|
+
effectiveCost === null ? `${selector}: automatic pricing (unknown)`
|
|
137
|
+
: `${selector}: ${JSON.stringify(effectiveCost)} USD per 1M tokens${cost === null ? " (automatic estimate)" : ""}`,
|
|
129
138
|
]);
|
|
130
139
|
return;
|
|
131
140
|
}
|
|
@@ -199,7 +208,7 @@ async function edit(argv: string[], deps: RuntimeApiDeps): Promise<void> {
|
|
|
199
208
|
if (displayName !== undefined) patch.displayName = displayName === "-" ? "" : displayName;
|
|
200
209
|
if (contextRaw !== undefined) {
|
|
201
210
|
const value = Number(contextRaw.replace(/[_,]/g, ""));
|
|
202
|
-
if (!Number.
|
|
211
|
+
if (!Number.isSafeInteger(value) || value < 0) throw new CliUsageError("--context-window must be a safe integer >= 0", USAGE);
|
|
203
212
|
patch.contextWindow = value === 0 ? null : value;
|
|
204
213
|
}
|
|
205
214
|
if (modalitiesRaw !== undefined) patch.inputModalities = modalitiesRaw === "-" ? [] : csv(modalitiesRaw);
|
|
@@ -240,6 +249,98 @@ async function edit(argv: string[], deps: RuntimeApiDeps): Promise<void> {
|
|
|
240
249
|
printData(result, wantsJson, [`Updated custom model ${id}.`]);
|
|
241
250
|
}
|
|
242
251
|
|
|
252
|
+
/**
|
|
253
|
+
* Per-model overrides on a routed row: the twin of `ocx models edit` for rows that already
|
|
254
|
+
* exist, rather than for a custom definition.
|
|
255
|
+
*
|
|
256
|
+
* Every option has a clear spelling instead of an empty-value sentinel, because the API treats
|
|
257
|
+
* `null` as "hand this fact back to the registry, the catalog and the provider default". That is
|
|
258
|
+
* a different intent from storing a value that merely looks blank, and collapsing the two is how
|
|
259
|
+
* an override outlives the data it was copied from.
|
|
260
|
+
*/
|
|
261
|
+
async function setModelSettings(argv: string[], deps: RuntimeApiDeps): Promise<void> {
|
|
262
|
+
const args = [...argv];
|
|
263
|
+
const selector = args.shift()?.trim();
|
|
264
|
+
const wantsJson = takeFlag(args, "--json");
|
|
265
|
+
const reset = takeFlag(args, "--reset");
|
|
266
|
+
if (!selector) throw new CliUsageError("model selector is required", USAGE);
|
|
267
|
+
const contextRaw = takeOption(args, "--context-window");
|
|
268
|
+
const modalitiesRaw = takeOption(args, "--modalities");
|
|
269
|
+
const reasoningEffortsRaw = takeOption(args, "--reasoning-efforts");
|
|
270
|
+
const defaultEffortRaw = takeOption(args, "--default-reasoning-effort");
|
|
271
|
+
rejectArgs(args, USAGE);
|
|
272
|
+
const target = parseSelector(selector, false);
|
|
273
|
+
if (target.native) {
|
|
274
|
+
throw new CliUsageError(
|
|
275
|
+
"model settings address a routed model; use provider/model (the native openai lane has no per-model overrides)",
|
|
276
|
+
USAGE,
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
const patch: Record<string, unknown> = {};
|
|
280
|
+
if (contextRaw !== undefined) {
|
|
281
|
+
const raw = contextRaw.trim();
|
|
282
|
+
if (raw === "-") patch.contextWindow = null;
|
|
283
|
+
else {
|
|
284
|
+
const value = Number(raw.replace(/[_,]/g, ""));
|
|
285
|
+
if (!Number.isSafeInteger(value) || value < 0) {
|
|
286
|
+
throw new CliUsageError("--context-window must be a safe integer >= 0 (0 or - clears the override)", USAGE);
|
|
287
|
+
}
|
|
288
|
+
patch.contextWindow = value === 0 ? null : value;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
if (modalitiesRaw !== undefined) {
|
|
292
|
+
// "-" is the one spelling that clears. A blank value or a blank member would otherwise
|
|
293
|
+
// become [] after CSV normalization, which the server also reads as "clear".
|
|
294
|
+
const trimmed = modalitiesRaw.trim();
|
|
295
|
+
if (trimmed === "-") patch.inputModalities = null;
|
|
296
|
+
else {
|
|
297
|
+
const members = trimmed.split(",").map(item => item.trim());
|
|
298
|
+
if (members.some(item => !["text", "image", "audio"].includes(item))) {
|
|
299
|
+
throw new CliUsageError("--modalities must list text, image or audio (comma-separated), or - to clear", USAGE);
|
|
300
|
+
}
|
|
301
|
+
patch.inputModalities = [...new Set(members)];
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
if (reasoningEffortsRaw !== undefined) {
|
|
305
|
+
// "-" restores inheritance by clearing the stored ladder (null); "" stores an explicit empty
|
|
306
|
+
// ladder, which is the "this model does not reason" override. Embedded blank CSV members
|
|
307
|
+
// (`low,,high`) are malformed and rejected rather than normalized away.
|
|
308
|
+
const trimmed = reasoningEffortsRaw.trim();
|
|
309
|
+
if (trimmed === "-") {
|
|
310
|
+
patch.reasoningEfforts = null;
|
|
311
|
+
} else if (trimmed === "") {
|
|
312
|
+
patch.reasoningEfforts = [];
|
|
313
|
+
} else {
|
|
314
|
+
const values = trimmed.split(",").map(value => value.trim());
|
|
315
|
+
if (values.some(value => value === "")) {
|
|
316
|
+
throw new CliUsageError("--reasoning-efforts must be comma-separated values from none, minimal, low, medium, high, xhigh, max, ultra (\"\" for no reasoning, \"-\" to inherit)", USAGE);
|
|
317
|
+
}
|
|
318
|
+
patch.reasoningEfforts = values;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
if (defaultEffortRaw !== undefined) patch.defaultReasoningEffort = defaultEffortRaw.trim() === "-" ? null : defaultEffortRaw.trim();
|
|
322
|
+
if (reset) {
|
|
323
|
+
if (Object.keys(patch).length > 0) {
|
|
324
|
+
throw new CliUsageError("--reset clears every override and cannot be combined with other options", USAGE);
|
|
325
|
+
}
|
|
326
|
+
for (const key of ["contextWindow", "inputModalities", "reasoningEfforts", "defaultReasoningEffort"]) patch[key] = null;
|
|
327
|
+
}
|
|
328
|
+
if (Object.keys(patch).length === 0) throw new CliUsageError("at least one setting or --reset is required", USAGE);
|
|
329
|
+
const result = await runtimeRequest<{ changed?: boolean; hasOverrides?: boolean; saved?: boolean; catalogRefresh?: { status?: string; retryable?: boolean } }>("/api/model-settings", {
|
|
330
|
+
method: "PUT",
|
|
331
|
+
body: JSON.stringify({ provider: target.provider, modelId: target.id, ...patch }),
|
|
332
|
+
}, deps);
|
|
333
|
+
const message = result?.changed === false
|
|
334
|
+
? (reset && result.hasOverrides === false ? "Nothing to restore for " + selector + "." : "No settings changed for " + selector + ".")
|
|
335
|
+
: "Updated model settings for " + selector + ".";
|
|
336
|
+
printData(result, wantsJson, [message,
|
|
337
|
+
// A non-retryable skip means there is no managed Codex catalog to refresh, which is normal.
|
|
338
|
+
...(result?.saved && (result.catalogRefresh?.status === "failed"
|
|
339
|
+
|| (result.catalogRefresh?.status === "skipped" && result.catalogRefresh.retryable === true))
|
|
340
|
+
? ["Settings saved, but the Codex model catalog did not refresh. Run `ocx sync` so Codex picks up the change."] : []),
|
|
341
|
+
]);
|
|
342
|
+
}
|
|
343
|
+
|
|
243
344
|
function parseSelector(selector: string, forceNative: boolean): { provider: string; id: string; native: boolean } {
|
|
244
345
|
if (forceNative || !selector.includes("/")) return { provider: "openai", id: selector, native: true };
|
|
245
346
|
const slash = selector.indexOf("/");
|
|
@@ -410,7 +511,7 @@ async function context(argv: string[], deps: RuntimeApiDeps): Promise<void> {
|
|
|
410
511
|
const raw = args.shift();
|
|
411
512
|
if (!raw) throw new CliUsageError("context value is required", USAGE);
|
|
412
513
|
const value = Number(raw.replace(/[_,]/g, ""));
|
|
413
|
-
if (!Number.
|
|
514
|
+
if (!Number.isSafeInteger(value) || value <= 0) throw new CliUsageError("context value must be a positive safe integer", USAGE);
|
|
414
515
|
body = { value };
|
|
415
516
|
// Explicit apply-to-all switch for headless use: re-points every routed provider to
|
|
416
517
|
// the new value, mirroring the dashboard's "apply to every routed provider" toggle.
|
|
@@ -470,6 +571,7 @@ export async function handleModelsRuntimeCommand(sub: string, argv: string[], de
|
|
|
470
571
|
else if (sub === "price") action = () => price(false, argv, deps);
|
|
471
572
|
else if (sub === "set-price") action = () => price(true, argv, deps);
|
|
472
573
|
else if (sub === "edit") action = () => edit(argv, deps);
|
|
574
|
+
else if (sub === "set") action = () => setModelSettings(argv, deps);
|
|
473
575
|
else if (sub === "enable") action = () => visibility(true, argv, deps);
|
|
474
576
|
else if (sub === "disable") action = () => visibility(false, argv, deps);
|
|
475
577
|
else if (sub === "provider") action = () => providerVisibility(argv, deps);
|
package/src/cli/models.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `ocx models` subcommand — list configured models and manage custom models.
|
|
3
3
|
*/
|
|
4
|
+
import { resolveMatchedPrice, type MatchedPrice } from "../usage/cost";
|
|
4
5
|
import { randomUUID } from "node:crypto";
|
|
5
6
|
import { createInterface } from "node:readline/promises";
|
|
6
7
|
import { syncModelsToCodex } from "../codex/sync";
|
|
@@ -85,6 +86,7 @@ interface ModelEntry {
|
|
|
85
86
|
contextWindow: number | null;
|
|
86
87
|
inputModalities: string[] | null;
|
|
87
88
|
reasoningEfforts: string[] | null;
|
|
89
|
+
price: MatchedPrice | null;
|
|
88
90
|
}
|
|
89
91
|
|
|
90
92
|
/**
|
|
@@ -132,6 +134,7 @@ function collectModels(config: OcxConfig, providerFilter?: string): ModelEntry[]
|
|
|
132
134
|
contextWindow: configuredContextWindow(prov, model) ?? null,
|
|
133
135
|
inputModalities: modalities,
|
|
134
136
|
reasoningEfforts: efforts,
|
|
137
|
+
price: resolveMatchedPrice(provName, model),
|
|
135
138
|
});
|
|
136
139
|
};
|
|
137
140
|
|
|
@@ -428,7 +431,9 @@ function handleConfiguredModels(args: string[]): void {
|
|
|
428
431
|
for (const m of provModels) {
|
|
429
432
|
const marker = m.isDefault ? " *" : "";
|
|
430
433
|
const ctx = m.contextWindow ? ` (${Math.round(m.contextWindow / 1000)}k)` : "";
|
|
431
|
-
|
|
434
|
+
const rates = m.price?.cost4;
|
|
435
|
+
const pricing = rates ? ` ~$${rates.input}/$${rates.output} input/output per 1M tokens` : " price unknown";
|
|
436
|
+
console.log(` ${m.model}${marker}${ctx}${pricing}`);
|
|
432
437
|
}
|
|
433
438
|
console.log();
|
|
434
439
|
}
|