@bitkyc08/opencodex 2.6.31-preview.20260707 → 2.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ko.md +19 -3
- package/README.md +17 -2
- package/README.zh-CN.md +16 -3
- package/gui/dist/assets/index-BGdxwydf.js +34 -0
- package/gui/dist/assets/index-DANCQ2Jt.css +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic.ts +62 -1
- package/src/adapters/cursor/cursor-errors.ts +28 -1
- package/src/adapters/cursor/discovery.ts +56 -10
- package/src/adapters/cursor/effort-map.ts +35 -7
- package/src/adapters/cursor/live-models.ts +3 -0
- package/src/adapters/cursor/live-transport.ts +136 -7
- package/src/adapters/cursor/protobuf-request.ts +24 -1
- package/src/adapters/cursor/request-builder.ts +6 -5
- package/src/adapters/cursor/transport-retry.ts +22 -3
- package/src/adapters/cursor.ts +2 -1
- package/src/adapters/kiro.ts +1 -1
- package/src/adapters/openai-chat.ts +75 -26
- package/src/bridge.ts +50 -4
- package/src/cli/debug.ts +203 -0
- package/src/cli/doctor.ts +11 -0
- package/src/cli/help.ts +11 -0
- package/src/cli/index.ts +10 -0
- package/src/cli/v2.ts +131 -0
- package/src/codex/account-store.ts +42 -1
- package/src/codex/auth-api.ts +43 -0
- package/src/codex/catalog.ts +356 -29
- package/src/codex/data/upstream-models.json +830 -0
- package/src/codex/features.ts +178 -0
- package/src/codex/project-config-warnings.ts +388 -0
- package/src/codex/sync.ts +8 -0
- package/src/codex/warmup.ts +193 -0
- package/src/config.ts +7 -5
- package/src/lib/debug-log-buffer.ts +42 -0
- package/src/lib/debug-settings.ts +84 -0
- package/src/lib/debug.ts +18 -9
- package/src/lib/errors.ts +104 -1
- package/src/oauth/cursor.ts +35 -12
- package/src/oauth/store.ts +4 -3
- package/src/oauth/token-guardian.ts +32 -7
- package/src/providers/derive.ts +8 -0
- package/src/providers/kiro-models.ts +3 -3
- package/src/providers/registry.ts +77 -56
- package/src/reasoning-effort.ts +34 -12
- package/src/responses/parser.ts +7 -2
- package/src/router.ts +7 -3
- package/src/server/adapter-resolve.ts +1 -1
- package/src/server/index.ts +27 -3
- package/src/server/management-api.ts +168 -7
- package/src/server/relay.ts +2 -2
- package/src/server/request-log.ts +86 -2
- package/src/server/responses.ts +209 -0
- package/src/types.ts +38 -2
- package/src/usage/debug.ts +32 -5
- package/src/usage/summary.ts +6 -6
- package/src/web-search/index.ts +1 -1
- package/gui/dist/assets/index-ByGC8-Bm.css +0 -1
- package/gui/dist/assets/index-CWujz83O.js +0 -15
package/src/server/index.ts
CHANGED
|
@@ -69,6 +69,7 @@ export {
|
|
|
69
69
|
addFinalRequestLog,
|
|
70
70
|
filterRequestLogs,
|
|
71
71
|
httpStatusForTerminalStatus,
|
|
72
|
+
httpStatusFromTerminalError,
|
|
72
73
|
nextRequestLogId,
|
|
73
74
|
requestLogErrorCode,
|
|
74
75
|
requestLogSpeedLabel,
|
|
@@ -157,6 +158,24 @@ export function startServer(port?: number) {
|
|
|
157
158
|
config.subagentModels = [...DEFAULT_SUBAGENT_MODELS];
|
|
158
159
|
saveConfig(config);
|
|
159
160
|
}
|
|
161
|
+
// Sidecar model migration (KST 2026-07-10 06:00 = UTC 2026-07-09 21:00): auto-migrate the old
|
|
162
|
+
// gpt-5.4-mini default to gpt-5.6-luna for both search and vision sidecars. Only touches configs
|
|
163
|
+
// still on the old default — explicit user choices are preserved.
|
|
164
|
+
{
|
|
165
|
+
const SIDECAR_MIGRATION_CUTOFF = Date.UTC(2026, 6, 9, 21, 0); // July 9 21:00 UTC = KST July 10 06:00
|
|
166
|
+
if (Date.now() >= SIDECAR_MIGRATION_CUTOFF) {
|
|
167
|
+
let migrated = false;
|
|
168
|
+
if (config.webSearchSidecar?.model === "gpt-5.4-mini") {
|
|
169
|
+
config.webSearchSidecar = { ...config.webSearchSidecar, model: "gpt-5.6-luna" };
|
|
170
|
+
migrated = true;
|
|
171
|
+
}
|
|
172
|
+
if (config.visionSidecar?.model === "gpt-5.4-mini") {
|
|
173
|
+
config.visionSidecar = { ...config.visionSidecar, model: "gpt-5.6-luna" };
|
|
174
|
+
migrated = true;
|
|
175
|
+
}
|
|
176
|
+
if (migrated) saveConfig(config);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
160
179
|
invalidateCodexModelsCache();
|
|
161
180
|
|
|
162
181
|
const listenPort = port ?? config.port ?? 10100;
|
|
@@ -247,7 +266,7 @@ export function startServer(port?: number) {
|
|
|
247
266
|
return withCors(formatErrorResponse(403, "origin_rejected", "cross-origin data-plane request blocked"), req, config);
|
|
248
267
|
}
|
|
249
268
|
const goModels = await fetchAllModels(config);
|
|
250
|
-
const { buildCatalogEntries, loadCatalogTemplate, nativeOpenAiSlugs, orderForSubagents, filterCatalogVisibleModels } = await import("../codex/catalog");
|
|
269
|
+
const { applyNativeVisibility, buildCatalogEntries, disabledNativeSlugs, loadCatalogTemplate, nativeOpenAiSlugs, orderForSubagents, filterCatalogVisibleModels, visibleNativeSlugs } = await import("../codex/catalog");
|
|
251
270
|
const nativeSlugs = nativeOpenAiSlugs();
|
|
252
271
|
const goEnabled = filterCatalogVisibleModels(goModels, config);
|
|
253
272
|
const goOrdered = orderForSubagents(goEnabled, config.subagentModels);
|
|
@@ -255,11 +274,16 @@ export function startServer(port?: number) {
|
|
|
255
274
|
// Codex client → Codex catalog shape: native gpt + namespaced routed models,
|
|
256
275
|
// cloned from a native template so required fields (base_instructions, etc.) are present.
|
|
257
276
|
// Pass the subagent picks so featured models lead by priority (matches the on-disk file).
|
|
258
|
-
|
|
277
|
+
// Disabled natives stay in the catalog shape with visibility "hide" (mirrors the
|
|
278
|
+
// on-disk sync; codex-rs keeps them out of the picker itself).
|
|
279
|
+
const maMode = config.multiAgentMode === "v1" || config.multiAgentMode === "v2" ? config.multiAgentMode : "default";
|
|
280
|
+
const entries = buildCatalogEntries(loadCatalogTemplate(), nativeSlugs, goOrdered, config.subagentModels, websocketsEnabled(config), maMode as "v1" | "default" | "v2");
|
|
281
|
+
return jsonResponse({ models: applyNativeVisibility(entries, disabledNativeSlugs(config)) }, 200, req, config);
|
|
259
282
|
}
|
|
260
283
|
// OpenAI list shape: native gpt bare + routed models namespaced "<provider>/<id>"
|
|
284
|
+
// (pure availability list — disabled natives are omitted entirely).
|
|
261
285
|
const data = [
|
|
262
|
-
...
|
|
286
|
+
...visibleNativeSlugs(config).map(id => ({ id, object: "model", created: 0, owned_by: "openai" })),
|
|
263
287
|
...goOrdered.map(m => ({ id: `${m.provider}/${m.id}`, object: "model", created: 0, owned_by: m.owned_by ?? m.provider })),
|
|
264
288
|
];
|
|
265
289
|
return jsonResponse({ object: "list", data }, 200, req, config);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
2
|
import type { CatalogModel } from "../codex/catalog";
|
|
3
|
-
import { invalidateCodexModelsCache } from "../codex/catalog";
|
|
3
|
+
import { invalidateCodexModelsCache, nativeModelRows } from "../codex/catalog";
|
|
4
4
|
import {
|
|
5
5
|
DEFAULT_SUBAGENT_MODELS,
|
|
6
6
|
codexAutoStartEnabled,
|
|
@@ -24,8 +24,17 @@ import { deriveProviderPresets } from "../providers/derive";
|
|
|
24
24
|
import { fetchProviderQuotaReports } from "../providers/quota";
|
|
25
25
|
import { DEFAULT_PROVIDER_CONTEXT_CAP, globalContextCapValue, providerContextCap, providerContextCaps, setAllProviderContextCaps, setGlobalContextCapValue, setProviderContextCap } from "../providers/context-cap";
|
|
26
26
|
import { readUsageEntries } from "../usage/log";
|
|
27
|
+
import { getUsageDebugLogEntries } from "../usage/debug";
|
|
27
28
|
import { parseRange, summarizeUsage } from "../usage/summary";
|
|
28
29
|
import { stripCodexRuntimeProviderFields } from "../codex/auth-context";
|
|
30
|
+
import { getDebugLogEntries } from "../lib/debug-log-buffer";
|
|
31
|
+
import {
|
|
32
|
+
clearDebugSettings,
|
|
33
|
+
clearDebugSetting,
|
|
34
|
+
getDebugSettings,
|
|
35
|
+
setDebugSettings,
|
|
36
|
+
type DebugFlag,
|
|
37
|
+
} from "../lib/debug-settings";
|
|
29
38
|
import type { OcxConfig, OcxProviderConfig } from "../types";
|
|
30
39
|
import { drainAndShutdown } from "./lifecycle";
|
|
31
40
|
import { filterRequestLogs, getRequestLogEntries } from "./request-log";
|
|
@@ -41,6 +50,15 @@ export const VERSION = (() => {
|
|
|
41
50
|
}
|
|
42
51
|
})();
|
|
43
52
|
|
|
53
|
+
function parseDebugLogQuery(url: URL): { after: number; limit: number } {
|
|
54
|
+
const after = Number(url.searchParams.get("after") ?? url.searchParams.get("since") ?? "0");
|
|
55
|
+
const limit = Number(url.searchParams.get("limit") ?? "500");
|
|
56
|
+
return {
|
|
57
|
+
after: Number.isFinite(after) && after > 0 ? after : 0,
|
|
58
|
+
limit: Number.isFinite(limit) && limit > 0 ? Math.min(limit, 2000) : 500,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
44
62
|
export async function handleManagementAPI(req: Request, url: URL, config: OcxConfig): Promise<Response | null> {
|
|
45
63
|
if (!isAllowedRequestOrigin(req, config)) {
|
|
46
64
|
return jsonResponse({ error: "cross-origin request blocked" }, 403, req, config);
|
|
@@ -89,6 +107,12 @@ export async function handleManagementAPI(req: Request, url: URL, config: OcxCon
|
|
|
89
107
|
return jsonResponse({ ok: true, codexAutoStart: codexAutoStartEnabled(config) });
|
|
90
108
|
}
|
|
91
109
|
|
|
110
|
+
if (url.pathname === "/api/diagnostics/project-config" && req.method === "GET") {
|
|
111
|
+
const { getCachedProjectConfigDiagnostics } = await import("../codex/project-config-warnings");
|
|
112
|
+
const { warnings, grouped } = getCachedProjectConfigDiagnostics();
|
|
113
|
+
return jsonResponse({ warnings, grouped });
|
|
114
|
+
}
|
|
115
|
+
|
|
92
116
|
if (url.pathname === "/api/sync" && req.method === "POST") {
|
|
93
117
|
const { syncModelsToCodex } = await import("../codex/sync");
|
|
94
118
|
const result = await syncModelsToCodex(undefined, config, null);
|
|
@@ -138,8 +162,8 @@ export async function handleManagementAPI(req: Request, url: URL, config: OcxCon
|
|
|
138
162
|
const ws = config.webSearchSidecar ?? {};
|
|
139
163
|
const vs = config.visionSidecar ?? {};
|
|
140
164
|
return jsonResponse({
|
|
141
|
-
webSearch: { model: ws.model ?? "gpt-5.
|
|
142
|
-
vision: { model: vs.model ?? "gpt-5.
|
|
165
|
+
webSearch: { model: ws.model ?? "gpt-5.6-luna", reasoning: ws.reasoning ?? "low" },
|
|
166
|
+
vision: { model: vs.model ?? "gpt-5.6-luna" },
|
|
143
167
|
});
|
|
144
168
|
}
|
|
145
169
|
|
|
@@ -160,8 +184,8 @@ export async function handleManagementAPI(req: Request, url: URL, config: OcxCon
|
|
|
160
184
|
const vs = config.visionSidecar ?? {};
|
|
161
185
|
return jsonResponse({
|
|
162
186
|
ok: true,
|
|
163
|
-
webSearch: { model: ws.model ?? "gpt-5.
|
|
164
|
-
vision: { model: vs.model ?? "gpt-5.
|
|
187
|
+
webSearch: { model: ws.model ?? "gpt-5.6-luna", reasoning: ws.reasoning ?? "low" },
|
|
188
|
+
vision: { model: vs.model ?? "gpt-5.6-luna" },
|
|
165
189
|
});
|
|
166
190
|
}
|
|
167
191
|
|
|
@@ -169,6 +193,38 @@ export async function handleManagementAPI(req: Request, url: URL, config: OcxCon
|
|
|
169
193
|
return jsonResponse(filterRequestLogs(getRequestLogEntries(), url.searchParams));
|
|
170
194
|
}
|
|
171
195
|
|
|
196
|
+
if (url.pathname === "/api/debug" && req.method === "GET") {
|
|
197
|
+
return jsonResponse(getDebugSettings());
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (url.pathname === "/api/debug/logs" && req.method === "GET") {
|
|
201
|
+
const { after, limit } = parseDebugLogQuery(url);
|
|
202
|
+
return jsonResponse(getDebugLogEntries({ after, limit }));
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (url.pathname === "/api/debug/usage-logs" && req.method === "GET") {
|
|
206
|
+
const { after, limit } = parseDebugLogQuery(url);
|
|
207
|
+
return jsonResponse(getUsageDebugLogEntries({ after, limit }));
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (url.pathname === "/api/debug" && req.method === "PUT") {
|
|
211
|
+
let body: { debug?: unknown; usage?: unknown; reset?: unknown };
|
|
212
|
+
try { body = await req.json(); } catch { return jsonResponse({ error: "invalid JSON body" }, 400); }
|
|
213
|
+
if (body.reset === true) return jsonResponse(clearDebugSettings());
|
|
214
|
+
if (body.reset === "debug" || body.reset === "provider") return jsonResponse(clearDebugSetting("debug"));
|
|
215
|
+
if (body.reset === "usage") return jsonResponse(clearDebugSetting("usage"));
|
|
216
|
+
const partial: Partial<Record<DebugFlag, boolean>> = {};
|
|
217
|
+
for (const key of ["debug", "usage"] as const) {
|
|
218
|
+
if (body[key] === undefined) continue;
|
|
219
|
+
if (typeof body[key] !== "boolean") return jsonResponse({ error: `${key} must be a boolean` }, 400);
|
|
220
|
+
partial[key] = body[key];
|
|
221
|
+
}
|
|
222
|
+
if (Object.keys(partial).length === 0) {
|
|
223
|
+
return jsonResponse({ error: "provide debug/usage booleans or reset:true" }, 400);
|
|
224
|
+
}
|
|
225
|
+
return jsonResponse(setDebugSettings(partial));
|
|
226
|
+
}
|
|
227
|
+
|
|
172
228
|
if (url.pathname === "/api/usage" && req.method === "GET") {
|
|
173
229
|
const range = parseRange(url.searchParams.get("range"));
|
|
174
230
|
const now = Date.now();
|
|
@@ -284,7 +340,17 @@ export async function handleManagementAPI(req: Request, url: URL, config: OcxCon
|
|
|
284
340
|
if (url.pathname === "/api/models" && req.method === "GET") {
|
|
285
341
|
const models = await fetchAllModels(config);
|
|
286
342
|
const disabled = new Set(config.disabledModels ?? []);
|
|
287
|
-
|
|
343
|
+
// Native GPT passthrough rows lead (provider "openai", bare-slug namespaced ids): sourced
|
|
344
|
+
// from the static supported set so a disabled model stays listed and re-enableable.
|
|
345
|
+
const native = nativeModelRows(config).map(row => ({
|
|
346
|
+
provider: "openai",
|
|
347
|
+
id: row.slug,
|
|
348
|
+
namespaced: row.slug,
|
|
349
|
+
disabled: row.disabled,
|
|
350
|
+
native: true,
|
|
351
|
+
...(row.contextWindow !== undefined ? { contextWindow: row.contextWindow } : {}),
|
|
352
|
+
}));
|
|
353
|
+
return jsonResponse([...native, ...models.map(m => {
|
|
288
354
|
const namespaced = `${m.provider}/${m.id}`;
|
|
289
355
|
const contextCap = providerContextCap(config, m.provider);
|
|
290
356
|
return {
|
|
@@ -293,7 +359,7 @@ export async function handleManagementAPI(req: Request, url: URL, config: OcxCon
|
|
|
293
359
|
disabled: disabled.has(namespaced),
|
|
294
360
|
...(contextCap !== undefined ? { contextCap, contextCapped: m.contextCapped === true } : {}),
|
|
295
361
|
};
|
|
296
|
-
}));
|
|
362
|
+
})]);
|
|
297
363
|
}
|
|
298
364
|
|
|
299
365
|
if (url.pathname === "/api/provider-context-caps" && req.method === "GET") {
|
|
@@ -365,6 +431,76 @@ export async function handleManagementAPI(req: Request, url: URL, config: OcxCon
|
|
|
365
431
|
return jsonResponse({ ok: true, disabled });
|
|
366
432
|
}
|
|
367
433
|
|
|
434
|
+
// multi_agent_v2 surface toggle. GET reports the flag + the agents.max_threads
|
|
435
|
+
// boot conflict; PUT flips it via the official `codex features` CLI and RESYNCS
|
|
436
|
+
// the catalog so multi-agent surface metadata stays fresh. The catalog build
|
|
437
|
+
// itself never writes config — this endpoint is the only server-side mutation
|
|
438
|
+
// surface for the flag.
|
|
439
|
+
if (url.pathname === "/api/v2" && req.method === "GET") {
|
|
440
|
+
const { isMultiAgentV2Enabled, hasAgentsMaxThreads, getMaxConcurrentThreads } = await import("../codex/features");
|
|
441
|
+
return jsonResponse({
|
|
442
|
+
enabled: isMultiAgentV2Enabled(),
|
|
443
|
+
agentsMaxThreadsConflict: hasAgentsMaxThreads(),
|
|
444
|
+
maxConcurrentThreadsPerSession: getMaxConcurrentThreads(),
|
|
445
|
+
multiAgentMode: config.multiAgentMode ?? "default",
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
if (url.pathname === "/api/v2" && req.method === "PUT") {
|
|
449
|
+
let body: { enabled?: unknown; maxConcurrentThreadsPerSession?: unknown; multiAgentMode?: unknown };
|
|
450
|
+
try { body = await req.json(); } catch { return jsonResponse({ error: "invalid JSON body" }, 400); }
|
|
451
|
+
const wantsFlag = body.enabled !== undefined;
|
|
452
|
+
const wantsThreads = body.maxConcurrentThreadsPerSession !== undefined;
|
|
453
|
+
const wantsMode = body.multiAgentMode !== undefined;
|
|
454
|
+
if (!wantsFlag && !wantsThreads && !wantsMode) return jsonResponse({ error: "body must set enabled, multiAgentMode, and/or maxConcurrentThreadsPerSession" }, 400);
|
|
455
|
+
if (wantsFlag && typeof body.enabled !== "boolean") return jsonResponse({ error: "body.enabled must be a boolean" }, 400);
|
|
456
|
+
if (wantsMode && body.multiAgentMode !== "v1" && body.multiAgentMode !== "default" && body.multiAgentMode !== "v2") {
|
|
457
|
+
return jsonResponse({ error: "body.multiAgentMode must be 'v1', 'default', or 'v2'" }, 400);
|
|
458
|
+
}
|
|
459
|
+
if (wantsThreads && (typeof body.maxConcurrentThreadsPerSession !== "number" || !Number.isInteger(body.maxConcurrentThreadsPerSession) || body.maxConcurrentThreadsPerSession < 1)) {
|
|
460
|
+
return jsonResponse({ error: "body.maxConcurrentThreadsPerSession must be an integer >= 1" }, 400);
|
|
461
|
+
}
|
|
462
|
+
const { isMultiAgentV2Enabled, hasAgentsMaxThreads, getMaxConcurrentThreads, setMaxConcurrentThreads } = await import("../codex/features");
|
|
463
|
+
const warnings: string[] = [];
|
|
464
|
+
if (wantsFlag && isMultiAgentV2Enabled() !== body.enabled) {
|
|
465
|
+
const { execFileSync } = await import("node:child_process");
|
|
466
|
+
const command = process.env.CODEX_CLI_PATH?.trim() || "codex";
|
|
467
|
+
try {
|
|
468
|
+
execFileSync(command, ["features", body.enabled ? "enable" : "disable", "multi_agent_v2"],
|
|
469
|
+
{ stdio: ["ignore", "pipe", "pipe"], timeout: 15_000, windowsHide: true });
|
|
470
|
+
} catch (err) {
|
|
471
|
+
return jsonResponse({ error: `codex features ${body.enabled ? "enable" : "disable"} failed: ${err instanceof Error ? err.message : String(err)}` }, 502);
|
|
472
|
+
}
|
|
473
|
+
await refreshCodexCatalogBestEffort();
|
|
474
|
+
}
|
|
475
|
+
if (wantsThreads) {
|
|
476
|
+
// setMaxConcurrentThreads is idempotent (equal value -> no write) and refuses
|
|
477
|
+
// when the [features.multi_agent_v2] table is missing, so a threads-only PUT
|
|
478
|
+
// against a never-enabled config fails loudly instead of inventing state.
|
|
479
|
+
const result = setMaxConcurrentThreads(body.maxConcurrentThreadsPerSession as number);
|
|
480
|
+
if (!result.ok) return jsonResponse({ error: result.error }, 409);
|
|
481
|
+
if (result.changed) warnings.push("Thread limit applies to new sessions.");
|
|
482
|
+
}
|
|
483
|
+
if (wantsMode) {
|
|
484
|
+
const mode = body.multiAgentMode as "v1" | "default" | "v2";
|
|
485
|
+
if (mode === "default") delete config.multiAgentMode;
|
|
486
|
+
else config.multiAgentMode = mode;
|
|
487
|
+
saveConfig(config);
|
|
488
|
+
await refreshCodexCatalogBestEffort();
|
|
489
|
+
warnings.push(`Multi-agent mode set to '${mode}'. Applies to new sessions.`);
|
|
490
|
+
}
|
|
491
|
+
if ((wantsFlag ? body.enabled === true : isMultiAgentV2Enabled()) && hasAgentsMaxThreads()) {
|
|
492
|
+
warnings.push("[agents] max_threads is set — codex refuses to start while multi_agent_v2 is enabled; remove it (features.multi_agent_v2.max_concurrent_threads_per_session replaces it).");
|
|
493
|
+
}
|
|
494
|
+
if (wantsFlag) warnings.push("Applies to new sessions; restart the Codex app or wait out its picker cache to see the ladder change.");
|
|
495
|
+
return jsonResponse({
|
|
496
|
+
ok: true,
|
|
497
|
+
enabled: isMultiAgentV2Enabled(),
|
|
498
|
+
maxConcurrentThreadsPerSession: getMaxConcurrentThreads(),
|
|
499
|
+
multiAgentMode: config.multiAgentMode ?? "default",
|
|
500
|
+
warnings,
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
|
|
368
504
|
// Which providers support real OAuth login (drives the GUI's "Log in with …" buttons).
|
|
369
505
|
if (url.pathname === "/api/oauth/providers" && req.method === "GET") {
|
|
370
506
|
return jsonResponse({ providers: listOAuthProviders() });
|
|
@@ -381,6 +517,31 @@ export async function handleManagementAPI(req: Request, url: URL, config: OcxCon
|
|
|
381
517
|
return jsonResponse({ providers: deriveProviderPresets() });
|
|
382
518
|
}
|
|
383
519
|
|
|
520
|
+
// Subagent prompt injection model: single native or routed model whose info is
|
|
521
|
+
// dynamically injected into the v1 proactive prompt. GET returns the current pick
|
|
522
|
+
// + available models; PUT sets or clears the pick.
|
|
523
|
+
if (url.pathname === "/api/injection-model" && req.method === "GET") {
|
|
524
|
+
const models = await fetchAllModels(config);
|
|
525
|
+
const disabled = new Set(config.disabledModels ?? []);
|
|
526
|
+
const { listCatalogNativeSlugs } = await import("../codex/catalog");
|
|
527
|
+
const nativeModels = listCatalogNativeSlugs()
|
|
528
|
+
.filter(slug => !disabled.has(slug))
|
|
529
|
+
.map(slug => ({ provider: "openai", model: slug, namespaced: slug }));
|
|
530
|
+
const routedModels = models
|
|
531
|
+
.map(m => ({ provider: m.provider, model: m.id, namespaced: `${m.provider}/${m.id}` }))
|
|
532
|
+
.filter(m => !disabled.has(m.namespaced));
|
|
533
|
+
return jsonResponse({ model: config.injectionModel ?? null, available: [...nativeModels, ...routedModels] });
|
|
534
|
+
}
|
|
535
|
+
if (url.pathname === "/api/injection-model" && req.method === "PUT") {
|
|
536
|
+
let body: { model?: unknown };
|
|
537
|
+
try { body = await req.json(); } catch { return jsonResponse({ error: "invalid JSON body" }, 400); }
|
|
538
|
+
const model = typeof body.model === "string" && body.model.length > 0 ? body.model : undefined;
|
|
539
|
+
if (model) config.injectionModel = model;
|
|
540
|
+
else delete config.injectionModel;
|
|
541
|
+
saveConfig(config);
|
|
542
|
+
return jsonResponse({ ok: true, model: config.injectionModel ?? null });
|
|
543
|
+
}
|
|
544
|
+
|
|
384
545
|
// Subagent model picker: which ≤5 routed models Codex's spawn_agent advertises (it shows the
|
|
385
546
|
// first 5 routed catalog entries). PUT reorders the injected catalog so the chosen ones lead.
|
|
386
547
|
if (url.pathname === "/api/subagent-models" && req.method === "GET") {
|
package/src/server/relay.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { isUsageDebugEnabled } from "../usage/debug";
|
|
|
3
3
|
import {
|
|
4
4
|
addRequestLog,
|
|
5
5
|
addFinalRequestLog,
|
|
6
|
-
|
|
6
|
+
httpStatusForRequestLogTerminal,
|
|
7
7
|
inspectResponseLogJson,
|
|
8
8
|
inspectResponseLogSsePayload,
|
|
9
9
|
type RequestLogContext,
|
|
@@ -249,7 +249,7 @@ export function responseWithDeferredRequestLog(
|
|
|
249
249
|
status => {
|
|
250
250
|
if (logged) return;
|
|
251
251
|
logged = true;
|
|
252
|
-
addFinalRequestLog(requestId, start, logCtx,
|
|
252
|
+
addFinalRequestLog(requestId, start, logCtx, httpStatusForRequestLogTerminal(status, logCtx), {
|
|
253
253
|
terminalStatus: status,
|
|
254
254
|
closeReason: "terminal",
|
|
255
255
|
}, addLog);
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import type { ResponsesTerminalStatus } from "../bridge";
|
|
3
|
+
import { httpStatusFromTerminalError as httpStatusFromClassifiedTerminalError } from "../lib/errors";
|
|
3
4
|
import { CODEX_CONFIG_PATH, readRootTomlString } from "../codex/paths";
|
|
4
5
|
import { readCodexCatalogPath } from "../codex/catalog";
|
|
5
6
|
import type { OcxUsage } from "../types";
|
|
7
|
+
import { redactSecretString } from "../lib/redact";
|
|
6
8
|
import {
|
|
7
9
|
appendUsageEntry,
|
|
8
10
|
usageForFinalLog,
|
|
@@ -35,6 +37,12 @@ export interface RequestLogContext {
|
|
|
35
37
|
usageDebugBodyKind?: UsageDebugBodyKind;
|
|
36
38
|
usageDebugBodySample?: string;
|
|
37
39
|
usageDebugContentType?: string;
|
|
40
|
+
/** Secret-redacted upstream error reason (e.g. the granular Cursor "rate limit exceeded…"
|
|
41
|
+
* message) extracted from a `response.failed` SSE payload or non-streaming error body, so the
|
|
42
|
+
* request log / GUI shows the actual upstream failure rather than only the HTTP-mapped code. */
|
|
43
|
+
upstreamError?: string;
|
|
44
|
+
/** HTTP status derived from a terminal `response.failed` SSE payload (429/401/503/etc.). */
|
|
45
|
+
terminalHttpStatus?: number;
|
|
38
46
|
}
|
|
39
47
|
|
|
40
48
|
export interface RequestLogEntry {
|
|
@@ -56,6 +64,8 @@ export interface RequestLogEntry {
|
|
|
56
64
|
errorCode?: string;
|
|
57
65
|
terminalStatus?: ResponsesTerminalStatus;
|
|
58
66
|
closeReason?: "terminal" | "client_cancel" | "non_stream";
|
|
67
|
+
/** Secret-redacted upstream error reason, surfaced in /api/logs and the GUI detail modal. */
|
|
68
|
+
upstreamError?: string;
|
|
59
69
|
usageStatus: UsageStatus;
|
|
60
70
|
usage?: OcxUsage;
|
|
61
71
|
totalTokens?: number;
|
|
@@ -159,12 +169,12 @@ export function usageFromResponsesPayload(usage: unknown): OcxUsage | undefined
|
|
|
159
169
|
const raw = usage as {
|
|
160
170
|
input_tokens?: unknown;
|
|
161
171
|
output_tokens?: unknown;
|
|
162
|
-
input_tokens_details?: { cached_tokens?: unknown };
|
|
172
|
+
input_tokens_details?: { cached_tokens?: unknown; cache_write_tokens?: unknown };
|
|
163
173
|
output_tokens_details?: { reasoning_tokens?: unknown };
|
|
164
174
|
total_tokens?: unknown;
|
|
165
175
|
prompt_tokens?: unknown;
|
|
166
176
|
completion_tokens?: unknown;
|
|
167
|
-
prompt_tokens_details?: { cached_tokens?: unknown };
|
|
177
|
+
prompt_tokens_details?: { cached_tokens?: unknown; cache_write_tokens?: unknown };
|
|
168
178
|
completion_tokens_details?: { reasoning_tokens?: unknown };
|
|
169
179
|
};
|
|
170
180
|
if (typeof raw.input_tokens === "number" && typeof raw.output_tokens === "number") {
|
|
@@ -175,6 +185,9 @@ export function usageFromResponsesPayload(usage: unknown): OcxUsage | undefined
|
|
|
175
185
|
...(typeof raw.input_tokens_details?.cached_tokens === "number"
|
|
176
186
|
? { cachedInputTokens: raw.input_tokens_details.cached_tokens }
|
|
177
187
|
: {}),
|
|
188
|
+
...(typeof raw.input_tokens_details?.cache_write_tokens === "number"
|
|
189
|
+
? { cacheCreationInputTokens: raw.input_tokens_details.cache_write_tokens }
|
|
190
|
+
: {}),
|
|
178
191
|
...(typeof raw.output_tokens_details?.reasoning_tokens === "number"
|
|
179
192
|
? { reasoningOutputTokens: raw.output_tokens_details.reasoning_tokens }
|
|
180
193
|
: {}),
|
|
@@ -188,6 +201,9 @@ export function usageFromResponsesPayload(usage: unknown): OcxUsage | undefined
|
|
|
188
201
|
...(typeof raw.prompt_tokens_details?.cached_tokens === "number"
|
|
189
202
|
? { cachedInputTokens: raw.prompt_tokens_details.cached_tokens }
|
|
190
203
|
: {}),
|
|
204
|
+
...(typeof raw.prompt_tokens_details?.cache_write_tokens === "number"
|
|
205
|
+
? { cacheCreationInputTokens: raw.prompt_tokens_details.cache_write_tokens }
|
|
206
|
+
: {}),
|
|
191
207
|
...(typeof raw.completion_tokens_details?.reasoning_tokens === "number"
|
|
192
208
|
? { reasoningOutputTokens: raw.completion_tokens_details.reasoning_tokens }
|
|
193
209
|
: {}),
|
|
@@ -202,6 +218,7 @@ export function inspectResponseLogJson(logCtx: RequestLogContext, text: string):
|
|
|
202
218
|
} catch {
|
|
203
219
|
/* body may not be JSON; request log metadata is best-effort only */
|
|
204
220
|
}
|
|
221
|
+
captureUpstreamError(logCtx, text);
|
|
205
222
|
if (isUsageDebugEnabled() && logCtx.usageDebugBodyKind === undefined) {
|
|
206
223
|
logCtx.usageDebugBodyKind = "json";
|
|
207
224
|
logCtx.usageDebugBodySample = truncateForDebug(text);
|
|
@@ -217,6 +234,7 @@ export function inspectResponseLogSsePayload(logCtx: RequestLogContext, payload:
|
|
|
217
234
|
} catch {
|
|
218
235
|
/* SSE block payload may not be JSON; metadata inspection is best-effort */
|
|
219
236
|
}
|
|
237
|
+
captureUpstreamError(logCtx, payload);
|
|
220
238
|
if (debugEnabled) {
|
|
221
239
|
if (!sseAlreadyMarked) {
|
|
222
240
|
logCtx.usageDebugBodyKind = "sse";
|
|
@@ -229,10 +247,75 @@ export function inspectResponseLogSsePayload(logCtx: RequestLogContext, payload:
|
|
|
229
247
|
}
|
|
230
248
|
}
|
|
231
249
|
|
|
250
|
+
/**
|
|
251
|
+
* Capture the upstream error reason into the request log context. Codex/consumer surfaces only
|
|
252
|
+
* see an HTTP-mapped error code (502 → upstream_server_error); the granular reason lives inside
|
|
253
|
+
* a `response.failed` SSE payload's `error.message` (the adapter's redacted upstream message) or
|
|
254
|
+
* a non-streaming JSON error body. We keep the FIRST non-empty reason (the original failure) and
|
|
255
|
+
* run it through redactSecretString so secrets never reach /api/logs. Pure; safe on any text.
|
|
256
|
+
*/
|
|
257
|
+
function captureUpstreamError(logCtx: RequestLogContext, text: string | null): void {
|
|
258
|
+
if (!text || logCtx.upstreamError) return;
|
|
259
|
+
try {
|
|
260
|
+
const json = JSON.parse(text) as {
|
|
261
|
+
type?: unknown;
|
|
262
|
+
error?: { message?: unknown };
|
|
263
|
+
last_error?: { message?: unknown };
|
|
264
|
+
response?: { error?: { type?: unknown; code?: unknown; message?: unknown } };
|
|
265
|
+
};
|
|
266
|
+
captureTerminalHttpStatus(logCtx, json);
|
|
267
|
+
const message = json?.error?.message
|
|
268
|
+
?? json?.last_error?.message
|
|
269
|
+
?? json?.response?.error?.message;
|
|
270
|
+
if (typeof message === "string" && message.trim()) {
|
|
271
|
+
logCtx.upstreamError = redactSecretString(message).slice(0, 500);
|
|
272
|
+
}
|
|
273
|
+
} catch {
|
|
274
|
+
/* not JSON; nothing to capture */
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function captureTerminalHttpStatus(
|
|
279
|
+
logCtx: RequestLogContext,
|
|
280
|
+
json: {
|
|
281
|
+
type?: unknown;
|
|
282
|
+
response?: { error?: { type?: unknown; code?: unknown; message?: unknown } };
|
|
283
|
+
},
|
|
284
|
+
): void {
|
|
285
|
+
if (logCtx.terminalHttpStatus !== undefined) return;
|
|
286
|
+
if (json.type !== "response.failed") return;
|
|
287
|
+
const error = json.response?.error;
|
|
288
|
+
if (!error || typeof error !== "object") return;
|
|
289
|
+
logCtx.terminalHttpStatus = httpStatusFromTerminalError({
|
|
290
|
+
type: typeof error.type === "string" ? error.type : undefined,
|
|
291
|
+
code: error.code === null || typeof error.code === "string" ? error.code : undefined,
|
|
292
|
+
message: typeof error.message === "string" ? error.message : undefined,
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/** Map a terminal Responses error object to the HTTP status we record in /api/logs. */
|
|
297
|
+
export function httpStatusFromTerminalError(error: {
|
|
298
|
+
type?: string;
|
|
299
|
+
code?: string | null;
|
|
300
|
+
message?: string;
|
|
301
|
+
} | undefined): number {
|
|
302
|
+
return httpStatusFromClassifiedTerminalError(error);
|
|
303
|
+
}
|
|
304
|
+
|
|
232
305
|
export function httpStatusForTerminalStatus(status: ResponsesTerminalStatus): number {
|
|
233
306
|
return status === "completed" ? 200 : 502;
|
|
234
307
|
}
|
|
235
308
|
|
|
309
|
+
export function httpStatusForRequestLogTerminal(
|
|
310
|
+
status: ResponsesTerminalStatus,
|
|
311
|
+
logCtx?: RequestLogContext,
|
|
312
|
+
): number {
|
|
313
|
+
if (status === "failed" && logCtx?.terminalHttpStatus !== undefined) {
|
|
314
|
+
return logCtx.terminalHttpStatus;
|
|
315
|
+
}
|
|
316
|
+
return httpStatusForTerminalStatus(status);
|
|
317
|
+
}
|
|
318
|
+
|
|
236
319
|
export function addFinalRequestLog(
|
|
237
320
|
requestId: string,
|
|
238
321
|
start: number,
|
|
@@ -270,6 +353,7 @@ export function addFinalRequestLog(
|
|
|
270
353
|
...(errorCode ? { errorCode } : {}),
|
|
271
354
|
...(meta?.terminalStatus ? { terminalStatus: meta.terminalStatus } : {}),
|
|
272
355
|
...(meta?.closeReason ? { closeReason: meta.closeReason } : {}),
|
|
356
|
+
...(logCtx.upstreamError ? { upstreamError: logCtx.upstreamError } : {}),
|
|
273
357
|
usageStatus,
|
|
274
358
|
...(loggedUsage ? { usage: loggedUsage } : {}),
|
|
275
359
|
...(totalTokens !== undefined ? { totalTokens } : {}),
|