@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,11 +1,12 @@
|
|
|
1
1
|
import type { ServerWebSocket } from "bun";
|
|
2
|
-
import { FORWARD_HEADERS } from "
|
|
3
|
-
import type { CodexAuthContext } from "
|
|
4
|
-
import { headersForCodexAuthContext } from "
|
|
5
|
-
import type { ResponsesTerminalStatus } from "
|
|
2
|
+
import { FORWARD_HEADERS } from "../adapters/openai-responses";
|
|
3
|
+
import type { CodexAuthContext } from "../codex/auth-context";
|
|
4
|
+
import { headersForCodexAuthContext } from "../codex/auth-context";
|
|
5
|
+
import type { ResponsesTerminalStatus } from "../bridge";
|
|
6
6
|
|
|
7
7
|
const OPEN = 1;
|
|
8
8
|
type ResponsesTerminalReporter = (status: ResponsesTerminalStatus) => void;
|
|
9
|
+
type ResponsesPayloadObserver = (payload: string) => void;
|
|
9
10
|
const SAFE_RESPONSE_HEADER_EXACT = new Set([
|
|
10
11
|
"retry-after",
|
|
11
12
|
"x-request-id",
|
|
@@ -169,7 +170,11 @@ function sendProtocolError(ws: ServerWebSocket<WsData>, status: number, message:
|
|
|
169
170
|
export async function pumpResponsesSseToWebSocket(
|
|
170
171
|
ws: ServerWebSocket<WsData>,
|
|
171
172
|
sseStream: ReadableStream<Uint8Array>,
|
|
172
|
-
options: {
|
|
173
|
+
options: {
|
|
174
|
+
isCurrent?: () => boolean;
|
|
175
|
+
onTerminal?: ResponsesTerminalReporter;
|
|
176
|
+
onSsePayload?: ResponsesPayloadObserver;
|
|
177
|
+
} = {},
|
|
173
178
|
): Promise<void> {
|
|
174
179
|
const reader = sseStream.getReader();
|
|
175
180
|
const isCurrent = options.isCurrent ?? (() => true);
|
|
@@ -193,6 +198,11 @@ export async function pumpResponsesSseToWebSocket(
|
|
|
193
198
|
const handlePayload = (payload: string): boolean => {
|
|
194
199
|
if (!isCurrent()) return true;
|
|
195
200
|
if (payload === "[DONE]") return false;
|
|
201
|
+
try {
|
|
202
|
+
options.onSsePayload?.(payload);
|
|
203
|
+
} catch {
|
|
204
|
+
/* payload observation must not affect WebSocket delivery */
|
|
205
|
+
}
|
|
196
206
|
const type = payloadType(payload);
|
|
197
207
|
if (!type) {
|
|
198
208
|
reportTerminal("incomplete");
|
|
@@ -248,14 +258,24 @@ export function sendResponsesJsonAsEvents(
|
|
|
248
258
|
ws: ServerWebSocket<WsData>,
|
|
249
259
|
response: Record<string, unknown>,
|
|
250
260
|
onTerminal?: ResponsesTerminalReporter,
|
|
261
|
+
onPayload?: ResponsesPayloadObserver,
|
|
251
262
|
): void {
|
|
263
|
+
const sendObservedFrame = (payload: Record<string, unknown>) => {
|
|
264
|
+
const text = JSON.stringify(payload);
|
|
265
|
+
try {
|
|
266
|
+
onPayload?.(text);
|
|
267
|
+
} catch {
|
|
268
|
+
/* payload observation must not affect WebSocket delivery */
|
|
269
|
+
}
|
|
270
|
+
sendTextFrame(ws, text);
|
|
271
|
+
};
|
|
252
272
|
const output = Array.isArray(response.output) ? response.output : [];
|
|
253
|
-
|
|
273
|
+
sendObservedFrame({
|
|
254
274
|
type: "response.created",
|
|
255
275
|
response: { ...response, status: "in_progress", output: [] },
|
|
256
276
|
});
|
|
257
277
|
output.forEach((item, outputIndex) => {
|
|
258
|
-
|
|
278
|
+
sendObservedFrame({
|
|
259
279
|
type: "response.output_item.done",
|
|
260
280
|
output_index: outputIndex,
|
|
261
281
|
item,
|
|
@@ -264,7 +284,7 @@ export function sendResponsesJsonAsEvents(
|
|
|
264
284
|
const finalStatus = response.status === "failed" || response.status === "incomplete"
|
|
265
285
|
? response.status
|
|
266
286
|
: "completed";
|
|
267
|
-
|
|
287
|
+
sendObservedFrame({
|
|
268
288
|
type: `response.${finalStatus}` as "response.completed" | "response.failed" | "response.incomplete",
|
|
269
289
|
response: { ...response, status: finalStatus },
|
|
270
290
|
});
|
|
@@ -290,7 +310,10 @@ export async function sendResponseToWebSocket(
|
|
|
290
310
|
ws: ServerWebSocket<WsData>,
|
|
291
311
|
response: Response,
|
|
292
312
|
isCurrent: () => boolean,
|
|
293
|
-
options: {
|
|
313
|
+
options: {
|
|
314
|
+
onTerminal?: ResponsesTerminalReporter;
|
|
315
|
+
onSsePayload?: ResponsesPayloadObserver;
|
|
316
|
+
} = {},
|
|
294
317
|
): Promise<void> {
|
|
295
318
|
if (!isCurrent()) {
|
|
296
319
|
await response.body?.cancel().catch(() => {});
|
|
@@ -316,7 +339,11 @@ export async function sendResponseToWebSocket(
|
|
|
316
339
|
}
|
|
317
340
|
|
|
318
341
|
if (contentType.includes("text/event-stream")) {
|
|
319
|
-
await pumpResponsesSseToWebSocket(ws, response.body, {
|
|
342
|
+
await pumpResponsesSseToWebSocket(ws, response.body, {
|
|
343
|
+
isCurrent,
|
|
344
|
+
onTerminal: options.onTerminal,
|
|
345
|
+
onSsePayload: options.onSsePayload,
|
|
346
|
+
});
|
|
320
347
|
return;
|
|
321
348
|
}
|
|
322
349
|
|
|
@@ -324,7 +351,7 @@ export async function sendResponseToWebSocket(
|
|
|
324
351
|
const text = await response.text();
|
|
325
352
|
if (!isCurrent()) return;
|
|
326
353
|
const json = JSON.parse(text) as Record<string, unknown>;
|
|
327
|
-
sendResponsesJsonAsEvents(ws, json, options.onTerminal);
|
|
354
|
+
sendResponsesJsonAsEvents(ws, json, options.onTerminal, options.onSsePayload);
|
|
328
355
|
return;
|
|
329
356
|
}
|
|
330
357
|
|
|
@@ -334,7 +361,11 @@ export async function sendResponseToWebSocket(
|
|
|
334
361
|
return;
|
|
335
362
|
}
|
|
336
363
|
if (looksLikeSse(prefix)) {
|
|
337
|
-
await pumpResponsesSseToWebSocket(ws, stream, {
|
|
364
|
+
await pumpResponsesSseToWebSocket(ws, stream, {
|
|
365
|
+
isCurrent,
|
|
366
|
+
onTerminal: options.onTerminal,
|
|
367
|
+
onSsePayload: options.onSsePayload,
|
|
368
|
+
});
|
|
338
369
|
return;
|
|
339
370
|
}
|
|
340
371
|
|
|
@@ -343,7 +374,7 @@ export async function sendResponseToWebSocket(
|
|
|
343
374
|
const trimmed = text.trim();
|
|
344
375
|
if (trimmed.startsWith("{")) {
|
|
345
376
|
const json = JSON.parse(trimmed) as Record<string, unknown>;
|
|
346
|
-
sendResponsesJsonAsEvents(ws, json, options.onTerminal);
|
|
377
|
+
sendResponsesJsonAsEvents(ws, json, options.onTerminal, options.onSsePayload);
|
|
347
378
|
return;
|
|
348
379
|
}
|
|
349
380
|
|
package/src/service.ts
CHANGED
|
@@ -11,11 +11,11 @@ import { homedir } from "node:os";
|
|
|
11
11
|
import { dirname, join, resolve } from "node:path";
|
|
12
12
|
import { expandUserPath, getConfigDir, readPid, removePid, removeRuntimePort } from "./config";
|
|
13
13
|
import { loadConfig } from "./config";
|
|
14
|
-
import { restoreNativeCodex } from "./codex
|
|
15
|
-
import { durableBunPath, durableBunRuntime } from "./bun-runtime";
|
|
16
|
-
import { isProcessAlive, stopProxy } from "./process-control";
|
|
17
|
-
import { serviceApiTokenFilePath } from "./service-secrets";
|
|
18
|
-
import { windowsEnvIndirectBatchPathList, windowsEnvIndirectBatchValue } from "./win-paths";
|
|
14
|
+
import { restoreNativeCodex } from "./codex/inject";
|
|
15
|
+
import { durableBunPath, durableBunRuntime } from "./lib/bun-runtime";
|
|
16
|
+
import { isProcessAlive, stopProxy } from "./lib/process-control";
|
|
17
|
+
import { serviceApiTokenFilePath } from "./lib/service-secrets";
|
|
18
|
+
import { windowsEnvIndirectBatchPathList, windowsEnvIndirectBatchValue } from "./lib/win-paths";
|
|
19
19
|
|
|
20
20
|
const LABEL = "com.opencodex.proxy";
|
|
21
21
|
const TASK = "opencodex-proxy";
|
|
@@ -23,8 +23,8 @@ const TASK = "opencodex-proxy";
|
|
|
23
23
|
function cliEntry(): { bun: string; cli: string } {
|
|
24
24
|
// Bake the bundled Bun (npm global prefix, survives `ocx update`) rather than
|
|
25
25
|
// a transient system Bun, so launchd/systemd/schtasks keep resolving even if a
|
|
26
|
-
// standalone Bun is later removed.
|
|
27
|
-
return { bun: durableBunPath(), cli: join(import.meta.dir, "cli.ts") };
|
|
26
|
+
// standalone Bun is later removed. The CLI entry lives at src/cli/index.ts.
|
|
27
|
+
return { bun: durableBunPath(), cli: join(import.meta.dir, "cli", "index.ts") };
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
function plistPath(): string {
|
|
@@ -709,8 +709,11 @@ export async function serviceCommand(sub?: string): Promise<void> {
|
|
|
709
709
|
assertServiceEnvironmentMatchesInstall();
|
|
710
710
|
ops.stop();
|
|
711
711
|
await stopTrackedProxyForServiceCommand();
|
|
712
|
-
|
|
713
|
-
|
|
712
|
+
{
|
|
713
|
+
const restore = restoreNativeCodex();
|
|
714
|
+
if (restore.success) console.log("✅ service stopped + native Codex restored.");
|
|
715
|
+
else console.error(`⚠️ service stopped, but native Codex restore FAILED: ${restore.message}\nRun \`ocx restore\` (or check $CODEX_HOME/config.toml) before using native Codex.`);
|
|
716
|
+
}
|
|
714
717
|
break;
|
|
715
718
|
case "status": {
|
|
716
719
|
const s = ops.status();
|
|
@@ -724,10 +727,15 @@ export async function serviceCommand(sub?: string): Promise<void> {
|
|
|
724
727
|
ops.stop();
|
|
725
728
|
await stopTrackedProxyForServiceCommand();
|
|
726
729
|
ops.uninstall();
|
|
727
|
-
|
|
730
|
+
{
|
|
731
|
+
const restore = restoreNativeCodex();
|
|
732
|
+
if (!restore.success) {
|
|
733
|
+
console.error(`⚠️ native Codex restore FAILED: ${restore.message}\nRun \`ocx restore\` before using native Codex.`);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
728
736
|
removeServiceInstallState();
|
|
729
737
|
try { if (existsSync(serviceApiTokenFilePath())) unlinkSync(serviceApiTokenFilePath()); } catch { /* best-effort */ }
|
|
730
|
-
console.log("✅ service uninstalled
|
|
738
|
+
console.log("✅ service uninstalled.");
|
|
731
739
|
break;
|
|
732
740
|
default:
|
|
733
741
|
console.error("Usage: ocx service [install|start|stop|status|uninstall|remove]");
|
package/src/types.ts
CHANGED
|
@@ -5,6 +5,8 @@ export interface OcxParsedRequest {
|
|
|
5
5
|
stream: boolean;
|
|
6
6
|
options: OcxRequestOptions;
|
|
7
7
|
_rawBody?: unknown;
|
|
8
|
+
/** True when the proxy expanded a previous_response_id request into a full input replay. */
|
|
9
|
+
_previousResponseInputExpanded?: boolean;
|
|
8
10
|
/** Provider-private stable Cursor conversation id resolved from the Responses previous_response_id chain. */
|
|
9
11
|
_cursorConversationId?: string;
|
|
10
12
|
/**
|
|
@@ -19,6 +21,13 @@ export interface OcxParsedRequest {
|
|
|
19
21
|
* answer/"Sources:" text can't bleed into and corrupt the model's schema-constrained output.
|
|
20
22
|
*/
|
|
21
23
|
_structuredOutput?: boolean;
|
|
24
|
+
/**
|
|
25
|
+
* True when the input carried `{type:"compaction_trigger"}` — Codex remote compaction v2 asking
|
|
26
|
+
* this turn to produce a `{type:"compaction"}` output item. Routed adapters can't natively;
|
|
27
|
+
* the server runs the model as a summarizer and the bridge emits a synthetic compaction item
|
|
28
|
+
* (see src/responses/compaction.ts).
|
|
29
|
+
*/
|
|
30
|
+
_compactionRequest?: boolean;
|
|
22
31
|
}
|
|
23
32
|
|
|
24
33
|
export interface OcxContext {
|
|
@@ -85,6 +94,8 @@ export interface OcxThinkingContent {
|
|
|
85
94
|
thinking: string;
|
|
86
95
|
signature?: string;
|
|
87
96
|
itemId?: string;
|
|
97
|
+
/** Raw Anthropic redacted_thinking block payloads to replay verbatim (order preserved). */
|
|
98
|
+
redacted?: string[];
|
|
88
99
|
}
|
|
89
100
|
|
|
90
101
|
export interface OcxToolCall {
|
|
@@ -182,6 +193,10 @@ export type AdapterEvent =
|
|
|
182
193
|
| { type: "heartbeat" }
|
|
183
194
|
| { type: "text_delta"; text: string }
|
|
184
195
|
| { type: "thinking_delta"; thinking: string }
|
|
196
|
+
// Anthropic extended-thinking round-trip: signature_delta for the current thinking block, and
|
|
197
|
+
// opaque redacted_thinking blocks. Both must be replayed verbatim or tool-use turns 400.
|
|
198
|
+
| { type: "thinking_signature"; signature: string }
|
|
199
|
+
| { type: "redacted_thinking"; data: string }
|
|
185
200
|
| { type: "reasoning_raw_delta"; text: string }
|
|
186
201
|
| { type: "tool_call_start"; id: string; name: string }
|
|
187
202
|
| { type: "tool_call_delta"; arguments: string }
|
|
@@ -337,6 +352,12 @@ export interface OcxProviderConfig {
|
|
|
337
352
|
/** Keep provider settings on disk but exclude it from routing and model/catalog listings. */
|
|
338
353
|
disabled?: boolean;
|
|
339
354
|
apiKey?: string;
|
|
355
|
+
/**
|
|
356
|
+
* Multi-key pool (API-key twin of OAuth multiauth). `apiKey` always mirrors the ACTIVE
|
|
357
|
+
* entry so routing stays single-key; managed via /api/providers/keys. A legacy bare
|
|
358
|
+
* `apiKey` seeds a one-entry pool on first management touch.
|
|
359
|
+
*/
|
|
360
|
+
apiKeyPool?: Array<{ id: string; key: string; label?: string; addedAt?: number }>;
|
|
340
361
|
defaultModel?: string;
|
|
341
362
|
models?: string[];
|
|
342
363
|
/**
|
|
@@ -400,6 +421,12 @@ export interface OcxProviderConfig {
|
|
|
400
421
|
autoToolChoiceOnlyModels?: string[];
|
|
401
422
|
/** Model ids that expect prior assistant `reasoning_content` to be preserved in chat history. */
|
|
402
423
|
preserveReasoningContentModels?: string[];
|
|
424
|
+
/**
|
|
425
|
+
* Model ids whose reasoning is a vendor `thinking: {type: enabled|disabled}` toggle on the
|
|
426
|
+
* chat-completions wire (MiMo v2.x, GLM 5/5.1 style), NOT an OpenAI `reasoning_effort` ladder.
|
|
427
|
+
* The openai-chat adapter translates the mapped effort into the thinking toggle for these.
|
|
428
|
+
*/
|
|
429
|
+
thinkingToggleModels?: string[];
|
|
403
430
|
/** Anthropic-compatible gateways that need custom tool names escaped on the wire. */
|
|
404
431
|
escapeBuiltinToolNames?: boolean;
|
|
405
432
|
/**
|
|
@@ -2,7 +2,7 @@ import { spawnSync } from "node:child_process";
|
|
|
2
2
|
import { readFileSync, readdirSync } from "node:fs";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
import { dirname, join } from "node:path";
|
|
5
|
-
import { getConfigDir, readPid, readRuntimePort } from "
|
|
5
|
+
import { getConfigDir, readPid, readRuntimePort } from "../config";
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* A `codex-history-backup-*.json` surviving a stop means the native-history restore was
|
|
@@ -19,7 +19,7 @@ export function historyRestoreIncomplete(configDir = getConfigDir()): boolean {
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
export const PKG = "@bitkyc08/opencodex";
|
|
22
|
-
const HERE = dirname(fileURLToPath(import.meta.url)); // .../opencodex/src
|
|
22
|
+
const HERE = dirname(fileURLToPath(import.meta.url)); // .../opencodex/src/update
|
|
23
23
|
|
|
24
24
|
export type Installer = "bun" | "npm" | "source";
|
|
25
25
|
export type Channel = "latest" | "preview";
|
|
@@ -32,7 +32,7 @@ export function detectInstall(): Installer {
|
|
|
32
32
|
|
|
33
33
|
export function currentVersion(): string {
|
|
34
34
|
try {
|
|
35
|
-
return (JSON.parse(readFileSync(join(HERE, "..", "package.json"), "utf8")).version as string) ?? "?";
|
|
35
|
+
return (JSON.parse(readFileSync(join(HERE, "..", "..", "package.json"), "utf8")).version as string) ?? "?";
|
|
36
36
|
} catch {
|
|
37
37
|
return "?";
|
|
38
38
|
}
|
|
@@ -105,7 +105,7 @@ export async function runUpdate(): Promise<void> {
|
|
|
105
105
|
// unloads it permanently, so a successful update must reinstall/restart it afterwards.
|
|
106
106
|
let serviceWasInstalled = false;
|
|
107
107
|
try {
|
|
108
|
-
const { isServiceInstalled } = await import("
|
|
108
|
+
const { isServiceInstalled } = await import("../service");
|
|
109
109
|
serviceWasInstalled = isServiceInstalled();
|
|
110
110
|
} catch { /* best-effort */ }
|
|
111
111
|
|
|
@@ -140,7 +140,7 @@ export async function runUpdate(): Promise<void> {
|
|
|
140
140
|
// Re-bake the bundled Bun path into the Codex autostart shim on every
|
|
141
141
|
// platform when one is installed (refresh-only; never installs fresh).
|
|
142
142
|
try {
|
|
143
|
-
const { isCodexShimInstalled, installCodexShim } = await import("
|
|
143
|
+
const { isCodexShimInstalled, installCodexShim } = await import("../codex/shim");
|
|
144
144
|
if (isCodexShimInstalled()) {
|
|
145
145
|
const result = installCodexShim();
|
|
146
146
|
if (result.installed) console.log(`🔧 ${result.message}`);
|
|
@@ -2,9 +2,9 @@ import { spawn, spawnSync } from "node:child_process";
|
|
|
2
2
|
import { existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
|
-
import { atomicWriteFile, getConfigDir, readPid } from "
|
|
6
|
-
import { killProxy } from "
|
|
7
|
-
import { isServiceInstalled } from "
|
|
5
|
+
import { atomicWriteFile, getConfigDir, readPid } from "../config";
|
|
6
|
+
import { killProxy } from "../lib/process-control";
|
|
7
|
+
import { isServiceInstalled } from "../service";
|
|
8
8
|
import {
|
|
9
9
|
type Channel,
|
|
10
10
|
type Installer,
|
|
@@ -14,8 +14,8 @@ import {
|
|
|
14
14
|
latestVersion,
|
|
15
15
|
updateCommand,
|
|
16
16
|
updateCommandStr,
|
|
17
|
-
} from "./
|
|
18
|
-
import { isNewer } from "./
|
|
17
|
+
} from "./index";
|
|
18
|
+
import { isNewer } from "./notify";
|
|
19
19
|
|
|
20
20
|
const RELEASE_NOTES_URL = "https://github.com/lidge-jun/opencodex/releases/latest";
|
|
21
21
|
const UPDATE_JOB_FILENAME = "update-job.json";
|
|
@@ -79,7 +79,8 @@ function nodeBin(): string {
|
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
function packageLauncherPath(): string {
|
|
82
|
-
|
|
82
|
+
// This module lives at src/update/job.ts — the launcher is <pkg-root>/bin/ocx.mjs.
|
|
83
|
+
return join(dirname(fileURLToPath(import.meta.url)), "..", "..", "bin", "ocx.mjs");
|
|
83
84
|
}
|
|
84
85
|
|
|
85
86
|
function formatCommand(bin: string, args: string[]): string {
|
|
@@ -2,8 +2,8 @@ import { spawn } from "node:child_process";
|
|
|
2
2
|
import { existsSync, readFileSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { createInterface } from "node:readline/promises";
|
|
5
|
-
import { atomicWriteFile, getConfigDir } from "
|
|
6
|
-
import { hasStarPromptRun } from "
|
|
5
|
+
import { atomicWriteFile, getConfigDir } from "../config";
|
|
6
|
+
import { hasStarPromptRun } from "../cli/star-prompt";
|
|
7
7
|
import {
|
|
8
8
|
type Channel,
|
|
9
9
|
currentVersion,
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
runUpdate,
|
|
13
13
|
updateCommandStr,
|
|
14
14
|
updateTag,
|
|
15
|
-
} from "./
|
|
15
|
+
} from "./index";
|
|
16
16
|
|
|
17
17
|
const VERSION_FILENAME = "version.json";
|
|
18
18
|
const REFRESH_INTERVAL_MS = 20 * 60 * 60 * 1000; // 20h, matching codex-rs
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { appendFileSync, chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import { getConfigDir } from "
|
|
4
|
-
import { redactSecretString, redactSecrets } from "
|
|
5
|
-
import type { OcxUsage } from "
|
|
3
|
+
import { getConfigDir } from "../config";
|
|
4
|
+
import { redactSecretString, redactSecrets } from "../lib/redact";
|
|
5
|
+
import type { OcxUsage } from "../types";
|
|
6
6
|
|
|
7
7
|
export const USAGE_DEBUG_ENV = "OPENCODEX_USAGE_DEBUG";
|
|
8
8
|
export const USAGE_DEBUG_BODY_SAMPLE_BYTES = 2048;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { chmodSync, existsSync, mkdirSync, readFileSync, appendFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import { getConfigDir } from "
|
|
4
|
-
import { usageDisplayTotalTokens } from "./
|
|
5
|
-
import type { OcxUsage } from "
|
|
3
|
+
import { getConfigDir } from "../config";
|
|
4
|
+
import { usageDisplayTotalTokens } from "./totals";
|
|
5
|
+
import type { OcxUsage } from "../types";
|
|
6
6
|
|
|
7
7
|
export type UsageStatus = "reported" | "unreported" | "unsupported" | "estimated";
|
|
8
8
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { baseProviderLabel } from "
|
|
2
|
-
import { usageDisplayTotalTokens } from "./
|
|
3
|
-
import type { PersistedUsageEntry, UsageStatus } from "./
|
|
1
|
+
import { baseProviderLabel } from "../providers/label";
|
|
2
|
+
import { usageDisplayTotalTokens } from "./totals";
|
|
3
|
+
import type { PersistedUsageEntry, UsageStatus } from "./log";
|
|
4
4
|
|
|
5
5
|
export type UsageRange = "7d" | "30d" | "all";
|
|
6
6
|
|
package/src/vision/describe.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { OcxProviderConfig } from "../types";
|
|
2
2
|
import { FORWARD_HEADERS } from "../adapters/openai-responses";
|
|
3
|
-
import { signalWithTimeout, cancelBodyOnAbort } from "../abort";
|
|
4
|
-
import { sidecarEnter } from "../sidecar-tracker";
|
|
5
|
-
import { fetchWithResetRetry } from "../upstream-retry";
|
|
3
|
+
import { signalWithTimeout, cancelBodyOnAbort } from "../lib/abort";
|
|
4
|
+
import { sidecarEnter } from "../lib/sidecar-tracker";
|
|
5
|
+
import { fetchWithResetRetry } from "../lib/upstream-retry";
|
|
6
6
|
import { parseSidecarSSE } from "../web-search/parse";
|
|
7
7
|
import type { SidecarOutcomeRecorder } from "../web-search/executor";
|
|
8
8
|
|
package/src/vision/index.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { OcxConfig, OcxContentPart, OcxMessage, OcxParsedRequest, OcxProviderConfig, OcxTextContent } from "../types";
|
|
2
2
|
import { modelInList } from "../types";
|
|
3
3
|
import { describeImage, type VisionSettings } from "./describe";
|
|
4
|
-
import type { CodexAuthContext } from "../codex
|
|
4
|
+
import type { CodexAuthContext } from "../codex/auth-context";
|
|
5
5
|
import type { SidecarOutcomeRecorder } from "../web-search/executor";
|
|
6
6
|
|
|
7
7
|
export { describeImage } from "./describe";
|
|
@@ -145,3 +145,23 @@ export async function describeImagesInPlace(
|
|
|
145
145
|
msg.content = newParts;
|
|
146
146
|
}
|
|
147
147
|
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Fail-closed image strip for sidecar-covered models when NO sidecar plan exists (no forward
|
|
151
|
+
* provider / missing forwarded auth / sidecar disabled): the upstream is text-only, so forwarding
|
|
152
|
+
* raw images would 400 or silently confuse it. Replace each image with an explicit marker so the
|
|
153
|
+
* model (and the user, via its reply) knows the image was dropped rather than ignored.
|
|
154
|
+
*/
|
|
155
|
+
export function stripImagesInPlace(parsed: OcxParsedRequest): boolean {
|
|
156
|
+
let stripped = false;
|
|
157
|
+
for (const msg of parsed.context.messages) {
|
|
158
|
+
if (!carriesImages(msg.role) || !Array.isArray(msg.content)) continue;
|
|
159
|
+
const parts = msg.content as OcxContentPart[];
|
|
160
|
+
if (!parts.some(p => p.type === "image")) continue;
|
|
161
|
+
msg.content = parts.map(p => p.type === "image"
|
|
162
|
+
? { type: "text", text: "[image omitted: this model is text-only and the vision sidecar is unavailable (no ChatGPT login)]" } as OcxContentPart
|
|
163
|
+
: p);
|
|
164
|
+
stripped = true;
|
|
165
|
+
}
|
|
166
|
+
return stripped;
|
|
167
|
+
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import type { OcxProviderConfig } from "../types";
|
|
2
2
|
import { FORWARD_HEADERS } from "../adapters/openai-responses";
|
|
3
|
-
import { signalWithTimeout, cancelBodyOnAbort } from "../abort";
|
|
4
|
-
import { sidecarEnter } from "../sidecar-tracker";
|
|
5
|
-
import { fetchWithResetRetry } from "../upstream-retry";
|
|
3
|
+
import { signalWithTimeout, cancelBodyOnAbort } from "../lib/abort";
|
|
4
|
+
import { sidecarEnter } from "../lib/sidecar-tracker";
|
|
5
|
+
import { fetchWithResetRetry } from "../lib/upstream-retry";
|
|
6
6
|
import { parseSidecarSSE, type WebSearchResult } from "./parse";
|
|
7
|
-
import type { CodexUpstreamOutcome } from "../codex
|
|
7
|
+
import type { CodexUpstreamOutcome } from "../codex/routing";
|
|
8
8
|
|
|
9
9
|
export interface SidecarSettings {
|
|
10
10
|
model: string;
|
package/src/web-search/index.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { OcxConfig, OcxParsedRequest, OcxProviderConfig } from "../types";
|
|
2
2
|
import { modelInList } from "../types";
|
|
3
3
|
import type { SidecarSettings } from "./executor";
|
|
4
|
-
import type { CodexAuthContext } from "../codex
|
|
4
|
+
import type { CodexAuthContext } from "../codex/auth-context";
|
|
5
5
|
|
|
6
6
|
export { runWithWebSearch } from "./loop";
|
|
7
7
|
export { buildWebSearchTool, extractHostedWebSearch, WEB_SEARCH_TOOL_NAME } from "./synthetic-tool";
|
package/src/web-search/loop.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import type { ProviderAdapter } from "../adapters/base";
|
|
2
|
-
import type { AdapterEvent, OcxMessage, OcxParsedRequest, OcxProviderConfig } from "../types";
|
|
2
|
+
import type { AdapterEvent, OcxMessage, OcxParsedRequest, OcxProviderConfig, OcxThinkingContent } from "../types";
|
|
3
3
|
import { namespacedToolName } from "../types";
|
|
4
4
|
import { bridgeToResponsesSSE } from "../bridge";
|
|
5
5
|
import { runWebSearch, type SidecarOutcome, type SidecarOutcomeRecorder, type SidecarSettings } from "./executor";
|
|
6
|
-
import { cancelBodyOnAbort } from "../abort";
|
|
7
|
-
import { fetchWithResetRetry } from "../upstream-retry";
|
|
6
|
+
import { cancelBodyOnAbort } from "../lib/abort";
|
|
7
|
+
import { fetchWithResetRetry } from "../lib/upstream-retry";
|
|
8
8
|
import { formatWebSearchResults } from "./format-result";
|
|
9
9
|
import { WEB_SEARCH_TOOL_NAME } from "./synthetic-tool";
|
|
10
10
|
|
|
@@ -92,6 +92,32 @@ async function* replay(events: AdapterEvent[]): AsyncGenerator<AdapterEvent> {
|
|
|
92
92
|
for (const e of events) yield e;
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
+
/**
|
|
96
|
+
* Collect the thinking block that preceded a web_search call in this iteration's events, so the
|
|
97
|
+
* replayed assistant turn can carry it. Anthropic extended thinking REQUIRES the assistant
|
|
98
|
+
* message that contains tool_use to start with its signed thinking/redacted_thinking blocks —
|
|
99
|
+
* replaying a bare toolCall 400s ("Expected `thinking` or `redacted_thinking`, but found
|
|
100
|
+
* `tool_use`"). The signature validity gate stays in the anthropic adapter; other adapters
|
|
101
|
+
* ignore or serialize the part harmlessly.
|
|
102
|
+
*/
|
|
103
|
+
function extractIterationThinking(events: AdapterEvent[]): OcxThinkingContent | null {
|
|
104
|
+
let thinking = "";
|
|
105
|
+
let signature: string | undefined;
|
|
106
|
+
const redacted: string[] = [];
|
|
107
|
+
for (const e of events) {
|
|
108
|
+
if (e.type === "thinking_delta") thinking += e.thinking;
|
|
109
|
+
else if (e.type === "thinking_signature") signature = e.signature;
|
|
110
|
+
else if (e.type === "redacted_thinking") redacted.push(e.data);
|
|
111
|
+
}
|
|
112
|
+
if (!thinking && !signature && redacted.length === 0) return null;
|
|
113
|
+
return {
|
|
114
|
+
type: "thinking",
|
|
115
|
+
thinking,
|
|
116
|
+
...(signature ? { signature } : {}),
|
|
117
|
+
...(redacted.length > 0 ? { redacted } : {}),
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
95
121
|
/** Normalize a query for failed-query de-duplication (case/whitespace-insensitive). */
|
|
96
122
|
function normalizeQuery(q: string): string {
|
|
97
123
|
return q.trim().toLowerCase().replace(/\s+/g, " ");
|
|
@@ -142,6 +168,13 @@ export interface WebSearchLoopDeps {
|
|
|
142
168
|
forceEmptyResponseId?: boolean;
|
|
143
169
|
abortSignal?: AbortSignal;
|
|
144
170
|
recordSidecarOutcome?: SidecarOutcomeRecorder;
|
|
171
|
+
/** Per-iteration deadline for routed model calls (mirrors the normal path's connectTimeoutMs). */
|
|
172
|
+
connectTimeoutMs?: number;
|
|
173
|
+
/**
|
|
174
|
+
* 429 key-failover hook: rotate the provider's active pool key and return a rebuilt adapter,
|
|
175
|
+
* or null when the pool is exhausted (same semantics as the normal routed path).
|
|
176
|
+
*/
|
|
177
|
+
on429?: (retryAfterHeader: string | null) => ProviderAdapter | null;
|
|
145
178
|
}
|
|
146
179
|
|
|
147
180
|
/**
|
|
@@ -151,7 +184,9 @@ export interface WebSearchLoopDeps {
|
|
|
151
184
|
* streamed Responses SSE. web_search calls are executed internally and never relayed to Codex.
|
|
152
185
|
*/
|
|
153
186
|
export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Response> {
|
|
154
|
-
const { parsed,
|
|
187
|
+
const { parsed, selectedForwardHeaders, forwardProvider, hostedTool, settings, maxSearches, abortSignal, recordSidecarOutcome } = deps;
|
|
188
|
+
// Mutable: 429 key-failover (deps.on429) can swap in a rebuilt adapter mid-loop.
|
|
189
|
+
let adapter = deps.adapter;
|
|
155
190
|
if (!adapter.parseResponse) return jsonError(500, "web-search sidecar requires a non-streaming adapter");
|
|
156
191
|
|
|
157
192
|
const messages: OcxMessage[] = [...parsed.context.messages];
|
|
@@ -196,22 +231,41 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
|
|
|
196
231
|
...parsed, stream: false,
|
|
197
232
|
context: { ...parsed.context, messages: iterMessages, tools: forceAnswer ? toolsNoWebSearch : allTools },
|
|
198
233
|
};
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
234
|
+
// Per-iteration deadline: routed calls elsewhere carry connectTimeoutMs; without it a hung
|
|
235
|
+
// upstream would stall the whole loop until the client gives up.
|
|
236
|
+
const iterationSignal = deps.connectTimeoutMs
|
|
237
|
+
? AbortSignal.any([signal, AbortSignal.timeout(deps.connectTimeoutMs)])
|
|
238
|
+
: signal;
|
|
239
|
+
const fetchOnce = async (): Promise<Response> => {
|
|
240
|
+
const request = await adapter.buildRequest(iterParsed, { headers: selectedForwardHeaders });
|
|
241
|
+
try {
|
|
242
|
+
return adapter.fetchResponse
|
|
243
|
+
? await adapter.fetchResponse(request, { abortSignal: iterationSignal, ...(deps.connectTimeoutMs ? { timeoutMs: deps.connectTimeoutMs } : {}) })
|
|
244
|
+
: await fetchWithResetRetry(
|
|
245
|
+
() => fetch(request.url, {
|
|
246
|
+
method: request.method,
|
|
247
|
+
headers: request.headers,
|
|
248
|
+
body: request.body,
|
|
249
|
+
signal: iterationSignal,
|
|
250
|
+
}),
|
|
251
|
+
{ abortSignal: iterationSignal, label: "web-search-loop" },
|
|
252
|
+
);
|
|
253
|
+
} catch (e) {
|
|
254
|
+
if (!signal.aborted && iterationSignal.aborted) {
|
|
255
|
+
throw new LoopError(504, `Provider timeout after ${deps.connectTimeoutMs}ms during web-search`);
|
|
256
|
+
}
|
|
257
|
+
throw new LoopError(502, `Provider unreachable: ${e instanceof Error ? e.message : String(e)}`);
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
let resp = await fetchOnce();
|
|
261
|
+
// 429 key-failover parity with the normal routed path: rotate pool keys until one responds
|
|
262
|
+
// or the pool is exhausted (deps.on429 returns null — cooldown map guarantees termination).
|
|
263
|
+
while (resp.status === 429 && deps.on429) {
|
|
264
|
+
const rotated = deps.on429(resp.headers.get("retry-after"));
|
|
265
|
+
if (!rotated?.parseResponse) break;
|
|
266
|
+
try { void resp.body?.cancel(); } catch { /* already consumed */ }
|
|
267
|
+
adapter = rotated;
|
|
268
|
+
resp = await fetchOnce();
|
|
215
269
|
}
|
|
216
270
|
if (!resp.ok) {
|
|
217
271
|
const t = await resp.text().catch(() => "");
|
|
@@ -242,7 +296,7 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
|
|
|
242
296
|
// valid, and surface as ONE search cell carrying every attempted query. A real search (one that
|
|
243
297
|
// hits the sidecar) shows the spinner WHILE the batch runs. Empty/limit/repeat placeholders never
|
|
244
298
|
// emit a cell (matching the prior single-query behavior).
|
|
245
|
-
async function* runSearchCall(call: WebSearchCall): AsyncGenerator<AdapterEvent> {
|
|
299
|
+
async function* runSearchCall(call: WebSearchCall, precedingThinking?: OcxThinkingContent | null): AsyncGenerator<AdapterEvent> {
|
|
246
300
|
const results: { query: string; outcome: SidecarOutcome }[] = [];
|
|
247
301
|
let beganCell = false;
|
|
248
302
|
if (call.queries.length === 0) {
|
|
@@ -279,7 +333,11 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
|
|
|
279
333
|
: { query: call.queries[0] ?? "" };
|
|
280
334
|
messages.push({
|
|
281
335
|
role: "assistant",
|
|
282
|
-
content: [
|
|
336
|
+
content: [
|
|
337
|
+
// Signed thinking must precede tool_use on replay (Anthropic extended thinking).
|
|
338
|
+
...(precedingThinking ? [precedingThinking] : []),
|
|
339
|
+
{ type: "toolCall" as const, id: call.id, name: WEB_SEARCH_TOOL_NAME, arguments: callArgs },
|
|
340
|
+
],
|
|
283
341
|
timestamp: now,
|
|
284
342
|
});
|
|
285
343
|
// One aggregated tool result. isError only when EVERY query failed (a partial success is usable).
|
|
@@ -357,8 +415,10 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
|
|
|
357
415
|
yield* replay(split.passthrough);
|
|
358
416
|
return;
|
|
359
417
|
}
|
|
360
|
-
|
|
361
|
-
|
|
418
|
+
// The thinking that led to the search belongs to the FIRST call's assistant replay turn.
|
|
419
|
+
const iterationThinking = extractIterationThinking(split.passthrough);
|
|
420
|
+
for (const [callIndex, call] of split.calls.entries()) {
|
|
421
|
+
yield* runSearchCall(call, callIndex === 0 ? iterationThinking : null);
|
|
362
422
|
}
|
|
363
423
|
}
|
|
364
424
|
}
|