@bitkyc08/opencodex 2.6.32 → 2.7.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/README.ko.md +9 -5
- package/README.md +7 -4
- package/README.zh-CN.md +8 -4
- package/gui/dist/assets/index-BGdxwydf.js +34 -0
- package/gui/dist/assets/index-DANCQ2Jt.css +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic.ts +62 -1
- package/src/adapters/cursor/cursor-errors.ts +28 -1
- package/src/adapters/cursor/discovery.ts +56 -10
- package/src/adapters/cursor/effort-map.ts +35 -7
- package/src/adapters/cursor/live-models.ts +3 -0
- package/src/adapters/cursor/live-transport.ts +136 -7
- package/src/adapters/cursor/protobuf-request.ts +24 -1
- package/src/adapters/cursor/request-builder.ts +6 -5
- package/src/adapters/cursor/transport-retry.ts +22 -3
- package/src/adapters/cursor.ts +2 -1
- package/src/adapters/openai-chat.ts +75 -26
- package/src/bridge.ts +42 -3
- package/src/cli/debug.ts +203 -0
- package/src/cli/doctor.ts +11 -0
- package/src/cli/help.ts +11 -0
- package/src/cli/index.ts +10 -0
- package/src/cli/v2.ts +131 -0
- package/src/codex/auth-api.ts +7 -3
- package/src/codex/catalog.ts +334 -31
- package/src/codex/data/upstream-models.json +830 -0
- package/src/codex/features.ts +178 -0
- package/src/codex/project-config-warnings.ts +388 -0
- package/src/codex/sync.ts +8 -0
- package/src/codex/warmup.ts +62 -6
- package/src/config.ts +7 -5
- package/src/lib/debug-log-buffer.ts +42 -0
- package/src/lib/debug-settings.ts +84 -0
- package/src/lib/debug.ts +18 -9
- package/src/lib/errors.ts +104 -1
- package/src/oauth/cursor.ts +35 -12
- package/src/oauth/store.ts +4 -3
- package/src/providers/derive.ts +8 -0
- package/src/providers/registry.ts +56 -21
- package/src/reasoning-effort.ts +32 -9
- package/src/responses/parser.ts +7 -2
- package/src/router.ts +5 -0
- package/src/server/adapter-resolve.ts +1 -1
- package/src/server/index.ts +27 -3
- package/src/server/management-api.ts +168 -7
- package/src/server/relay.ts +2 -2
- package/src/server/request-log.ts +78 -0
- package/src/server/responses.ts +209 -0
- package/src/types.ts +28 -1
- package/src/usage/debug.ts +32 -5
- package/src/usage/summary.ts +6 -6
- package/src/web-search/index.ts +1 -1
- package/gui/dist/assets/index-ByGC8-Bm.css +0 -1
- package/gui/dist/assets/index-D_JZzI0r.js +0 -15
package/src/bridge.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AdapterEvent, OcxUsage } from "./types";
|
|
2
|
-
import { classifyError, type OcxErrorPayload } from "./lib/errors";
|
|
2
|
+
import { adapterFailureFromMessage, classifyError, type OcxErrorPayload } from "./lib/errors";
|
|
3
3
|
import { encodeCompactionSummary } from "./responses/compaction";
|
|
4
4
|
import { encodeReasoningEnvelope, type ReasoningEnvelope } from "./responses/reasoning-envelope";
|
|
5
5
|
import { usageDisplayTotalTokens, usageInputTokensWithCacheDetail } from "./usage/totals";
|
|
@@ -40,6 +40,8 @@ function responseError(status: number, type: string, message: string): OcxErrorP
|
|
|
40
40
|
return classifyError(status, type, message);
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
export { adapterFailureFromMessage } from "./lib/errors";
|
|
44
|
+
|
|
43
45
|
/**
|
|
44
46
|
* Build the native `WebSearchAction::Search` payload from the queries that ran. codex-rs prefers a
|
|
45
47
|
* non-empty `query` over `queries` for the cell label, and only renders "<first> ..." when `query`
|
|
@@ -184,6 +186,7 @@ export function bridgeToResponsesSSE(
|
|
|
184
186
|
if (currentMsg) closeCurrentMessage();
|
|
185
187
|
if (currentReasoning) closeCurrentReasoning();
|
|
186
188
|
if (currentRawReasoning) closeCurrentRawReasoning();
|
|
189
|
+
flushHiddenRawReasoning();
|
|
187
190
|
if (currentToolCall) closeCurrentToolCall();
|
|
188
191
|
if (currentWebSearch) closeCurrentWebSearch("failed", []);
|
|
189
192
|
emit("response.incomplete", {
|
|
@@ -236,6 +239,23 @@ export function bridgeToResponsesSSE(
|
|
|
236
239
|
finishedItems.push(item as OutputItem);
|
|
237
240
|
outputIndex++;
|
|
238
241
|
};
|
|
242
|
+
// hideThinkingSummary for RAW reasoning (openai-chat reasoning_content, kiro tags): no
|
|
243
|
+
// visible reasoning item is emitted — the app renders nothing, so tool cells keep grouping
|
|
244
|
+
// like native models — but the text still round-trips in a txt-only ocxr1 envelope so
|
|
245
|
+
// preserveReasoningContentModels replay (GLM interleaved thinking) keeps working. Direct
|
|
246
|
+
// encodeReasoningEnvelope: takeReasoningEnvelope's sig/red guard would drop txt-only.
|
|
247
|
+
let hiddenRawReasoningText = "";
|
|
248
|
+
const flushHiddenRawReasoning = () => {
|
|
249
|
+
if (!hiddenRawReasoningText) return;
|
|
250
|
+
const encrypted = encodeReasoningEnvelope({ txt: hiddenRawReasoningText });
|
|
251
|
+
hiddenRawReasoningText = "";
|
|
252
|
+
const itemId = `rs_${uuid()}`;
|
|
253
|
+
const item = { type: "reasoning", id: itemId, summary: [] as never[], encrypted_content: encrypted };
|
|
254
|
+
emit("response.output_item.added", { output_index: outputIndex, item });
|
|
255
|
+
emit("response.output_item.done", { output_index: outputIndex, item });
|
|
256
|
+
finishedItems.push(item as OutputItem);
|
|
257
|
+
outputIndex++;
|
|
258
|
+
};
|
|
239
259
|
// Full assistant text of a compaction turn (across message boundaries) — becomes the
|
|
240
260
|
// synthetic compaction item's payload on done.
|
|
241
261
|
let compactionText = "";
|
|
@@ -392,6 +412,7 @@ export function bridgeToResponsesSSE(
|
|
|
392
412
|
case "text_delta": {
|
|
393
413
|
if (currentReasoning) closeCurrentReasoning();
|
|
394
414
|
if (currentRawReasoning) closeCurrentRawReasoning();
|
|
415
|
+
flushHiddenRawReasoning();
|
|
395
416
|
if (currentToolCall) closeCurrentToolCall();
|
|
396
417
|
if (!currentMsg) {
|
|
397
418
|
const itemId = `msg_${uuid()}`;
|
|
@@ -417,6 +438,7 @@ export function bridgeToResponsesSSE(
|
|
|
417
438
|
if (options?.hideThinkingSummary) { hiddenThinkingText += event.thinking; break; }
|
|
418
439
|
if (currentMsg) closeCurrentMessage();
|
|
419
440
|
if (currentRawReasoning) closeCurrentRawReasoning();
|
|
441
|
+
flushHiddenRawReasoning();
|
|
420
442
|
if (currentToolCall) closeCurrentToolCall();
|
|
421
443
|
if (!currentReasoning) {
|
|
422
444
|
const itemId = `rs_${uuid()}`;
|
|
@@ -448,6 +470,7 @@ export function bridgeToResponsesSSE(
|
|
|
448
470
|
break;
|
|
449
471
|
}
|
|
450
472
|
case "reasoning_raw_delta": {
|
|
473
|
+
if (options?.hideThinkingSummary) { hiddenRawReasoningText += event.text; break; }
|
|
451
474
|
if (currentMsg) closeCurrentMessage();
|
|
452
475
|
if (currentReasoning) closeCurrentReasoning();
|
|
453
476
|
if (currentToolCall) closeCurrentToolCall();
|
|
@@ -468,6 +491,7 @@ export function bridgeToResponsesSSE(
|
|
|
468
491
|
if (currentMsg) closeCurrentMessage();
|
|
469
492
|
if (currentReasoning) closeCurrentReasoning();
|
|
470
493
|
if (currentRawReasoning) closeCurrentRawReasoning();
|
|
494
|
+
flushHiddenRawReasoning();
|
|
471
495
|
if (currentToolCall) closeCurrentToolCall();
|
|
472
496
|
const itemId = `fc_${uuid()}`;
|
|
473
497
|
const mapped = toolNsMap?.get(event.name);
|
|
@@ -522,6 +546,7 @@ export function bridgeToResponsesSSE(
|
|
|
522
546
|
if (currentMsg) closeCurrentMessage();
|
|
523
547
|
if (currentReasoning) closeCurrentReasoning();
|
|
524
548
|
if (currentRawReasoning) closeCurrentRawReasoning();
|
|
549
|
+
flushHiddenRawReasoning();
|
|
525
550
|
if (currentToolCall) closeCurrentToolCall();
|
|
526
551
|
if (currentWebSearch) closeCurrentWebSearch("completed", []);
|
|
527
552
|
emit("response.output_item.added", {
|
|
@@ -556,6 +581,7 @@ export function bridgeToResponsesSSE(
|
|
|
556
581
|
if (currentMsg) closeCurrentMessage();
|
|
557
582
|
if (currentReasoning) closeCurrentReasoning();
|
|
558
583
|
if (currentRawReasoning) closeCurrentRawReasoning();
|
|
584
|
+
flushHiddenRawReasoning();
|
|
559
585
|
if (currentToolCall) closeCurrentToolCall();
|
|
560
586
|
if (currentWebSearch) closeCurrentWebSearch("completed", []);
|
|
561
587
|
// Redacted-only turns (or hidden thinking without a trailing signature event) still
|
|
@@ -584,16 +610,18 @@ export function bridgeToResponsesSSE(
|
|
|
584
610
|
if (currentMsg) closeCurrentMessage();
|
|
585
611
|
if (currentReasoning) closeCurrentReasoning();
|
|
586
612
|
if (currentRawReasoning) closeCurrentRawReasoning();
|
|
613
|
+
flushHiddenRawReasoning();
|
|
587
614
|
if (currentToolCall) closeCurrentToolCall();
|
|
588
615
|
if (currentWebSearch) closeCurrentWebSearch("failed", []);
|
|
616
|
+
const failure = adapterFailureFromMessage(event.message);
|
|
589
617
|
emit("response.failed", {
|
|
590
618
|
response: {
|
|
591
619
|
...responseSnapshot("failed", finishedItems),
|
|
592
620
|
// Partial consumption from a mid-stream upstream failure: surfaced so the request
|
|
593
621
|
// log can record real tokens instead of usageStatus "unreported" with 0.
|
|
594
622
|
...(event.usage ? { usage: responsesUsage(event.usage) } : {}),
|
|
595
|
-
error:
|
|
596
|
-
last_error:
|
|
623
|
+
error: failure.error,
|
|
624
|
+
last_error: failure.error,
|
|
597
625
|
},
|
|
598
626
|
});
|
|
599
627
|
reportTerminal("failed");
|
|
@@ -603,6 +631,7 @@ export function bridgeToResponsesSSE(
|
|
|
603
631
|
}
|
|
604
632
|
}
|
|
605
633
|
} catch (err) {
|
|
634
|
+
flushHiddenRawReasoning();
|
|
606
635
|
if (currentWebSearch) closeCurrentWebSearch("failed", []);
|
|
607
636
|
emit("response.failed", {
|
|
608
637
|
response: {
|
|
@@ -623,6 +652,7 @@ export function bridgeToResponsesSSE(
|
|
|
623
652
|
if (currentMsg) closeCurrentMessage();
|
|
624
653
|
if (currentReasoning) closeCurrentReasoning();
|
|
625
654
|
if (currentRawReasoning) closeCurrentRawReasoning();
|
|
655
|
+
flushHiddenRawReasoning();
|
|
626
656
|
if (currentToolCall) closeCurrentToolCall();
|
|
627
657
|
if (currentWebSearch) closeCurrentWebSearch("failed", []);
|
|
628
658
|
emit("response.incomplete", {
|
|
@@ -723,6 +753,15 @@ export function buildResponseJSON(
|
|
|
723
753
|
};
|
|
724
754
|
const flushRawReasoning = () => {
|
|
725
755
|
if (!currentRawReasoning) return;
|
|
756
|
+
if (options?.hideThinkingSummary === true) {
|
|
757
|
+
// Same contract as the streaming path: no visible reasoning, txt-only envelope round-trip.
|
|
758
|
+
output.push({
|
|
759
|
+
type: "reasoning", id: `rs_${uuid()}`, summary: [],
|
|
760
|
+
encrypted_content: encodeReasoningEnvelope({ txt: currentRawReasoning }),
|
|
761
|
+
});
|
|
762
|
+
currentRawReasoning = "";
|
|
763
|
+
return;
|
|
764
|
+
}
|
|
726
765
|
output.push({
|
|
727
766
|
type: "reasoning", id: `rs_${uuid()}`, summary: [],
|
|
728
767
|
content: [{ type: "reasoning_text", text: currentRawReasoning }],
|
package/src/cli/debug.ts
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { findLiveProxy, probeHostname } from "../server/proxy-liveness";
|
|
2
|
+
import { DEBUG_ENV, type DebugSettingsView } from "../lib/debug-settings";
|
|
3
|
+
import { runningProxyUpdateHeaders } from "../oauth/login-cli";
|
|
4
|
+
|
|
5
|
+
type DebugScope = "provider" | "usage";
|
|
6
|
+
|
|
7
|
+
async function requireLiveProxy() {
|
|
8
|
+
const live = await findLiveProxy();
|
|
9
|
+
if (!live) {
|
|
10
|
+
console.error("Proxy is not running. Start it with: ocx start");
|
|
11
|
+
process.exit(1);
|
|
12
|
+
}
|
|
13
|
+
return live;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async function fetchDebugSettings(): Promise<DebugSettingsView> {
|
|
17
|
+
const live = await requireLiveProxy();
|
|
18
|
+
try {
|
|
19
|
+
const res = await fetch(`http://${probeHostname(live.hostname)}:${live.port}/api/debug`, {
|
|
20
|
+
headers: runningProxyUpdateHeaders(),
|
|
21
|
+
});
|
|
22
|
+
if (!res.ok) {
|
|
23
|
+
console.error(`Failed to read debug settings (${res.status})`);
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
return await res.json() as DebugSettingsView;
|
|
27
|
+
} catch (err) {
|
|
28
|
+
console.error(`Proxy is running but /api/debug is unreachable: ${err instanceof Error ? err.message : String(err)}`);
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function putDebugSettings(body: Record<string, unknown>): Promise<DebugSettingsView> {
|
|
34
|
+
const live = await requireLiveProxy();
|
|
35
|
+
const res = await fetch(`http://${probeHostname(live.hostname)}:${live.port}/api/debug`, {
|
|
36
|
+
method: "PUT",
|
|
37
|
+
headers: runningProxyUpdateHeaders(),
|
|
38
|
+
body: JSON.stringify(body),
|
|
39
|
+
});
|
|
40
|
+
if (!res.ok) {
|
|
41
|
+
const text = await res.text().catch(() => "");
|
|
42
|
+
console.error(`Failed to update debug settings (${res.status})${text ? `: ${text.slice(0, 200)}` : ""}`);
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
return await res.json() as DebugSettingsView;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function printScopeStatus(scope: DebugScope, view: DebugSettingsView): void {
|
|
49
|
+
if (scope === "provider") {
|
|
50
|
+
console.log(`Provider debug: ${view.enabled ? "ON" : "off"}`);
|
|
51
|
+
console.log(` env=${view.env.debug ? "on" : "off"}, runtime=${view.runtimeOverride.debug === undefined ? "env/default" : view.runtimeOverride.debug ? "on" : "off"}`);
|
|
52
|
+
console.log(" Tail: ocx debug provider logs [-f]");
|
|
53
|
+
} else {
|
|
54
|
+
console.log(`Usage debug: ${view.usage ? "ON" : "off"}`);
|
|
55
|
+
console.log(` env=${view.env.usage ? "on" : "off"}, runtime=${view.runtimeOverride.usage === undefined ? "env/default" : view.runtimeOverride.usage ? "on" : "off"}`);
|
|
56
|
+
console.log(" Tail: ocx debug usage logs [-f] (via running proxy API)");
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function envDebugEnabled(): boolean {
|
|
61
|
+
return process.env.OCX_DEBUG === "1"
|
|
62
|
+
|| process.env.OCX_DEBUG_FRAMES === "1";
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function printProviderLogs(follow: boolean): Promise<void> {
|
|
66
|
+
const live = await requireLiveProxy();
|
|
67
|
+
const base = `http://${probeHostname(live.hostname)}:${live.port}/api/debug/logs`;
|
|
68
|
+
|
|
69
|
+
let after = 0;
|
|
70
|
+
try {
|
|
71
|
+
const res = await fetch(`${base}?limit=500`, { headers: runningProxyUpdateHeaders() });
|
|
72
|
+
if (!res.ok) {
|
|
73
|
+
console.error(`Failed to read debug logs (${res.status})`);
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
const entries = await res.json() as { seq: number; line: string }[];
|
|
77
|
+
for (const entry of entries) console.log(entry.line);
|
|
78
|
+
if (entries.length > 0) after = entries[entries.length - 1]!.seq;
|
|
79
|
+
} catch (err) {
|
|
80
|
+
console.error(`Failed to read debug logs: ${err instanceof Error ? err.message : String(err)}`);
|
|
81
|
+
process.exit(1);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (!follow) return;
|
|
85
|
+
|
|
86
|
+
while (true) {
|
|
87
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
88
|
+
try {
|
|
89
|
+
const res = await fetch(`${base}?after=${after}&limit=500`, { headers: runningProxyUpdateHeaders() });
|
|
90
|
+
if (!res.ok) continue;
|
|
91
|
+
const entries = await res.json() as { seq: number; line: string }[];
|
|
92
|
+
for (const entry of entries) console.log(entry.line);
|
|
93
|
+
if (entries.length > 0) after = entries[entries.length - 1]!.seq;
|
|
94
|
+
} catch {
|
|
95
|
+
/* keep following */
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function printUsageLogs(follow: boolean): Promise<void> {
|
|
101
|
+
const live = await requireLiveProxy();
|
|
102
|
+
const base = `http://${probeHostname(live.hostname)}:${live.port}/api/debug/usage-logs`;
|
|
103
|
+
|
|
104
|
+
let after = 0;
|
|
105
|
+
try {
|
|
106
|
+
const res = await fetch(`${base}?limit=500`, { headers: runningProxyUpdateHeaders() });
|
|
107
|
+
if (!res.ok) {
|
|
108
|
+
console.error(`Failed to read usage debug logs (${res.status})`);
|
|
109
|
+
process.exit(1);
|
|
110
|
+
}
|
|
111
|
+
const entries = await res.json() as { seq: number; line: string }[];
|
|
112
|
+
for (const entry of entries) console.log(entry.line);
|
|
113
|
+
if (entries.length === 0) console.log("(empty — enable with: ocx debug usage on)");
|
|
114
|
+
if (entries.length > 0) after = entries[entries.length - 1]!.seq;
|
|
115
|
+
} catch (err) {
|
|
116
|
+
console.error(`Failed to read usage debug logs: ${err instanceof Error ? err.message : String(err)}`);
|
|
117
|
+
process.exit(1);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (!follow) return;
|
|
121
|
+
|
|
122
|
+
while (true) {
|
|
123
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
124
|
+
try {
|
|
125
|
+
const res = await fetch(`${base}?after=${after}&limit=500`, { headers: runningProxyUpdateHeaders() });
|
|
126
|
+
if (!res.ok) continue;
|
|
127
|
+
const entries = await res.json() as { seq: number; line: string }[];
|
|
128
|
+
for (const entry of entries) console.log(entry.line);
|
|
129
|
+
if (entries.length > 0) after = entries[entries.length - 1]!.seq;
|
|
130
|
+
} catch {
|
|
131
|
+
/* keep following */
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async function handleScopeCommand(scope: DebugScope, actionArgv: string[]): Promise<void> {
|
|
137
|
+
const action = (actionArgv[0] ?? "status").trim().toLowerCase();
|
|
138
|
+
|
|
139
|
+
if (action === "on" || action === "off") {
|
|
140
|
+
const enabled = action === "on";
|
|
141
|
+
const body = scope === "provider" ? { debug: enabled } : { usage: enabled };
|
|
142
|
+
printScopeStatus(scope, await putDebugSettings(body));
|
|
143
|
+
console.log(`\n${scope} debug is now ${enabled ? "enabled" : "disabled"}.`);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (action === "status") {
|
|
148
|
+
printScopeStatus(scope, await fetchDebugSettings());
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (action === "reset") {
|
|
153
|
+
const resetKey = scope === "provider" ? "provider" : "usage";
|
|
154
|
+
printScopeStatus(scope, await putDebugSettings({ reset: resetKey }));
|
|
155
|
+
console.log(`\nRuntime override cleared for ${scope}; effective value follows env again.`);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (action === "logs") {
|
|
160
|
+
const follow = actionArgv.slice(1).some(arg => arg === "-f" || arg === "--follow");
|
|
161
|
+
if (scope === "provider") await printProviderLogs(follow);
|
|
162
|
+
else await printUsageLogs(follow);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
console.error(`Usage: ocx debug ${scope} on|off|status|reset|logs [-f]`);
|
|
167
|
+
process.exit(1);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function printTopLevelHelp(): void {
|
|
171
|
+
console.log("Debug commands (proxy must be running):");
|
|
172
|
+
console.log("");
|
|
173
|
+
console.log(" ocx debug provider on|off|status|reset|logs [-f]");
|
|
174
|
+
console.log(" ocx debug usage on|off|status|reset|logs [-f]");
|
|
175
|
+
console.log("");
|
|
176
|
+
console.log("Env defaults on start:");
|
|
177
|
+
console.log(" provider → OCX_DEBUG=1 (legacy OCX_DEBUG_FRAMES still works)");
|
|
178
|
+
console.log(` usage → ${DEBUG_ENV.usage}=1`);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export async function handleDebugCommand(argv: string[]): Promise<void> {
|
|
182
|
+
const sub = (argv[0] ?? "").trim().toLowerCase();
|
|
183
|
+
|
|
184
|
+
if (sub === "provider" || sub === "usage") {
|
|
185
|
+
await handleScopeCommand(sub, argv.slice(1));
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (sub === "" || sub === "help" || sub === "--help" || sub === "-h") {
|
|
190
|
+
const live = await findLiveProxy();
|
|
191
|
+
if (!live) {
|
|
192
|
+
console.log("Proxy is not running — env defaults for the next start:");
|
|
193
|
+
console.log(` provider → OCX_DEBUG = ${envDebugEnabled() ? "on" : "off"}`);
|
|
194
|
+
console.log(` usage → ${DEBUG_ENV.usage} = ${process.env[DEBUG_ENV.usage] === "1" ? "on" : "off"}`);
|
|
195
|
+
console.log("");
|
|
196
|
+
}
|
|
197
|
+
printTopLevelHelp();
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
printTopLevelHelp();
|
|
202
|
+
process.exit(1);
|
|
203
|
+
}
|
package/src/cli/doctor.ts
CHANGED
|
@@ -15,6 +15,7 @@ import { readCodexTokens } from "../codex/auth-collision";
|
|
|
15
15
|
import { resolveCodexHomeDir as resolveCodexHomeDirImpl, isWslRuntime, listWslWindowsCodexHomes, wslAutomountRoot, type CodexHomeDeps } from "../codex/home";
|
|
16
16
|
import { findCodexOnPath, isWindowsInteropDir } from "../codex/shim";
|
|
17
17
|
import { countPendingOpencodexHistory } from "../codex/history-provider";
|
|
18
|
+
import { collectProjectCodexConfigWarnings, formatProjectCodexConfigWarningsForDoctor } from "../codex/project-config-warnings";
|
|
18
19
|
export { resolveCodexHomeDir } from "../codex/home";
|
|
19
20
|
|
|
20
21
|
const WHAM_USAGE_URL = "https://chatgpt.com/backend-api/wham/usage";
|
|
@@ -373,6 +374,16 @@ export async function runDoctor(): Promise<void> {
|
|
|
373
374
|
console.log(` -- ${pending.pendingRows} thread(s) still tagged opencodex, ${pending.backupEntries} backup manifest entr${pending.backupEntries === 1 ? "y" : "ies"}`);
|
|
374
375
|
}
|
|
375
376
|
|
|
377
|
+
console.log("\nProject Codex configs");
|
|
378
|
+
const projectWarnings = collectProjectCodexConfigWarnings();
|
|
379
|
+
if (projectWarnings.length === 0) {
|
|
380
|
+
console.log(" ok no project-local provider bypass detected");
|
|
381
|
+
} else {
|
|
382
|
+
for (const line of formatProjectCodexConfigWarningsForDoctor(projectWarnings)) {
|
|
383
|
+
console.log(line);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
376
387
|
const dual = collectWslDualInstall();
|
|
377
388
|
if (dual.wsl) {
|
|
378
389
|
console.log("\nWSL Codex installs");
|
package/src/cli/help.ts
CHANGED
|
@@ -54,6 +54,15 @@ const helpEntries: Record<string, HelpEntry> = {
|
|
|
54
54
|
"sync-cache": { usage: "ocx sync-cache", summary: "Refresh Codex's model cache from the active catalog." },
|
|
55
55
|
status: { usage: "ocx status", summary: "Check proxy server status." },
|
|
56
56
|
doctor: { usage: "ocx doctor", summary: "Diagnose environment/network issues (paths, WSL /mnt, proxy env, ChatGPT reachability)." },
|
|
57
|
+
debug: {
|
|
58
|
+
usage: "ocx debug [provider on|off|status|reset|logs [-f]|usage on|off|status|reset|logs [-f]]",
|
|
59
|
+
summary: "Show or toggle runtime provider debug logging on the running proxy.",
|
|
60
|
+
details: [
|
|
61
|
+
"Provider: ocx debug provider on | off | status | reset | logs [-f]",
|
|
62
|
+
"Usage JSONL: ocx debug usage on | off | status | reset | logs [-f]",
|
|
63
|
+
"Env default: OCX_DEBUG=1 (legacy OCX_DEBUG_FRAMES still works)",
|
|
64
|
+
],
|
|
65
|
+
},
|
|
57
66
|
login: { usage: "ocx login <provider>", summary: "OAuth or API-key login for a provider." },
|
|
58
67
|
logout: { usage: "ocx logout <provider>", summary: "Remove a stored provider login." },
|
|
59
68
|
gui: { usage: "ocx gui", summary: "Open the opencodex dashboard." },
|
|
@@ -115,6 +124,8 @@ Usage:
|
|
|
115
124
|
ocx sync-cache Refresh Codex's model cache from the active catalog
|
|
116
125
|
ocx status Check proxy server status
|
|
117
126
|
ocx doctor Diagnose environment/network issues (WSL, proxy, ChatGPT reachability)
|
|
127
|
+
ocx debug [provider|usage ...]
|
|
128
|
+
provider/usage on|off|status|reset|logs [-f]
|
|
118
129
|
ocx login <provider> OAuth login (xai) — opens browser, stores token in ~/.opencodex/auth.json
|
|
119
130
|
ocx logout <provider> Remove a stored OAuth login
|
|
120
131
|
ocx gui Open the opencodex dashboard
|
package/src/cli/index.ts
CHANGED
|
@@ -449,6 +449,11 @@ switch (command) {
|
|
|
449
449
|
await runDoctor();
|
|
450
450
|
break;
|
|
451
451
|
}
|
|
452
|
+
case "debug": {
|
|
453
|
+
const { handleDebugCommand } = await import("./debug");
|
|
454
|
+
await handleDebugCommand(args.slice(1));
|
|
455
|
+
break;
|
|
456
|
+
}
|
|
452
457
|
case "ensure":
|
|
453
458
|
await handleEnsure();
|
|
454
459
|
break;
|
|
@@ -468,6 +473,11 @@ switch (command) {
|
|
|
468
473
|
await syncModelsToCodex((await findLiveProxy())?.port);
|
|
469
474
|
break;
|
|
470
475
|
}
|
|
476
|
+
case "v2": {
|
|
477
|
+
const { cmdV2 } = await import("./v2");
|
|
478
|
+
process.exitCode = await cmdV2(args.slice(1), {}, async () => (await findLiveProxy())?.port);
|
|
479
|
+
break;
|
|
480
|
+
}
|
|
471
481
|
case "sync-cache": {
|
|
472
482
|
const { invalidateCodexModelsCache } = await import("../codex/catalog");
|
|
473
483
|
invalidateCodexModelsCache();
|
package/src/cli/v2.ts
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ocx v2 status|on|off` — toggle/report the codex `multi_agent_v2` feature that
|
|
3
|
+
* controls the multi-agent surface (v1 vs v2 collab mode).
|
|
4
|
+
*
|
|
5
|
+
* Contract:
|
|
6
|
+
* - config.toml writes go through the official `codex features enable|disable`
|
|
7
|
+
* CLI only (format-preserving TOML edit stays upstream-owned).
|
|
8
|
+
* - after a successful flip the catalog is RESYNCED so model metadata stays fresh.
|
|
9
|
+
* - `on` warns when [agents] max_threads is still present (codex-rs refuses to
|
|
10
|
+
* boot with it while v2 is enabled) — ocx never edits that key itself.
|
|
11
|
+
* - nothing in the catalog build path calls this module; no auto-flip exists.
|
|
12
|
+
*/
|
|
13
|
+
import { execFileSync } from "node:child_process";
|
|
14
|
+
import { getMaxConcurrentThreads, hasAgentsMaxThreads, isMultiAgentV2Enabled, setMaxConcurrentThreads } from "../codex/features";
|
|
15
|
+
|
|
16
|
+
import { loadConfig, saveConfig } from "../config";
|
|
17
|
+
|
|
18
|
+
export interface V2CliDeps {
|
|
19
|
+
execFile?: (file: string, args: string[]) => void;
|
|
20
|
+
isEnabled?: typeof isMultiAgentV2Enabled;
|
|
21
|
+
hasMaxThreads?: typeof hasAgentsMaxThreads;
|
|
22
|
+
sync?: (port?: number) => Promise<unknown>;
|
|
23
|
+
log?: Pick<Console, "log" | "error">;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function runCodexFeatures(action: "enable" | "disable", deps: V2CliDeps): void {
|
|
27
|
+
const exec = deps.execFile ?? ((file: string, args: string[]) => {
|
|
28
|
+
execFileSync(file, args, { stdio: ["ignore", "pipe", "pipe"], timeout: 15_000, windowsHide: true });
|
|
29
|
+
});
|
|
30
|
+
const command = process.env.CODEX_CLI_PATH?.trim() || "codex";
|
|
31
|
+
exec(command, ["features", action, "multi_agent_v2"]);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function v2StatusLine(enabled: boolean): string {
|
|
35
|
+
return enabled
|
|
36
|
+
? "multi_agent_v2: ON — v2 multi-agent surface active"
|
|
37
|
+
: "multi_agent_v2: OFF — v1 multi-agent surface (default install)";
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function multiAgentModeLine(mode: string): string {
|
|
41
|
+
switch (mode) {
|
|
42
|
+
case "v1": return "multi_agent_mode: v1 — ALL models forced to v1 surface (upstream pins overridden)";
|
|
43
|
+
case "v2": return "multi_agent_mode: v2 — ALL models forced to v2 surface (upstream pins overridden)";
|
|
44
|
+
default: return "multi_agent_mode: default — upstream model pins respected (sol/terra=v2, luna=v1, rest=codex flag)";
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function cmdV2(args: string[], deps: V2CliDeps = {}, findPort?: () => Promise<number | undefined>): Promise<number> {
|
|
49
|
+
const log = deps.log ?? console;
|
|
50
|
+
const isEnabled = deps.isEnabled ?? isMultiAgentV2Enabled;
|
|
51
|
+
const hasMaxThreads = deps.hasMaxThreads ?? hasAgentsMaxThreads;
|
|
52
|
+
const verb = (args[0] ?? "status").trim().toLowerCase();
|
|
53
|
+
|
|
54
|
+
if (verb === "status") {
|
|
55
|
+
log.log(v2StatusLine(isEnabled()));
|
|
56
|
+
const cfg = loadConfig();
|
|
57
|
+
log.log(multiAgentModeLine(cfg.multiAgentMode ?? "default"));
|
|
58
|
+
const threads = getMaxConcurrentThreads();
|
|
59
|
+
log.log(`max_concurrent_threads_per_session: ${threads ?? "(unset — codex default)"}`);
|
|
60
|
+
if (isEnabled() && hasMaxThreads()) {
|
|
61
|
+
log.log("WARNING: [agents] max_threads is set — codex refuses to start while multi_agent_v2 is enabled. Remove it from config.toml (concurrency lives in features.multi_agent_v2.max_concurrent_threads_per_session).");
|
|
62
|
+
}
|
|
63
|
+
return 0;
|
|
64
|
+
}
|
|
65
|
+
if (verb === "threads") {
|
|
66
|
+
const value = Number((args[1] ?? "").trim());
|
|
67
|
+
if (!Number.isInteger(value) || value < 1) {
|
|
68
|
+
log.error("v2 threads: pass an integer >= 1 (features.multi_agent_v2.max_concurrent_threads_per_session)");
|
|
69
|
+
return 1;
|
|
70
|
+
}
|
|
71
|
+
const result = setMaxConcurrentThreads(value);
|
|
72
|
+
if (!result.ok) { log.error(`v2 threads: ${result.error}`); return 1; }
|
|
73
|
+
log.log(result.changed
|
|
74
|
+
? `max_concurrent_threads_per_session = ${value} — applies to new sessions.`
|
|
75
|
+
: `max_concurrent_threads_per_session already ${value} — nothing to do.`);
|
|
76
|
+
return 0;
|
|
77
|
+
}
|
|
78
|
+
if (verb === "mode") {
|
|
79
|
+
const modeArg = (args[1] ?? "").trim().toLowerCase();
|
|
80
|
+
if (modeArg !== "v1" && modeArg !== "default" && modeArg !== "v2") {
|
|
81
|
+
log.error("v2 mode: expected v1|default|v2");
|
|
82
|
+
return 1;
|
|
83
|
+
}
|
|
84
|
+
const cfg = loadConfig();
|
|
85
|
+
if (modeArg === "default") delete cfg.multiAgentMode;
|
|
86
|
+
else cfg.multiAgentMode = modeArg as "v1" | "v2";
|
|
87
|
+
saveConfig(cfg);
|
|
88
|
+
try {
|
|
89
|
+
const sync = deps.sync ?? (await import("../codex/sync")).syncModelsToCodex;
|
|
90
|
+
await sync(findPort ? await findPort() : undefined);
|
|
91
|
+
} catch (err) {
|
|
92
|
+
log.error(`catalog resync failed: ${err instanceof Error ? err.message : String(err)} — run 'ocx sync' manually.`);
|
|
93
|
+
return 1;
|
|
94
|
+
}
|
|
95
|
+
log.log(multiAgentModeLine(modeArg));
|
|
96
|
+
log.log("Applies to NEW sessions; running sessions keep their pinned multi-agent version.");
|
|
97
|
+
return 0;
|
|
98
|
+
}
|
|
99
|
+
if (verb !== "on" && verb !== "off") {
|
|
100
|
+
log.error(`v2: unknown verb '${verb}' (expected status|on|off|mode <v1|default|v2>|threads <n>)`);
|
|
101
|
+
return 1;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const want = verb === "on";
|
|
105
|
+
if (isEnabled() === want) {
|
|
106
|
+
log.log(`multi_agent_v2 already ${want ? "ON" : "OFF"} — nothing to do.`);
|
|
107
|
+
return 0;
|
|
108
|
+
}
|
|
109
|
+
try {
|
|
110
|
+
runCodexFeatures(want ? "enable" : "disable", deps);
|
|
111
|
+
} catch (err) {
|
|
112
|
+
log.error(`codex features ${want ? "enable" : "disable"} multi_agent_v2 failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
113
|
+
return 1;
|
|
114
|
+
}
|
|
115
|
+
if (want && hasMaxThreads()) {
|
|
116
|
+
log.log("WARNING: [agents] max_threads is still set — codex will REFUSE to start until you remove it (features.multi_agent_v2.max_concurrent_threads_per_session replaces it).");
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Resync catalog so multi-agent surface metadata stays fresh in both the
|
|
120
|
+
// on-disk catalog and models_cache.json after the toggle flip.
|
|
121
|
+
try {
|
|
122
|
+
const sync = deps.sync ?? (await import("../codex/sync")).syncModelsToCodex;
|
|
123
|
+
await sync(findPort ? await findPort() : undefined);
|
|
124
|
+
} catch (err) {
|
|
125
|
+
log.error(`catalog resync failed (flag IS flipped): ${err instanceof Error ? err.message : String(err)} — run 'ocx sync' manually.`);
|
|
126
|
+
return 1;
|
|
127
|
+
}
|
|
128
|
+
log.log(v2StatusLine(want));
|
|
129
|
+
log.log("Applies to NEW sessions; running sessions keep their pinned multi-agent version. Restart the Codex app (or wait out its picker cache) to see the ladder change.");
|
|
130
|
+
return 0;
|
|
131
|
+
}
|
package/src/codex/auth-api.ts
CHANGED
|
@@ -27,7 +27,7 @@ export { clearAccountQuota, getAccountQuota, parseUsageQuota, updateAccountQuota
|
|
|
27
27
|
import { extractAccountId, decodeJwtPayload } from "../oauth/chatgpt";
|
|
28
28
|
import { MAIN_CODEX_ACCOUNT_ID, setMainAccountPlan } from "./main-account";
|
|
29
29
|
import { maskEmail } from "../lib/privacy";
|
|
30
|
-
import { codexWarmupFailureReason, warmCodexAccount } from "./warmup";
|
|
30
|
+
import { CodexWarmupError, codexWarmupFailureReason, warmCodexAccount } from "./warmup";
|
|
31
31
|
export { maskEmail } from "../lib/privacy";
|
|
32
32
|
import type { CodexAccount, OcxConfig } from "../types";
|
|
33
33
|
|
|
@@ -149,12 +149,16 @@ async function verifyCodexAccountWarmup(
|
|
|
149
149
|
await warmCodexAccount({ accessToken, chatgptAccountId });
|
|
150
150
|
return { ok: true, validatedAt: Date.now() };
|
|
151
151
|
} catch (err) {
|
|
152
|
+
const reason = codexWarmupFailureReason(err);
|
|
153
|
+
const upstream = err instanceof CodexWarmupError ? err.upstreamDetail : undefined;
|
|
152
154
|
return {
|
|
153
155
|
ok: false,
|
|
154
156
|
response: jsonResponse({
|
|
155
|
-
error:
|
|
157
|
+
error: upstream
|
|
158
|
+
? `Codex account warmup failed: ${upstream}`
|
|
159
|
+
: "Codex account warmup failed. Reauthenticate the account and try again.",
|
|
156
160
|
code: "codex_warmup_failed",
|
|
157
|
-
reason
|
|
161
|
+
reason,
|
|
158
162
|
accountId,
|
|
159
163
|
}, 401),
|
|
160
164
|
};
|