@bitkyc08/opencodex 2.1.1 → 2.1.5
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 +29 -4
- package/README.md +30 -5
- package/README.zh-CN.md +6 -0
- package/gui/dist/assets/index-DB2i6w5f.js +9 -0
- package/gui/dist/assets/{index-cEIM1XWY.css → index-dCS-lwCM.css} +1 -1
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic.ts +6 -3
- package/src/adapters/azure.ts +7 -7
- package/src/adapters/google.ts +4 -3
- package/src/adapters/openai-chat.ts +2 -1
- package/src/adapters/openai-responses.ts +2 -1
- package/src/bridge.ts +125 -24
- package/src/cli.ts +166 -13
- package/src/codex-catalog.ts +1 -0
- package/src/codex-history-provider.ts +86 -0
- package/src/codex-inject.ts +9 -1
- package/src/codex-shim.ts +42 -24
- package/src/config.ts +31 -5
- package/src/init.ts +11 -0
- package/src/oauth/store.ts +10 -4
- package/src/open-url.ts +7 -3
- package/src/ports.ts +30 -0
- package/src/providers/registry.ts +1 -1
- package/src/responses/parser.ts +9 -6
- package/src/responses/schema.ts +1 -0
- package/src/server.ts +182 -13
- package/src/service.ts +29 -2
- package/src/types.ts +8 -0
- package/src/update.ts +12 -2
- package/src/web-search/loop.ts +4 -1
- package/src/ws-bridge.ts +1 -1
- package/gui/dist/assets/index-DgCnBxqJ.js +0 -9
package/src/server.ts
CHANGED
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
type WsData,
|
|
17
17
|
} from "./ws-bridge";
|
|
18
18
|
import type { ServerWebSocket } from "bun";
|
|
19
|
-
import { DEFAULT_SUBAGENT_MODELS, loadConfig, saveConfig, websocketsEnabled } from "./config";
|
|
19
|
+
import { DEFAULT_SUBAGENT_MODELS, codexAutoStartEnabled, loadConfig, saveConfig, websocketsEnabled } from "./config";
|
|
20
20
|
import { parseRequest } from "./responses/parser";
|
|
21
21
|
import { routeModel } from "./router";
|
|
22
22
|
import { namespacedToolName } from "./types";
|
|
@@ -183,20 +183,29 @@ async function handleResponses(
|
|
|
183
183
|
// whose cancel() aborts the upstream — preventing leaked connections (RC2, passthrough path).
|
|
184
184
|
const upstream = new AbortController();
|
|
185
185
|
linkAbortSignal(upstream, options.abortSignal);
|
|
186
|
+
const connectMs = config.connectTimeoutMs ?? 30_000;
|
|
186
187
|
let upstreamResponse: Response;
|
|
187
188
|
try {
|
|
188
|
-
upstreamResponse = await
|
|
189
|
+
upstreamResponse = await fetchWithHeaderTimeout(request.url, {
|
|
189
190
|
method: request.method,
|
|
190
191
|
headers: request.headers,
|
|
191
192
|
body: request.body,
|
|
192
|
-
|
|
193
|
-
});
|
|
193
|
+
}, upstream.signal, connectMs);
|
|
194
194
|
} catch (err) {
|
|
195
|
-
|
|
195
|
+
upstream.abort();
|
|
196
|
+
const msg = err instanceof Error && err.name === "TimeoutError"
|
|
197
|
+
? `Provider connect timeout after ${connectMs}ms`
|
|
198
|
+
: `Provider unreachable: ${err instanceof Error ? err.message : String(err)}`;
|
|
199
|
+
return formatErrorResponse(502, "upstream_error", msg);
|
|
196
200
|
}
|
|
197
|
-
|
|
201
|
+
const headers = sanitizePassthroughHeaders(upstreamResponse.headers);
|
|
202
|
+
const isEventStream = headers.get("content-type")?.toLowerCase().includes("text/event-stream") ?? false;
|
|
203
|
+
const body = isEventStream
|
|
204
|
+
? relaySseWithHeartbeat(upstreamResponse.body, upstream)
|
|
205
|
+
: relayWithAbort(upstreamResponse.body, upstream);
|
|
206
|
+
return new Response(body, {
|
|
198
207
|
status: upstreamResponse.status,
|
|
199
|
-
headers
|
|
208
|
+
headers,
|
|
200
209
|
});
|
|
201
210
|
}
|
|
202
211
|
|
|
@@ -220,15 +229,20 @@ async function handleResponses(
|
|
|
220
229
|
|
|
221
230
|
const upstream = new AbortController();
|
|
222
231
|
linkAbortSignal(upstream, options.abortSignal);
|
|
232
|
+
const connectMs = config.connectTimeoutMs ?? 30_000;
|
|
223
233
|
|
|
224
234
|
const request = adapter.buildRequest(parsed, { headers: req.headers });
|
|
225
235
|
let upstreamResponse: Response;
|
|
226
236
|
try {
|
|
227
|
-
upstreamResponse = await
|
|
228
|
-
method: request.method, headers: request.headers, body: request.body,
|
|
229
|
-
});
|
|
237
|
+
upstreamResponse = await fetchWithHeaderTimeout(request.url, {
|
|
238
|
+
method: request.method, headers: request.headers, body: request.body,
|
|
239
|
+
}, upstream.signal, connectMs);
|
|
230
240
|
} catch (err) {
|
|
231
|
-
|
|
241
|
+
upstream.abort();
|
|
242
|
+
const msg = err instanceof Error && err.name === "TimeoutError"
|
|
243
|
+
? `Provider connect timeout after ${connectMs}ms`
|
|
244
|
+
: `Provider unreachable: ${err instanceof Error ? err.message : String(err)}`;
|
|
245
|
+
return formatErrorResponse(502, "upstream_error", msg);
|
|
232
246
|
}
|
|
233
247
|
|
|
234
248
|
if (!upstreamResponse.ok) {
|
|
@@ -249,7 +263,11 @@ async function handleResponses(
|
|
|
249
263
|
const sseStream = bridgeToResponsesSSE(
|
|
250
264
|
eventStream, parsed.modelId, toolNsMap, freeformToolNames, toolSearchToolNames,
|
|
251
265
|
() => upstream.abort(), 2_000,
|
|
252
|
-
|
|
266
|
+
{
|
|
267
|
+
...(options.forceEmptyResponseId ? { responseId: "" } : {}),
|
|
268
|
+
stallTimeoutSec: config.stallTimeoutSec,
|
|
269
|
+
hideThinkingSummary: parsed.options.hideThinkingSummary,
|
|
270
|
+
},
|
|
253
271
|
);
|
|
254
272
|
return new Response(sseStream, {
|
|
255
273
|
headers: { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", "Connection": "keep-alive", "X-Accel-Buffering": "no" },
|
|
@@ -258,7 +276,20 @@ async function handleResponses(
|
|
|
258
276
|
|
|
259
277
|
if (adapter.parseResponse) {
|
|
260
278
|
const events = await adapter.parseResponse(upstreamResponse);
|
|
261
|
-
const
|
|
279
|
+
const toolNsMap = new Map<string, { namespace: string; name: string }>();
|
|
280
|
+
const freeformToolNames = new Set<string>();
|
|
281
|
+
const toolSearchToolNames = new Set<string>();
|
|
282
|
+
for (const t of parsed.context.tools ?? []) {
|
|
283
|
+
if (t.namespace) toolNsMap.set(namespacedToolName(t.namespace, t.name), { namespace: t.namespace, name: t.name });
|
|
284
|
+
if (t.freeform) freeformToolNames.add(t.name);
|
|
285
|
+
if (t.toolSearch) toolSearchToolNames.add(t.name);
|
|
286
|
+
}
|
|
287
|
+
const json = buildResponseJSON(events, parsed.modelId, {
|
|
288
|
+
hideThinkingSummary: parsed.options.hideThinkingSummary,
|
|
289
|
+
toolNsMap,
|
|
290
|
+
freeformToolNames,
|
|
291
|
+
toolSearchToolNames,
|
|
292
|
+
});
|
|
262
293
|
return new Response(JSON.stringify(json), { headers: { "Content-Type": "application/json" } });
|
|
263
294
|
}
|
|
264
295
|
|
|
@@ -274,6 +305,26 @@ export function linkAbortSignal(upstream: AbortController, signal?: AbortSignal)
|
|
|
274
305
|
signal.addEventListener("abort", () => upstream.abort(signal.reason), { once: true });
|
|
275
306
|
}
|
|
276
307
|
|
|
308
|
+
async function fetchWithHeaderTimeout(
|
|
309
|
+
url: string,
|
|
310
|
+
init: Omit<RequestInit, "signal">,
|
|
311
|
+
abortSignal: AbortSignal,
|
|
312
|
+
timeoutMs: number,
|
|
313
|
+
): Promise<Response> {
|
|
314
|
+
const timeout = new AbortController();
|
|
315
|
+
const timer = setTimeout(() => {
|
|
316
|
+
if (!timeout.signal.aborted) timeout.abort(new DOMException("Timeout elapsed", "TimeoutError"));
|
|
317
|
+
}, timeoutMs);
|
|
318
|
+
try {
|
|
319
|
+
return await fetch(url, {
|
|
320
|
+
...init,
|
|
321
|
+
signal: AbortSignal.any([abortSignal, timeout.signal]),
|
|
322
|
+
});
|
|
323
|
+
} finally {
|
|
324
|
+
clearTimeout(timer);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
277
328
|
const requestLog: { timestamp: number; model: string; provider: string; status: number; durationMs: number }[] = [];
|
|
278
329
|
const MAX_LOG_SIZE = 200;
|
|
279
330
|
|
|
@@ -315,6 +366,56 @@ export function relayWithAbort(
|
|
|
315
366
|
});
|
|
316
367
|
}
|
|
317
368
|
|
|
369
|
+
export function relaySseWithHeartbeat(
|
|
370
|
+
body: ReadableStream<Uint8Array> | null,
|
|
371
|
+
upstream: AbortController,
|
|
372
|
+
heartbeatMs = 15_000,
|
|
373
|
+
): ReadableStream<Uint8Array> | null {
|
|
374
|
+
if (!body) return null;
|
|
375
|
+
const reader = body.getReader();
|
|
376
|
+
const heartbeat = new TextEncoder().encode(": opencodex keepalive\n\n");
|
|
377
|
+
let timer: ReturnType<typeof setInterval> | undefined;
|
|
378
|
+
let closed = false;
|
|
379
|
+
|
|
380
|
+
const cleanup = () => {
|
|
381
|
+
closed = true;
|
|
382
|
+
if (timer) clearInterval(timer);
|
|
383
|
+
timer = undefined;
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
return new ReadableStream<Uint8Array>({
|
|
387
|
+
start(controller) {
|
|
388
|
+
timer = setInterval(() => {
|
|
389
|
+
if (closed) return;
|
|
390
|
+
try {
|
|
391
|
+
controller.enqueue(heartbeat);
|
|
392
|
+
} catch {
|
|
393
|
+
cleanup();
|
|
394
|
+
}
|
|
395
|
+
}, heartbeatMs);
|
|
396
|
+
},
|
|
397
|
+
async pull(controller) {
|
|
398
|
+
try {
|
|
399
|
+
const { done, value } = await reader.read();
|
|
400
|
+
if (done) {
|
|
401
|
+
cleanup();
|
|
402
|
+
controller.close();
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
controller.enqueue(value);
|
|
406
|
+
} catch (err) {
|
|
407
|
+
cleanup();
|
|
408
|
+
try { controller.error(err); } catch { /* already torn down */ }
|
|
409
|
+
}
|
|
410
|
+
},
|
|
411
|
+
cancel(reason) {
|
|
412
|
+
cleanup();
|
|
413
|
+
upstream.abort(reason);
|
|
414
|
+
reader.cancel(reason).catch(() => {});
|
|
415
|
+
},
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
|
|
318
419
|
/**
|
|
319
420
|
* Bun's fetch auto-decompresses the response body but leaves the upstream `content-encoding`
|
|
320
421
|
* (and a now-stale `content-length`) on `response.headers`. Relaying those with the already-decoded
|
|
@@ -358,7 +459,18 @@ function jsonResponse(data: unknown, status = 200): Response {
|
|
|
358
459
|
});
|
|
359
460
|
}
|
|
360
461
|
|
|
462
|
+
function isLocalOrigin(req: Request): boolean {
|
|
463
|
+
const origin = req.headers.get("Origin");
|
|
464
|
+
if (!origin) return true;
|
|
465
|
+
const localhostOrigin = _corsOrigin;
|
|
466
|
+
const loopbackOrigin = _corsOrigin.replace("localhost", "127.0.0.1");
|
|
467
|
+
return origin === localhostOrigin || origin === loopbackOrigin;
|
|
468
|
+
}
|
|
469
|
+
|
|
361
470
|
async function handleManagementAPI(req: Request, url: URL, config: OcxConfig): Promise<Response | null> {
|
|
471
|
+
if ((req.method === "POST" || req.method === "PUT" || req.method === "DELETE") && !isLocalOrigin(req)) {
|
|
472
|
+
return jsonResponse({ error: "cross-origin request blocked" }, 403);
|
|
473
|
+
}
|
|
362
474
|
async function refreshCodexCatalogBestEffort(): Promise<void> {
|
|
363
475
|
try {
|
|
364
476
|
const { refreshCodexModelCatalog } = await import("./codex-refresh");
|
|
@@ -370,6 +482,7 @@ async function handleManagementAPI(req: Request, url: URL, config: OcxConfig): P
|
|
|
370
482
|
|
|
371
483
|
if (url.pathname === "/api/config" && req.method === "GET") {
|
|
372
484
|
const safeConfig = JSON.parse(JSON.stringify(config));
|
|
485
|
+
safeConfig.codexAutoStart = codexAutoStartEnabled(config);
|
|
373
486
|
for (const prov of Object.values(safeConfig.providers as Record<string, OcxProviderConfig>)) {
|
|
374
487
|
if (prov.apiKey) prov.apiKey = prov.apiKey.slice(0, 8) + "...";
|
|
375
488
|
}
|
|
@@ -380,6 +493,56 @@ async function handleManagementAPI(req: Request, url: URL, config: OcxConfig): P
|
|
|
380
493
|
return jsonResponse({ error: "Full config PUT is disabled. Use /api/providers POST for provider changes." }, 405);
|
|
381
494
|
}
|
|
382
495
|
|
|
496
|
+
if (url.pathname === "/api/settings" && req.method === "GET") {
|
|
497
|
+
return jsonResponse({
|
|
498
|
+
codexAutoStart: codexAutoStartEnabled(config),
|
|
499
|
+
port: config.port,
|
|
500
|
+
hostname: config.hostname ?? "127.0.0.1",
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
if (url.pathname === "/api/settings" && req.method === "PUT") {
|
|
505
|
+
let body: { codexAutoStart?: unknown };
|
|
506
|
+
try { body = await req.json(); } catch { return jsonResponse({ error: "invalid JSON body" }, 400); }
|
|
507
|
+
if (typeof body.codexAutoStart !== "boolean") {
|
|
508
|
+
return jsonResponse({ error: "codexAutoStart boolean is required" }, 400);
|
|
509
|
+
}
|
|
510
|
+
config.codexAutoStart = body.codexAutoStart;
|
|
511
|
+
saveConfig(config);
|
|
512
|
+
return jsonResponse({ ok: true, codexAutoStart: codexAutoStartEnabled(config) });
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
if (url.pathname === "/api/sidecar-settings" && req.method === "GET") {
|
|
516
|
+
const ws = config.webSearchSidecar ?? {};
|
|
517
|
+
const vs = config.visionSidecar ?? {};
|
|
518
|
+
return jsonResponse({
|
|
519
|
+
webSearch: { model: ws.model ?? "gpt-5.4-mini", reasoning: ws.reasoning ?? "low" },
|
|
520
|
+
vision: { model: vs.model ?? "gpt-5.4-mini" },
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
if (url.pathname === "/api/sidecar-settings" && req.method === "PUT") {
|
|
525
|
+
let body: { webSearch?: { model?: string; reasoning?: string }; vision?: { model?: string } };
|
|
526
|
+
try { body = await req.json(); } catch { return jsonResponse({ error: "invalid JSON body" }, 400); }
|
|
527
|
+
if (body.webSearch) {
|
|
528
|
+
config.webSearchSidecar = { ...config.webSearchSidecar };
|
|
529
|
+
if (typeof body.webSearch.model === "string") config.webSearchSidecar.model = body.webSearch.model;
|
|
530
|
+
if (typeof body.webSearch.reasoning === "string") config.webSearchSidecar.reasoning = body.webSearch.reasoning;
|
|
531
|
+
}
|
|
532
|
+
if (body.vision) {
|
|
533
|
+
config.visionSidecar = { ...config.visionSidecar };
|
|
534
|
+
if (typeof body.vision.model === "string") config.visionSidecar.model = body.vision.model;
|
|
535
|
+
}
|
|
536
|
+
saveConfig(config);
|
|
537
|
+
const ws = config.webSearchSidecar ?? {};
|
|
538
|
+
const vs = config.visionSidecar ?? {};
|
|
539
|
+
return jsonResponse({
|
|
540
|
+
ok: true,
|
|
541
|
+
webSearch: { model: ws.model ?? "gpt-5.4-mini", reasoning: ws.reasoning ?? "low" },
|
|
542
|
+
vision: { model: vs.model ?? "gpt-5.4-mini" },
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
|
|
383
546
|
if (url.pathname === "/api/logs" && req.method === "GET") {
|
|
384
547
|
return jsonResponse(requestLog);
|
|
385
548
|
}
|
|
@@ -573,6 +736,9 @@ export function startServer(port?: number) {
|
|
|
573
736
|
// Responses WebSocket (phase 120.2). Codex upgrades the same /v1/responses path; auth is
|
|
574
737
|
// handshake-time only, so capture inbound headers and thread them into the pipeline.
|
|
575
738
|
if (url.pathname === "/v1/responses" && req.headers.get("upgrade")?.toLowerCase() === "websocket") {
|
|
739
|
+
if (!isLocalOrigin(req)) {
|
|
740
|
+
return formatErrorResponse(403, "origin_rejected", "WebSocket upgrade blocked: non-local Origin");
|
|
741
|
+
}
|
|
576
742
|
if (server.upgrade(req, { data: { headers: selectForwardHeaders(req.headers) } })) return undefined as unknown as Response;
|
|
577
743
|
return formatErrorResponse(426, "upgrade_required", "WebSocket upgrade failed");
|
|
578
744
|
}
|
|
@@ -608,6 +774,9 @@ export function startServer(port?: number) {
|
|
|
608
774
|
}
|
|
609
775
|
|
|
610
776
|
if (url.pathname === "/v1/responses" && req.method === "POST") {
|
|
777
|
+
if (!isLocalOrigin(req)) {
|
|
778
|
+
return formatErrorResponse(403, "origin_rejected", "cross-origin data-plane request blocked");
|
|
779
|
+
}
|
|
611
780
|
const start = Date.now();
|
|
612
781
|
const logCtx = { model: "unknown", provider: "unknown" };
|
|
613
782
|
const response = await handleResponses(req, config, logCtx);
|
package/src/service.ts
CHANGED
|
@@ -85,6 +85,12 @@ function systemdEnvironmentAssignment(name: string, value: string | undefined):
|
|
|
85
85
|
return `Environment=${systemdQuote(`${name}=${value}`)}`;
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
+
function systemdOutputTarget(value: string): string {
|
|
89
|
+
// StandardOutput/StandardError use output specifiers such as append:/path.
|
|
90
|
+
// Quoting the full specifier makes systemd reject it as an invalid output target.
|
|
91
|
+
return value.replace(/%/g, "%%").replace(/\n/g, "\\n");
|
|
92
|
+
}
|
|
93
|
+
|
|
88
94
|
function sh(cmd: string): string {
|
|
89
95
|
return execSync(cmd, { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
90
96
|
}
|
|
@@ -183,8 +189,8 @@ ExecStart=${systemdQuote(bun)} ${systemdQuote(cli)} start
|
|
|
183
189
|
Restart=on-failure
|
|
184
190
|
RestartSec=5
|
|
185
191
|
${envLines}
|
|
186
|
-
StandardOutput=${
|
|
187
|
-
StandardError=${
|
|
192
|
+
StandardOutput=${systemdOutputTarget(`append:${log}`)}
|
|
193
|
+
StandardError=${systemdOutputTarget(`append:${log}`)}
|
|
188
194
|
|
|
189
195
|
[Install]
|
|
190
196
|
WantedBy=default.target
|
|
@@ -256,6 +262,27 @@ export function stopServiceIfInstalled(): boolean {
|
|
|
256
262
|
return false;
|
|
257
263
|
}
|
|
258
264
|
|
|
265
|
+
/**
|
|
266
|
+
* Best-effort service removal for full uninstall. Unlike `ocx service uninstall`, this is quiet
|
|
267
|
+
* when no service exists and never exits the process just because the platform has no service
|
|
268
|
+
* manager.
|
|
269
|
+
*/
|
|
270
|
+
export function uninstallServiceIfInstalled(): boolean {
|
|
271
|
+
if (process.platform === "darwin") {
|
|
272
|
+
if (existsSync(plistPath())) {
|
|
273
|
+
try { uninstallLaunchd(); return true; } catch { return false; }
|
|
274
|
+
}
|
|
275
|
+
} else if (process.platform === "win32") {
|
|
276
|
+
try {
|
|
277
|
+
const q = sh(`schtasks /query /tn ${TASK} 2>nul`);
|
|
278
|
+
if (q.includes(TASK)) { uninstallWindows(); return true; }
|
|
279
|
+
} catch { /* task not found */ }
|
|
280
|
+
} else if (process.platform === "linux" && isSystemd() && existsSync(unitPath())) {
|
|
281
|
+
try { uninstallSystemd(); return true; } catch { return false; }
|
|
282
|
+
}
|
|
283
|
+
return false;
|
|
284
|
+
}
|
|
285
|
+
|
|
259
286
|
export function serviceCommand(sub?: string): void {
|
|
260
287
|
const ops = platformOps();
|
|
261
288
|
if (!ops) {
|
package/src/types.ts
CHANGED
|
@@ -54,6 +54,8 @@ export interface OcxToolResultMessage {
|
|
|
54
54
|
role: "toolResult";
|
|
55
55
|
toolCallId: string;
|
|
56
56
|
toolName: string;
|
|
57
|
+
/** MCP namespace from the originating tool call, if any. */
|
|
58
|
+
toolNamespace?: string;
|
|
57
59
|
/** Text, or content parts when a tool (e.g. Codex view_image) returns an image in its output. */
|
|
58
60
|
content: string | OcxContentPart[];
|
|
59
61
|
isError: boolean;
|
|
@@ -177,8 +179,14 @@ export interface OcxConfig {
|
|
|
177
179
|
disabledModels?: string[];
|
|
178
180
|
/** Bind hostname. Default "127.0.0.1" (loopback only). Set "0.0.0.0" to expose on all interfaces. */
|
|
179
181
|
hostname?: string;
|
|
182
|
+
/** Upstream stall timeout (seconds). After this many seconds of no upstream data, emits response.incomplete. Default 90. Min 1. */
|
|
183
|
+
stallTimeoutSec?: number;
|
|
184
|
+
/** Connect timeout (ms) for upstream fetch — covers DNS, TCP, TLS, and response header. Default 30000. */
|
|
185
|
+
connectTimeoutMs?: number;
|
|
180
186
|
/** Advertise supports_websockets so Codex opens the WS endpoint. Default false; set true to opt in. */
|
|
181
187
|
websockets?: boolean;
|
|
188
|
+
/** Auto-start/sync the proxy from the Codex shim before launching Codex. Default true. */
|
|
189
|
+
codexAutoStart?: boolean;
|
|
182
190
|
/** Freshness window (ms) for the per-provider live `/models` cache. Defaults to 5 min. */
|
|
183
191
|
modelCacheTtlMs?: number;
|
|
184
192
|
/** Web-search sidecar: route web_search for non-OpenAI models through a gpt-mini via ChatGPT passthrough. */
|
package/src/update.ts
CHANGED
|
@@ -32,7 +32,7 @@ function latestVersion(): string | null {
|
|
|
32
32
|
* `ocx update` — self-update opencodex to the latest published version, using the same package
|
|
33
33
|
* manager it was installed with (bun or npm global). A source checkout is told to `git pull` instead.
|
|
34
34
|
*/
|
|
35
|
-
export function runUpdate(): void {
|
|
35
|
+
export async function runUpdate(): Promise<void> {
|
|
36
36
|
const installer = detectInstall();
|
|
37
37
|
const current = currentVersion();
|
|
38
38
|
console.log(`opencodex v${current} (installed via ${installer})`);
|
|
@@ -56,7 +56,17 @@ export function runUpdate(): void {
|
|
|
56
56
|
|
|
57
57
|
const r = spawnSync(bin, cmdArgs, { stdio: "inherit", timeout: 180000, windowsHide: true });
|
|
58
58
|
if (r.status === 0) {
|
|
59
|
-
console.log(`\n✅ Updated${latest ? ` to v${latest}` : ""}
|
|
59
|
+
console.log(`\n✅ Updated${latest ? ` to v${latest}` : ""}.`);
|
|
60
|
+
if (process.platform === "win32") {
|
|
61
|
+
try {
|
|
62
|
+
const { installCodexShim } = await import("./codex-shim");
|
|
63
|
+
const result = installCodexShim();
|
|
64
|
+
if (result.installed) console.log(`🔧 ${result.message}`);
|
|
65
|
+
} catch (e) {
|
|
66
|
+
console.warn(`⚠️ Shim repair skipped: ${e instanceof Error ? e.message : e}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
console.log("Restart the proxy: ocx stop && ocx start");
|
|
60
70
|
} else {
|
|
61
71
|
console.error(`\n⚠️ Update failed (${bin} exit ${r.status ?? "?"}). Try manually: ${bin} ${cmdArgs.join(" ")}`);
|
|
62
72
|
process.exit(1);
|
package/src/web-search/loop.ts
CHANGED
|
@@ -193,7 +193,10 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
|
|
|
193
193
|
const sse = bridgeToResponsesSSE(
|
|
194
194
|
replay(finalEvents), parsed.modelId, toolNsMap, freeform, toolSearch,
|
|
195
195
|
undefined, undefined,
|
|
196
|
-
|
|
196
|
+
{
|
|
197
|
+
...(deps.forceEmptyResponseId ? { responseId: "" } : {}),
|
|
198
|
+
hideThinkingSummary: parsed.options.hideThinkingSummary,
|
|
199
|
+
},
|
|
197
200
|
);
|
|
198
201
|
return new Response(sse, { headers: SSE_HEADERS });
|
|
199
202
|
}
|
package/src/ws-bridge.ts
CHANGED
|
@@ -223,7 +223,7 @@ export function sendResponsesJsonAsEvents(
|
|
|
223
223
|
? response.status
|
|
224
224
|
: "completed";
|
|
225
225
|
sendJsonFrame(ws, {
|
|
226
|
-
type: finalStatus
|
|
226
|
+
type: `response.${finalStatus}` as "response.completed" | "response.failed" | "response.incomplete",
|
|
227
227
|
response: { ...response, status: finalStatus },
|
|
228
228
|
});
|
|
229
229
|
}
|