@bitkyc08/opencodex 2.6.26-preview.20260705 → 2.6.28-preview.20260707
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.md +1 -0
- package/bin/ocx.mjs +4 -4
- package/gui/dist/assets/index-ByGC8-Bm.css +1 -0
- package/gui/dist/assets/index-CkV5xFA8.js +15 -0
- package/gui/dist/index.html +2 -2
- package/package.json +4 -4
- package/src/adapters/anthropic-image-guard.ts +195 -0
- package/src/adapters/anthropic.ts +85 -14
- package/src/adapters/cursor/cursor-errors.ts +2 -2
- package/src/adapters/cursor/live-transport.ts +1 -1
- package/src/adapters/cursor/transport-retry.ts +2 -2
- package/src/adapters/google-errors.ts +1 -1
- package/src/adapters/google-http.ts +1 -1
- package/src/adapters/google-truncation.ts +1 -1
- package/src/adapters/google.ts +1 -1
- package/src/adapters/kiro-errors.ts +1 -1
- package/src/adapters/kiro-retry.ts +1 -1
- package/src/adapters/kiro-truncation.ts +1 -1
- package/src/adapters/kiro.ts +1 -1
- package/src/adapters/openai-chat.ts +12 -2
- package/src/adapters/openai-responses.ts +126 -3
- package/src/bridge.ts +164 -7
- package/src/{doctor.ts → cli/doctor.ts} +21 -4
- package/src/{cli-help.ts → cli/help.ts} +1 -1
- package/src/cli/index.ts +584 -0
- package/src/{init.ts → cli/init.ts} +6 -6
- package/src/{cli-models.ts → cli/models.ts} +2 -2
- package/src/{cli-provider.ts → cli/provider.ts} +12 -8
- package/src/{star-prompt.ts → cli/star-prompt.ts} +1 -1
- package/src/{cli-status.ts → cli/status.ts} +7 -7
- package/src/cli.ts +9 -575
- package/src/{codex-account-label.ts → codex/account-label.ts} +1 -1
- package/src/{codex-account-lifecycle.ts → codex/account-lifecycle.ts} +6 -6
- package/src/{codex-account-store.ts → codex/account-store.ts} +2 -2
- package/src/{codex-account-usability.ts → codex/account-usability.ts} +4 -4
- package/src/{codex-auth-api.ts → codex/auth-api.ts} +18 -18
- package/src/{codex-auth-collision.ts → codex/auth-collision.ts} +4 -4
- package/src/{codex-auth-context.ts → codex/auth-context.ts} +9 -9
- package/src/{codex-catalog.ts → codex/catalog.ts} +49 -20
- package/src/codex/history-migration-guardian.ts +102 -0
- package/src/{codex-history-provider.ts → codex/history-provider.ts} +111 -7
- package/src/{codex-home.ts → codex/home.ts} +1 -1
- package/src/{codex-inject.ts → codex/inject.ts} +204 -26
- package/src/{codex-journal.ts → codex/journal.ts} +2 -2
- package/src/{codex-main-account.ts → codex/main-account.ts} +2 -2
- package/src/{model-cache.ts → codex/model-cache.ts} +1 -1
- package/src/{codex-paths.ts → codex/paths.ts} +2 -2
- package/src/{codex-plugins-doctor.ts → codex/plugins-doctor.ts} +2 -2
- package/src/{codex-refresh.ts → codex/refresh.ts} +4 -4
- package/src/{codex-routing.ts → codex/routing.ts} +8 -8
- package/src/{codex-shim.ts → codex/shim.ts} +6 -5
- package/src/{codex-sync.ts → codex/sync.ts} +4 -4
- package/src/{codex-websocket-registry.ts → codex/websocket-registry.ts} +1 -1
- package/src/config.ts +2 -0
- package/src/generated/jawcode-model-metadata.ts +2 -0
- package/src/{bun-runtime.ts → lib/bun-runtime.ts} +1 -1
- package/src/{crash-guard.ts → lib/crash-guard.ts} +1 -1
- package/src/{process-control.ts → lib/process-control.ts} +1 -1
- package/src/{service-secrets.ts → lib/service-secrets.ts} +1 -1
- package/src/oauth/callback-server.ts +1 -1
- package/src/oauth/google-antigravity.ts +7 -4
- package/src/oauth/index.ts +67 -16
- package/src/oauth/login-cli.ts +2 -2
- package/src/oauth/store.ts +236 -20
- package/src/oauth/token-guardian.ts +24 -20
- package/src/oauth/types.ts +16 -0
- package/src/providers/api-keys.ts +121 -0
- package/src/{provider-context-cap.ts → providers/context-cap.ts} +1 -1
- package/src/providers/derive.ts +2 -0
- package/src/providers/key-failover.ts +145 -0
- package/src/{provider-label.ts → providers/label.ts} +1 -1
- package/src/{provider-quota.ts → providers/quota.ts} +12 -7
- package/src/providers/registry.ts +66 -4
- package/src/responses/compaction.ts +117 -0
- package/src/responses/parser.ts +89 -13
- package/src/responses/reasoning-envelope.ts +52 -0
- package/src/responses/schema.ts +15 -3
- package/src/responses/state.ts +117 -2
- package/src/router.ts +2 -0
- package/src/server/auth-cors.ts +231 -0
- package/src/server/index.ts +523 -0
- package/src/server/lifecycle.ts +73 -0
- package/src/server/management-api.ts +628 -0
- package/src/{proxy-liveness.ts → server/proxy-liveness.ts} +1 -1
- package/src/server/relay.ts +534 -0
- package/src/server/request-decompress.ts +46 -0
- package/src/server/request-log.ts +310 -0
- package/src/server/responses.ts +775 -0
- package/src/{ws-bridge.ts → server/ws-bridge.ts} +44 -13
- package/src/service.ts +19 -11
- package/src/types.ts +27 -0
- package/src/{update.ts → update/index.ts} +5 -5
- package/src/{update-job.ts → update/job.ts} +7 -6
- package/src/{update-notify.ts → update/notify.ts} +3 -3
- package/src/{usage-debug.ts → usage/debug.ts} +3 -3
- package/src/{usage-log.ts → usage/log.ts} +3 -3
- package/src/{usage-summary.ts → usage/summary.ts} +3 -3
- package/src/{usage-totals.ts → usage/totals.ts} +1 -1
- package/src/vision/describe.ts +3 -3
- package/src/vision/index.ts +21 -1
- package/src/web-search/executor.ts +4 -4
- package/src/web-search/index.ts +1 -1
- package/src/web-search/loop.ts +84 -24
- package/gui/dist/assets/index-BcHhxo1I.css +0 -1
- package/gui/dist/assets/index-DCC1q_Jx.js +0 -15
- package/src/server.ts +0 -2501
- /package/src/{codex-account-runtime-state.ts → codex/account-runtime-state.ts} +0 -0
- /package/src/{codex-quota.ts → codex/quota.ts} +0 -0
- /package/src/{abort.ts → lib/abort.ts} +0 -0
- /package/src/{debug.ts → lib/debug.ts} +0 -0
- /package/src/{errors.ts → lib/errors.ts} +0 -0
- /package/src/{open-url.ts → lib/open-url.ts} +0 -0
- /package/src/{privacy.ts → lib/privacy.ts} +0 -0
- /package/src/{redact.ts → lib/redact.ts} +0 -0
- /package/src/{sidecar-tracker.ts → lib/sidecar-tracker.ts} +0 -0
- /package/src/{upstream-retry.ts → lib/upstream-retry.ts} +0 -0
- /package/src/{win-paths.ts → lib/win-paths.ts} +0 -0
- /package/src/{ports.ts → server/ports.ts} +0 -0
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { IncomingMeta, ProviderAdapter } from "./base";
|
|
2
2
|
import type { AdapterEvent, OcxParsedRequest, OcxProviderConfig } from "../types";
|
|
3
|
+
import { decodeCompactionSummary, SUMMARY_PREFIX } from "../responses/compaction";
|
|
4
|
+
import { OCX_REASONING_PREFIX } from "../responses/reasoning-envelope";
|
|
3
5
|
|
|
4
6
|
// Headers relayed verbatim from the caller in OAuth-passthrough ("forward") mode.
|
|
5
7
|
// Exported so the web-search sidecar reuses the exact same forwarded-auth set for its ChatGPT call.
|
|
@@ -31,17 +33,50 @@ function sanitizeReasoningInputContent(body: unknown): unknown {
|
|
|
31
33
|
const input = raw.input.map(item => {
|
|
32
34
|
if (!item || typeof item !== "object" || Array.isArray(item)) return item;
|
|
33
35
|
const rec = item as Record<string, unknown>;
|
|
34
|
-
if (rec.type !== "reasoning"
|
|
36
|
+
if (rec.type !== "reasoning") return item;
|
|
37
|
+
const hasRawContent = Array.isArray(rec.content) && rec.content.length > 0;
|
|
38
|
+
// ocxr1 envelopes are proxy-minted (Anthropic signatures), not OpenAI encryption — the native
|
|
39
|
+
// backend cannot decrypt them and would reject the request. Strip regardless of content shape.
|
|
40
|
+
const hasOcxEnvelope = typeof rec.encrypted_content === "string" && rec.encrypted_content.startsWith(OCX_REASONING_PREFIX);
|
|
41
|
+
if (!hasRawContent && !hasOcxEnvelope) return item;
|
|
35
42
|
changed = true;
|
|
36
43
|
// Routed models can produce raw `reasoning_text` output items. Codex echoes those in later
|
|
37
44
|
// native GPT requests, but ChatGPT's Responses backend accepts reasoning input only with empty
|
|
38
45
|
// `content`; keep summaries/ids and drop the raw content so native passthrough does not 400.
|
|
39
|
-
|
|
46
|
+
const next: Record<string, unknown> = { ...rec, content: [] };
|
|
47
|
+
if (hasOcxEnvelope) delete next.encrypted_content;
|
|
48
|
+
return next;
|
|
40
49
|
});
|
|
41
50
|
|
|
42
51
|
return changed ? { ...raw, input } : body;
|
|
43
52
|
}
|
|
44
53
|
|
|
54
|
+
/**
|
|
55
|
+
* Replace proxy-minted compaction items (`encrypted_content` starting with `ocx1:`) with plain
|
|
56
|
+
* user messages before forwarding to the ChatGPT backend. Our envelope is transparent base64, not
|
|
57
|
+
* OpenAI encryption — the native backend cannot decrypt it and would reject the request. Real
|
|
58
|
+
* OpenAI-encrypted compaction items are forwarded untouched.
|
|
59
|
+
*/
|
|
60
|
+
function scrubOcxCompactionItems(body: unknown): unknown {
|
|
61
|
+
if (!isPlainObject(body) || !Array.isArray(body.input)) return body;
|
|
62
|
+
|
|
63
|
+
let changed = false;
|
|
64
|
+
const input = body.input.map(item => {
|
|
65
|
+
if (!isPlainObject(item)) return item;
|
|
66
|
+
if (item.type !== "compaction" && item.type !== "compaction_summary" && item.type !== "context_compaction") return item;
|
|
67
|
+
const decoded = typeof item.encrypted_content === "string" ? decodeCompactionSummary(item.encrypted_content) : null;
|
|
68
|
+
if (decoded === null) return item;
|
|
69
|
+
changed = true;
|
|
70
|
+
return {
|
|
71
|
+
type: "message",
|
|
72
|
+
role: "user",
|
|
73
|
+
content: [{ type: "input_text", text: `${SUMMARY_PREFIX}\n\n${decoded}` }],
|
|
74
|
+
};
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
return changed ? { ...body, input } : body;
|
|
78
|
+
}
|
|
79
|
+
|
|
45
80
|
/**
|
|
46
81
|
* Hosted (OpenAI-executed) tool types that specific native slugs reject at request time. Codex
|
|
47
82
|
* attaches these for app skills (e.g. `image_generation` for imagegen) regardless of the target
|
|
@@ -58,6 +93,87 @@ function isPlainObject(v: unknown): v is Record<string, unknown> {
|
|
|
58
93
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
59
94
|
}
|
|
60
95
|
|
|
96
|
+
/** Flatten a Responses tool-output `output` value (string or content-part array) to plain text. */
|
|
97
|
+
function toolOutputText(output: unknown): string {
|
|
98
|
+
if (typeof output === "string") return output;
|
|
99
|
+
if (!Array.isArray(output)) return JSON.stringify(output ?? "");
|
|
100
|
+
return output.map(part => {
|
|
101
|
+
if (!isPlainObject(part)) return "";
|
|
102
|
+
if (typeof part.text === "string") return part.text;
|
|
103
|
+
if (part.type === "refusal" && typeof part.refusal === "string") return `[refusal] ${part.refusal}`;
|
|
104
|
+
return "";
|
|
105
|
+
}).filter(Boolean).join("\n");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Repair a forward-mode input array whose continuation context was lost. When the replay
|
|
110
|
+
* expansion misses (proxy restart, unrecorded prior turn), previous_response_id is stripped
|
|
111
|
+
* (the ChatGPT backend rejects it), so the delta may carry items that reference now-absent
|
|
112
|
+
* prior items and 400 upstream:
|
|
113
|
+
* - `function_call_output`/`custom_tool_call_output` without their paired call item
|
|
114
|
+
* ("No tool call found for function call output with call_id ..."). Converted to user
|
|
115
|
+
* messages so the result text survives. `function_call_output` also pairs with
|
|
116
|
+
* `local_shell_call` (codex-rs emits shell outputs as function_call_output).
|
|
117
|
+
* - `reasoning` items ("Item 'rs_*' ... was provided without its required following item").
|
|
118
|
+
* Dropped, but only when `dropReasoning` (unexpanded miss): on a replay hit the prior
|
|
119
|
+
* reasoning chain is intact and must be preserved.
|
|
120
|
+
* Runs on every forward request; with intact pairs it returns the original reference.
|
|
121
|
+
*/
|
|
122
|
+
function repairOrphanedInputItems(body: unknown, dropReasoning: boolean): unknown {
|
|
123
|
+
if (!isPlainObject(body) || !Array.isArray(body.input)) return body;
|
|
124
|
+
const input = body.input;
|
|
125
|
+
|
|
126
|
+
const functionCallIds = new Set<string>();
|
|
127
|
+
const customCallIds = new Set<string>();
|
|
128
|
+
for (const item of input) {
|
|
129
|
+
if (!isPlainObject(item) || typeof item.call_id !== "string") continue;
|
|
130
|
+
if (item.type === "function_call" || item.type === "local_shell_call") functionCallIds.add(item.call_id);
|
|
131
|
+
else if (item.type === "custom_tool_call") customCallIds.add(item.call_id);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
let changed = false;
|
|
135
|
+
const repaired: unknown[] = [];
|
|
136
|
+
for (const item of input) {
|
|
137
|
+
if (!isPlainObject(item)) { repaired.push(item); continue; }
|
|
138
|
+
if (dropReasoning && item.type === "reasoning") { changed = true; continue; }
|
|
139
|
+
const isFnOutput = item.type === "function_call_output";
|
|
140
|
+
const isCustomOutput = item.type === "custom_tool_call_output";
|
|
141
|
+
if (isFnOutput || isCustomOutput) {
|
|
142
|
+
const callId = typeof item.call_id === "string" ? item.call_id : "";
|
|
143
|
+
const paired = isFnOutput ? functionCallIds.has(callId) : customCallIds.has(callId);
|
|
144
|
+
if (!paired) {
|
|
145
|
+
changed = true;
|
|
146
|
+
repaired.push({
|
|
147
|
+
type: "message",
|
|
148
|
+
role: "user",
|
|
149
|
+
content: [{ type: "input_text", text: `[tool output for ${callId || "unknown call"}]\n${toolOutputText(item.output)}` }],
|
|
150
|
+
});
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
repaired.push(item);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return changed ? { ...body, input: repaired } : body;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Remove `previous_response_id` before forwarding. Two triggers:
|
|
162
|
+
* - the proxy expanded the request into a full input replay (the id is now redundant), or
|
|
163
|
+
* - the target is the ChatGPT backend (`authMode: "forward"`), whose Codex REST endpoint
|
|
164
|
+
* categorically rejects the parameter with `{"detail":"Unsupported parameter:
|
|
165
|
+
* previous_response_id"}` (strict allowlist; it also rejects `metadata` and
|
|
166
|
+
* `max_output_tokens`). Codex only sends the id on WS turns, and ocx converts those to
|
|
167
|
+
* internal HTTP requests, so forwarding it upstream is a guaranteed 400 — stripping is
|
|
168
|
+
* strictly better even when the local replay state missed. API-key mode keeps the field on
|
|
169
|
+
* unexpanded requests: the platform `/v1/responses` supports real server-side storage.
|
|
170
|
+
*/
|
|
171
|
+
function stripPreviousResponseId(body: unknown, strip: boolean): unknown {
|
|
172
|
+
if (!strip || !isPlainObject(body) || !Object.prototype.hasOwnProperty.call(body, "previous_response_id")) return body;
|
|
173
|
+
const { previous_response_id: _previousResponseId, ...rest } = body;
|
|
174
|
+
return rest;
|
|
175
|
+
}
|
|
176
|
+
|
|
61
177
|
/**
|
|
62
178
|
* Remove hosted tool entries the target native slug rejects, so the OAuth-passthrough body never
|
|
63
179
|
* carries a tool the upstream model 400s on. No-op (returns the original reference) when nothing
|
|
@@ -113,11 +229,18 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
|
|
|
113
229
|
if (provider.headers) Object.assign(headers, provider.headers);
|
|
114
230
|
}
|
|
115
231
|
|
|
232
|
+
const forward = provider.authMode === "forward";
|
|
233
|
+
const unexpandedMiss = !!parsed.previousResponseId && parsed._previousResponseInputExpanded !== true;
|
|
234
|
+
let outBody = stripPreviousResponseId(
|
|
235
|
+
parsed._rawBody,
|
|
236
|
+
forward || parsed._previousResponseInputExpanded === true,
|
|
237
|
+
);
|
|
238
|
+
if (forward) outBody = repairOrphanedInputItems(outBody, unexpandedMiss);
|
|
116
239
|
return {
|
|
117
240
|
url,
|
|
118
241
|
method: "POST",
|
|
119
242
|
headers,
|
|
120
|
-
body: JSON.stringify(stripUnsupportedHostedTools(sanitizeReasoningInputContent(
|
|
243
|
+
body: JSON.stringify(stripUnsupportedHostedTools(sanitizeReasoningInputContent(scrubOcxCompactionItems(outBody)))),
|
|
121
244
|
};
|
|
122
245
|
},
|
|
123
246
|
|
package/src/bridge.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { AdapterEvent, OcxUsage } from "./types";
|
|
2
|
-
import { classifyError, type OcxErrorPayload } from "./errors";
|
|
3
|
-
import {
|
|
2
|
+
import { classifyError, type OcxErrorPayload } from "./lib/errors";
|
|
3
|
+
import { encodeCompactionSummary } from "./responses/compaction";
|
|
4
|
+
import { encodeReasoningEnvelope, type ReasoningEnvelope } from "./responses/reasoning-envelope";
|
|
5
|
+
import { usageDisplayTotalTokens, usageInputTokensWithCacheDetail } from "./usage/totals";
|
|
4
6
|
|
|
5
7
|
function uuid(): string {
|
|
6
8
|
return crypto.randomUUID().replace(/-/g, "");
|
|
@@ -62,6 +64,12 @@ export function bridgeToResponsesSSE(
|
|
|
62
64
|
responseId?: string;
|
|
63
65
|
stallTimeoutSec?: number;
|
|
64
66
|
hideThinkingSummary?: boolean;
|
|
67
|
+
/**
|
|
68
|
+
* Remote compaction v2 turn: accumulate all assistant text and, on done, emit ONE synthetic
|
|
69
|
+
* `{type:"compaction", encrypted_content:"ocx1:"+base64(text)}` output item before
|
|
70
|
+
* response.completed — codex-rs collect_compaction_output requires exactly one.
|
|
71
|
+
*/
|
|
72
|
+
compaction?: boolean;
|
|
65
73
|
onTerminal?: (status: ResponsesTerminalStatus) => void;
|
|
66
74
|
onCompletedResponse?: (response: Record<string, unknown>) => void;
|
|
67
75
|
},
|
|
@@ -72,6 +80,34 @@ export function bridgeToResponsesSSE(
|
|
|
72
80
|
try { const o = JSON.parse(args); if (o && typeof o.input === "string") return o.input; } catch { /* raw */ }
|
|
73
81
|
return args;
|
|
74
82
|
};
|
|
83
|
+
// Best-effort unwrap of a PARTIAL freeform arg buffer for live input streaming
|
|
84
|
+
// (`response.custom_tool_call_input.delta` — codex-rs uses it for UI preview only;
|
|
85
|
+
// the completed custom_tool_call item stays authoritative). Compact `{"input":"...`
|
|
86
|
+
// buffers get their string value progressively unescaped; anything else streams raw.
|
|
87
|
+
const FREEFORM_WRAP_PREFIX = '{"input":"';
|
|
88
|
+
const freeformPartialInput = (args: string): string => {
|
|
89
|
+
if (!args.startsWith(FREEFORM_WRAP_PREFIX)) return args;
|
|
90
|
+
const body = args.slice(FREEFORM_WRAP_PREFIX.length);
|
|
91
|
+
let out = "";
|
|
92
|
+
for (let i = 0; i < body.length; i++) {
|
|
93
|
+
const c = body[i];
|
|
94
|
+
if (c === '"') break; // unescaped closing quote: value complete
|
|
95
|
+
if (c === "\\") {
|
|
96
|
+
const n = body[i + 1];
|
|
97
|
+
if (n === undefined) break; // escape split across chunks: wait for more
|
|
98
|
+
i++;
|
|
99
|
+
if (n === "n") out += "\n";
|
|
100
|
+
else if (n === "t") out += "\t";
|
|
101
|
+
else if (n === "r") out += "\r";
|
|
102
|
+
else if (n === "u") {
|
|
103
|
+
const hex = body.slice(i + 1, i + 5);
|
|
104
|
+
if (hex.length === 4 && /^[0-9a-fA-F]{4}$/.test(hex)) { out += String.fromCharCode(parseInt(hex, 16)); i += 4; }
|
|
105
|
+
else break; // incomplete \uXXXX: wait for more
|
|
106
|
+
} else out += n; // \" \\ \/ etc.
|
|
107
|
+
} else out += c;
|
|
108
|
+
}
|
|
109
|
+
return out;
|
|
110
|
+
};
|
|
75
111
|
// tool_search_call carries arguments as a JSON object ({query, limit}); parse the model's arg string.
|
|
76
112
|
const parseArgsObj = (args: string): Record<string, unknown> => {
|
|
77
113
|
try { const o = JSON.parse(args); return o && typeof o === "object" ? o : {}; } catch { return {}; }
|
|
@@ -163,7 +199,40 @@ export function bridgeToResponsesSSE(
|
|
|
163
199
|
let currentMsg: { itemId: string; outputIndex: number; text: string } | null = null;
|
|
164
200
|
let currentReasoning: { itemId: string; outputIndex: number; text: string } | null = null;
|
|
165
201
|
let currentRawReasoning: { itemId: string; outputIndex: number; text: string } | null = null;
|
|
166
|
-
|
|
202
|
+
// Anthropic extended-thinking round-trip state: the signature signs the CURRENT thinking
|
|
203
|
+
// block; redacted blocks are opaque payloads replayed verbatim. Attached to the reasoning
|
|
204
|
+
// item as an ocxr1 encrypted_content envelope on close. hiddenThinkingText collects the
|
|
205
|
+
// suppressed text under hideThinkingSummary so the signed text still round-trips.
|
|
206
|
+
let pendingSignature: string | undefined;
|
|
207
|
+
let pendingRedacted: string[] = [];
|
|
208
|
+
let hiddenThinkingText = "";
|
|
209
|
+
const takeReasoningEnvelope = (hiddenText?: string): string | undefined => {
|
|
210
|
+
if (!pendingSignature && pendingRedacted.length === 0) return undefined;
|
|
211
|
+
const envelope: ReasoningEnvelope = {};
|
|
212
|
+
if (pendingSignature) envelope.sig = pendingSignature;
|
|
213
|
+
if (pendingRedacted.length > 0) envelope.red = pendingRedacted;
|
|
214
|
+
if (hiddenText) envelope.txt = hiddenText;
|
|
215
|
+
pendingSignature = undefined;
|
|
216
|
+
pendingRedacted = [];
|
|
217
|
+
return encodeReasoningEnvelope(envelope);
|
|
218
|
+
};
|
|
219
|
+
// hideThinkingSummary path: no visible reasoning item exists, but a signed thinking block
|
|
220
|
+
// must still round-trip — emit an envelope-only reasoning item (empty summary, no text leak).
|
|
221
|
+
const flushHiddenReasoningEnvelope = () => {
|
|
222
|
+
const encrypted = takeReasoningEnvelope(hiddenThinkingText || undefined);
|
|
223
|
+
hiddenThinkingText = "";
|
|
224
|
+
if (!encrypted) return;
|
|
225
|
+
const itemId = `rs_${uuid()}`;
|
|
226
|
+
const item = { type: "reasoning", id: itemId, summary: [] as never[], encrypted_content: encrypted };
|
|
227
|
+
emit("response.output_item.added", { output_index: outputIndex, item });
|
|
228
|
+
emit("response.output_item.done", { output_index: outputIndex, item });
|
|
229
|
+
finishedItems.push(item as OutputItem);
|
|
230
|
+
outputIndex++;
|
|
231
|
+
};
|
|
232
|
+
// Full assistant text of a compaction turn (across message boundaries) — becomes the
|
|
233
|
+
// synthetic compaction item's payload on done.
|
|
234
|
+
let compactionText = "";
|
|
235
|
+
let currentToolCall: { itemId: string; outputIndex: number; callId: string; name: string; args: string; namespace?: string; freeform?: boolean; toolSearch?: boolean; inputEmitted?: string } | null = null;
|
|
167
236
|
// Open native web-search cell (between begin and end). Holds the output index allocated on
|
|
168
237
|
// begin so the matching done reuses it; closed as `failed` if the stream terminates early.
|
|
169
238
|
let currentWebSearch: { itemId: string; outputIndex: number } | null = null;
|
|
@@ -212,9 +281,11 @@ export function bridgeToResponsesSSE(
|
|
|
212
281
|
item_id: currentReasoning.itemId, output_index: currentReasoning.outputIndex, summary_index: 0,
|
|
213
282
|
part: { type: "summary_text", text: currentReasoning.text },
|
|
214
283
|
});
|
|
284
|
+
const encrypted = takeReasoningEnvelope();
|
|
215
285
|
const item = {
|
|
216
286
|
type: "reasoning", id: currentReasoning.itemId,
|
|
217
287
|
summary: [{ type: "summary_text", text: currentReasoning.text }],
|
|
288
|
+
...(encrypted ? { encrypted_content: encrypted } : {}),
|
|
218
289
|
};
|
|
219
290
|
emit("response.output_item.done", { output_index: currentReasoning.outputIndex, item });
|
|
220
291
|
finishedItems.push(item as OutputItem);
|
|
@@ -246,6 +317,12 @@ export function bridgeToResponsesSSE(
|
|
|
246
317
|
item_id: currentToolCall.itemId, output_index: currentToolCall.outputIndex, arguments: argsStr,
|
|
247
318
|
});
|
|
248
319
|
}
|
|
320
|
+
if (currentToolCall.freeform) {
|
|
321
|
+
emit("response.custom_tool_call_input.done", {
|
|
322
|
+
item_id: currentToolCall.itemId, output_index: currentToolCall.outputIndex,
|
|
323
|
+
input: freeformInput(currentToolCall.args),
|
|
324
|
+
});
|
|
325
|
+
}
|
|
249
326
|
const item = currentToolCall.toolSearch
|
|
250
327
|
? {
|
|
251
328
|
type: "tool_search_call", id: currentToolCall.itemId,
|
|
@@ -295,6 +372,15 @@ export function bridgeToResponsesSSE(
|
|
|
295
372
|
for await (const event of events) {
|
|
296
373
|
activity = true;
|
|
297
374
|
stallTicks = 0;
|
|
375
|
+
// Compaction turns emit ONLY the synthetic compaction item + response.completed. The
|
|
376
|
+
// summary text is accumulated silently: emitting it as a normal assistant message would
|
|
377
|
+
// duplicate the summary if this response is ever replayed via previous_response_id
|
|
378
|
+
// expansion (rememberResponseState stores input + output). Codex ignores extra items but
|
|
379
|
+
// its compaction UI renders nothing mid-turn, so nothing is lost visually.
|
|
380
|
+
if (options?.compaction) {
|
|
381
|
+
if (event.type === "text_delta") { compactionText += event.text; continue; }
|
|
382
|
+
if (event.type !== "done" && event.type !== "error") continue;
|
|
383
|
+
}
|
|
298
384
|
switch (event.type) {
|
|
299
385
|
case "text_delta": {
|
|
300
386
|
if (currentReasoning) closeCurrentReasoning();
|
|
@@ -321,7 +407,7 @@ export function bridgeToResponsesSSE(
|
|
|
321
407
|
break;
|
|
322
408
|
}
|
|
323
409
|
case "thinking_delta": {
|
|
324
|
-
if (options?.hideThinkingSummary) break;
|
|
410
|
+
if (options?.hideThinkingSummary) { hiddenThinkingText += event.thinking; break; }
|
|
325
411
|
if (currentMsg) closeCurrentMessage();
|
|
326
412
|
if (currentRawReasoning) closeCurrentRawReasoning();
|
|
327
413
|
if (currentToolCall) closeCurrentToolCall();
|
|
@@ -342,6 +428,18 @@ export function bridgeToResponsesSSE(
|
|
|
342
428
|
});
|
|
343
429
|
break;
|
|
344
430
|
}
|
|
431
|
+
case "thinking_signature": {
|
|
432
|
+
pendingSignature = event.signature;
|
|
433
|
+
// Signature arrives at the end of the thinking block. With a visible reasoning item
|
|
434
|
+
// open, closeCurrentReasoning attaches the envelope; hidden/suppressed blocks flush
|
|
435
|
+
// an envelope-only reasoning item now.
|
|
436
|
+
if (!currentReasoning) flushHiddenReasoningEnvelope();
|
|
437
|
+
break;
|
|
438
|
+
}
|
|
439
|
+
case "redacted_thinking": {
|
|
440
|
+
pendingRedacted.push(event.data);
|
|
441
|
+
break;
|
|
442
|
+
}
|
|
345
443
|
case "reasoning_raw_delta": {
|
|
346
444
|
if (currentMsg) closeCurrentMessage();
|
|
347
445
|
if (currentReasoning) closeCurrentReasoning();
|
|
@@ -388,6 +486,21 @@ export function bridgeToResponsesSSE(
|
|
|
388
486
|
delta: event.arguments,
|
|
389
487
|
});
|
|
390
488
|
}
|
|
489
|
+
if (currentToolCall.freeform) {
|
|
490
|
+
// Hold while the buffer is still an ambiguous prefix of the JSON wrapper,
|
|
491
|
+
// then stream only the unwrapped input suffix (never rewind on mode flips).
|
|
492
|
+
if (!FREEFORM_WRAP_PREFIX.startsWith(currentToolCall.args)) {
|
|
493
|
+
const full = freeformPartialInput(currentToolCall.args);
|
|
494
|
+
const emitted = currentToolCall.inputEmitted ?? "";
|
|
495
|
+
if (full.startsWith(emitted) && full.length > emitted.length) {
|
|
496
|
+
emit("response.custom_tool_call_input.delta", {
|
|
497
|
+
item_id: currentToolCall.itemId, output_index: currentToolCall.outputIndex,
|
|
498
|
+
delta: full.slice(emitted.length),
|
|
499
|
+
});
|
|
500
|
+
currentToolCall.inputEmitted = full;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
}
|
|
391
504
|
}
|
|
392
505
|
break;
|
|
393
506
|
}
|
|
@@ -438,6 +551,19 @@ export function bridgeToResponsesSSE(
|
|
|
438
551
|
if (currentRawReasoning) closeCurrentRawReasoning();
|
|
439
552
|
if (currentToolCall) closeCurrentToolCall();
|
|
440
553
|
if (currentWebSearch) closeCurrentWebSearch("completed", []);
|
|
554
|
+
// Redacted-only turns (or hidden thinking without a trailing signature event) still
|
|
555
|
+
// need their envelope-only reasoning item so the blocks replay next turn.
|
|
556
|
+
flushHiddenReasoningEnvelope();
|
|
557
|
+
if (options?.compaction) {
|
|
558
|
+
// Exactly one compaction item per turn; codex-rs takes the first and fatals on 0.
|
|
559
|
+
const item = {
|
|
560
|
+
type: "compaction", id: `cmp_${uuid()}`,
|
|
561
|
+
encrypted_content: encodeCompactionSummary(compactionText),
|
|
562
|
+
};
|
|
563
|
+
emit("response.output_item.done", { output_index: outputIndex, item });
|
|
564
|
+
finishedItems.push(item as OutputItem);
|
|
565
|
+
outputIndex++;
|
|
566
|
+
}
|
|
441
567
|
const response = { ...responseSnapshot("completed", finishedItems), usage: responsesUsage(event.usage) };
|
|
442
568
|
options?.onCompletedResponse?.(response);
|
|
443
569
|
emit("response.completed", {
|
|
@@ -528,16 +654,22 @@ export function buildResponseJSON(
|
|
|
528
654
|
toolNsMap?: Map<string, { namespace: string; name: string }>;
|
|
529
655
|
freeformToolNames?: Set<string>;
|
|
530
656
|
toolSearchToolNames?: Set<string>;
|
|
657
|
+
/** Remote compaction v2 turn — append one synthetic compaction output item (see bridgeToResponsesSSE). */
|
|
658
|
+
compaction?: boolean;
|
|
531
659
|
},
|
|
532
660
|
): Record<string, unknown> {
|
|
533
661
|
const responseId = `resp_${uuid()}`;
|
|
534
662
|
const output: OutputItem[] = [];
|
|
535
663
|
let usage: OcxUsage | undefined;
|
|
536
664
|
let errorMessage: string | undefined;
|
|
665
|
+
let compactionText = "";
|
|
537
666
|
|
|
538
667
|
let currentText = "";
|
|
539
668
|
let currentSummaryReasoning = "";
|
|
540
669
|
let currentRawReasoning = "";
|
|
670
|
+
// Anthropic extended-thinking round-trip (batch): see bridgeToResponsesSSE counterpart.
|
|
671
|
+
let batchSignature: string | undefined;
|
|
672
|
+
let batchRedacted: string[] = [];
|
|
541
673
|
let currentToolCallId = "";
|
|
542
674
|
let currentToolCallName = "";
|
|
543
675
|
let currentToolCallArgs = "";
|
|
@@ -565,10 +697,20 @@ export function buildResponseJSON(
|
|
|
565
697
|
currentText = "";
|
|
566
698
|
};
|
|
567
699
|
const flushSummaryReasoning = () => {
|
|
568
|
-
if (!currentSummaryReasoning
|
|
700
|
+
if (!currentSummaryReasoning && !batchSignature && batchRedacted.length === 0) return;
|
|
701
|
+
const envelope: ReasoningEnvelope = {};
|
|
702
|
+
if (batchSignature) envelope.sig = batchSignature;
|
|
703
|
+
if (batchRedacted.length > 0) envelope.red = batchRedacted;
|
|
704
|
+
const hidden = options?.hideThinkingSummary === true;
|
|
705
|
+
if (hidden && currentSummaryReasoning && (envelope.sig || envelope.red)) envelope.txt = currentSummaryReasoning;
|
|
706
|
+
const encrypted = envelope.sig || envelope.red || envelope.txt ? encodeReasoningEnvelope(envelope) : undefined;
|
|
707
|
+
batchSignature = undefined;
|
|
708
|
+
batchRedacted = [];
|
|
709
|
+
if (hidden && !encrypted) { currentSummaryReasoning = ""; return; }
|
|
569
710
|
output.push({
|
|
570
711
|
type: "reasoning", id: `rs_${uuid()}`,
|
|
571
|
-
summary: [{ type: "summary_text", text: currentSummaryReasoning }],
|
|
712
|
+
summary: !hidden && currentSummaryReasoning ? [{ type: "summary_text", text: currentSummaryReasoning }] : [],
|
|
713
|
+
...(encrypted ? { encrypted_content: encrypted } : {}),
|
|
572
714
|
});
|
|
573
715
|
currentSummaryReasoning = "";
|
|
574
716
|
};
|
|
@@ -618,7 +760,10 @@ export function buildResponseJSON(
|
|
|
618
760
|
if (currentSummaryReasoning) flushSummaryReasoning();
|
|
619
761
|
if (currentRawReasoning) flushRawReasoning();
|
|
620
762
|
if (currentToolCallId) flushToolCall();
|
|
621
|
-
|
|
763
|
+
// Compaction turns keep the summary out of normal message output (replay dedup — see
|
|
764
|
+
// bridgeToResponsesSSE); it ships only inside the synthetic compaction item below.
|
|
765
|
+
if (options?.compaction) compactionText += e.text;
|
|
766
|
+
else currentText += e.text;
|
|
622
767
|
break;
|
|
623
768
|
case "thinking_delta":
|
|
624
769
|
if (currentText) flushText();
|
|
@@ -626,6 +771,15 @@ export function buildResponseJSON(
|
|
|
626
771
|
if (currentToolCallId) flushToolCall();
|
|
627
772
|
currentSummaryReasoning += e.thinking;
|
|
628
773
|
break;
|
|
774
|
+
case "thinking_signature":
|
|
775
|
+
// End of the current thinking block — flush it WITH the signature envelope so the
|
|
776
|
+
// block/signature pairing survives multi-block turns.
|
|
777
|
+
batchSignature = e.signature;
|
|
778
|
+
flushSummaryReasoning();
|
|
779
|
+
break;
|
|
780
|
+
case "redacted_thinking":
|
|
781
|
+
batchRedacted.push(e.data);
|
|
782
|
+
break;
|
|
629
783
|
case "reasoning_raw_delta":
|
|
630
784
|
if (currentText) flushText();
|
|
631
785
|
if (currentSummaryReasoning) flushSummaryReasoning();
|
|
@@ -679,6 +833,9 @@ export function buildResponseJSON(
|
|
|
679
833
|
flushSummaryReasoning();
|
|
680
834
|
flushRawReasoning();
|
|
681
835
|
flushToolCall();
|
|
836
|
+
if (options?.compaction && !errorMessage) {
|
|
837
|
+
output.push({ type: "compaction", id: `cmp_${uuid()}`, encrypted_content: encodeCompactionSummary(compactionText) });
|
|
838
|
+
}
|
|
682
839
|
|
|
683
840
|
return {
|
|
684
841
|
id: responseId, object: "response",
|
|
@@ -9,10 +9,11 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { existsSync, readFileSync } from "node:fs";
|
|
11
11
|
import { join } from "node:path";
|
|
12
|
-
import { getConfigDir, getConfigPath, readConfigDiagnostics, readPid, resolveEnvValue } from "
|
|
13
|
-
import { readCodexTokens } from "
|
|
14
|
-
import { resolveCodexHomeDir as resolveCodexHomeDirImpl } from "
|
|
15
|
-
|
|
12
|
+
import { getConfigDir, getConfigPath, readConfigDiagnostics, readPid, resolveEnvValue } from "../config";
|
|
13
|
+
import { readCodexTokens } from "../codex/auth-collision";
|
|
14
|
+
import { resolveCodexHomeDir as resolveCodexHomeDirImpl } from "../codex/home";
|
|
15
|
+
import { countPendingOpencodexHistory } from "../codex/history-provider";
|
|
16
|
+
export { resolveCodexHomeDir } from "../codex/home";
|
|
16
17
|
|
|
17
18
|
const WHAM_USAGE_URL = "https://chatgpt.com/backend-api/wham/usage";
|
|
18
19
|
const PROBE_TIMEOUT_MS = 8000;
|
|
@@ -293,6 +294,19 @@ export async function runDoctor(): Promise<void> {
|
|
|
293
294
|
console.log(` ${probe.ok ? "ok " : "-- "} ${WHAM_USAGE_URL}`);
|
|
294
295
|
console.log(` ${detail}, ${probe.durationMs}ms, ${probe.authenticated ? "authenticated" : "unauthenticated"}`);
|
|
295
296
|
|
|
297
|
+
// Design B upgrade visibility: threads still tagged opencodex are invisible to the native
|
|
298
|
+
// Codex app until the one-time migration lands. Read-only probe (readonly sqlite, 100ms
|
|
299
|
+
// busy timeout) — reports state, never mutates.
|
|
300
|
+
console.log("\nCodex history migration");
|
|
301
|
+
const pending = countPendingOpencodexHistory();
|
|
302
|
+
if (pending.failed) {
|
|
303
|
+
console.log(" -- state DB locked or unreadable (Codex app open?) — migration state unknown");
|
|
304
|
+
} else if (pending.pendingRows === 0 && pending.backupEntries === 0) {
|
|
305
|
+
console.log(" ok no legacy opencodex-tagged threads pending");
|
|
306
|
+
} else {
|
|
307
|
+
console.log(` -- ${pending.pendingRows} thread(s) still tagged opencodex, ${pending.backupEntries} backup manifest entr${pending.backupEntries === 1 ? "y" : "ies"}`);
|
|
308
|
+
}
|
|
309
|
+
|
|
296
310
|
// Hints, not fixes.
|
|
297
311
|
const hints: string[] = [];
|
|
298
312
|
const anyDrvfs = paths.some(p => detectFsType(p.path, mounts).isDrvfs || detectFsType(p.path, mounts).isMntDrive);
|
|
@@ -308,6 +322,9 @@ export async function runDoctor(): Promise<void> {
|
|
|
308
322
|
}
|
|
309
323
|
}
|
|
310
324
|
}
|
|
325
|
+
if (pending.failed || pending.pendingRows > 0 || pending.backupEntries > 0) {
|
|
326
|
+
hints.push("Legacy chat threads are still tagged opencodex (or the DB was locked). The running proxy retries the migration automatically; to force it now, close the Codex app and run 'ocx sync'.");
|
|
327
|
+
}
|
|
311
328
|
if (hints.length > 0) {
|
|
312
329
|
console.log("\nHints");
|
|
313
330
|
for (const h of hints) console.log(` - ${h}`);
|
|
@@ -2,7 +2,7 @@ import { readFileSync } from "node:fs";
|
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
|
|
5
|
-
const repoRoot = dirname(fileURLToPath(new URL("
|
|
5
|
+
const repoRoot = dirname(fileURLToPath(new URL("../../package.json", import.meta.url)));
|
|
6
6
|
|
|
7
7
|
type HelpEntry = {
|
|
8
8
|
usage: string;
|