@coseung2/opencodex 2.8.0-cs.13 → 2.8.0-cs.15
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/gui/dist/assets/index-BhXIu7c0.js +67 -0
- package/gui/dist/index.html +1 -1
- package/package.json +3 -3
- package/packages/ocx-notch/README.md +2 -1
- package/src/adapters/cursor/discovery.ts +6 -2
- package/src/adapters/cursor/effort-map.ts +3 -0
- package/src/adapters/google-antigravity-replay.ts +24 -0
- package/src/adapters/google.ts +16 -11
- package/src/chat/inbound.ts +5 -11
- package/src/cli/account-api.ts +9 -1
- package/src/cli/account-extended.ts +4 -1
- package/src/codex/account-label.ts +14 -1
- package/src/codex/account-lifecycle.ts +12 -1
- package/src/codex/account-namespaces.ts +21 -0
- package/src/codex/account-priority.ts +49 -0
- package/src/codex/account-store.ts +2 -1
- package/src/codex/auth-api.ts +108 -17
- package/src/codex/auth-context.ts +61 -16
- package/src/codex/catalog/metadata.ts +34 -12
- package/src/codex/catalog/parsing.ts +8 -1
- package/src/codex/catalog/provider-fetch.ts +24 -6
- package/src/codex/catalog.ts +1 -1
- package/src/codex/pool-rotation.ts +51 -4
- package/src/codex/quota.ts +154 -35
- package/src/codex/routing.ts +139 -33
- package/src/codex/warmup.ts +193 -85
- package/src/config.ts +84 -1
- package/src/lib/bounded-body.ts +13 -6
- package/src/lib/bun-stream-caps.ts +5 -6
- package/src/lib/redact.ts +13 -0
- package/src/oauth/index.ts +79 -12
- package/src/oauth/log.ts +3 -1
- package/src/oauth/store.ts +31 -8
- package/src/providers/antigravity-models.ts +53 -24
- package/src/providers/codex-capacity.ts +303 -0
- package/src/providers/model-rename-migration.ts +147 -0
- package/src/providers/model-rename-startup.ts +29 -0
- package/src/providers/quota.ts +126 -16
- package/src/providers/registry.ts +258 -38
- package/src/responses/parser.ts +19 -12
- package/src/responses/spill-store.ts +14 -1
- package/src/responses/state.ts +108 -14
- package/src/server/index.ts +9 -1
- package/src/server/management/logs-usage-routes.ts +1 -0
- package/src/server/management/oauth-account-routes.ts +8 -1
- package/src/server/relay.ts +10 -42
- package/src/server/request-log.ts +42 -1
- package/src/server/responses/compact.ts +16 -4
- package/src/server/responses/core.ts +217 -59
- package/src/server/responses/empty-completion-guard.ts +275 -0
- package/src/server/responses/encrypted-payload.ts +54 -39
- package/src/server/responses/fetch-helpers.ts +24 -3
- package/src/server/responses/ws-upstream.ts +318 -0
- package/src/server/sse-frame-buffer.ts +292 -0
- package/src/server/ws-bridge.ts +17 -11
- package/src/types.ts +8 -0
- package/src/usage/log.ts +24 -0
- package/src/usage/summary.ts +152 -2
- package/vendor/ocx-notch/win32-x64/ocx-notch.exe +0 -0
- package/gui/dist/assets/index-BucjyD4I.js +0 -67
|
@@ -45,9 +45,21 @@ export interface ResponseSpillRef {
|
|
|
45
45
|
payloadBytes: number;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
/** Single-spill ceiling enforced at admission and replay. */
|
|
49
|
+
export const MAX_RESPONSE_SPILL_PAYLOAD_BYTES = 256 * 1024 * 1024;
|
|
50
|
+
let spillPayloadCapOverride: number | null = null;
|
|
51
|
+
|
|
52
|
+
export function setResponseSpillPayloadCapForTests(bytes: number | null): void {
|
|
53
|
+
spillPayloadCapOverride = bytes;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function responseSpillPayloadCap(): number {
|
|
57
|
+
return spillPayloadCapOverride ?? MAX_RESPONSE_SPILL_PAYLOAD_BYTES;
|
|
58
|
+
}
|
|
59
|
+
|
|
48
60
|
export type ResponseSpillReadResult =
|
|
49
61
|
| { ok: true; payload: ResponseSpillPayload }
|
|
50
|
-
| { ok: false; reason: "missing" | "corrupt" };
|
|
62
|
+
| { ok: false; reason: "missing" | "corrupt" | "too_large" };
|
|
51
63
|
|
|
52
64
|
export interface ResponseSpillCleanupResult {
|
|
53
65
|
scanned: number;
|
|
@@ -306,6 +318,7 @@ export function writeResponseSpillDurably(
|
|
|
306
318
|
|
|
307
319
|
export function readResponseSpill(responseId: string, ref: ResponseSpillRef): ResponseSpillReadResult {
|
|
308
320
|
if (!validSpillRef(ref)) return { ok: false, reason: "corrupt" };
|
|
321
|
+
if (ref.payloadBytes > responseSpillPayloadCap()) return { ok: false, reason: "too_large" };
|
|
309
322
|
const match = OWNED_SPILL_NAME.exec(ref.fileName);
|
|
310
323
|
if (!match
|
|
311
324
|
|| match[2] !== sha256(responseId).slice(0, 12)
|
package/src/responses/state.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { chmodSync, existsSync, lstatSync, mkdirSync, opendirSync, readFileSync, rmSync, unlinkSync } from "node:fs";
|
|
1
|
+
import { chmodSync, existsSync, lstatSync, mkdirSync, opendirSync, readFileSync, rmSync, statSync, unlinkSync } from "node:fs";
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
import { isDeepStrictEqual } from "node:util";
|
|
4
4
|
import { atomicWriteFileAsync, getConfigDir } from "../config";
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
readResponseSpill,
|
|
11
11
|
recoverOrphanedResponseSpills,
|
|
12
12
|
responseSpillDirectory,
|
|
13
|
+
responseSpillPayloadCap,
|
|
13
14
|
type ResponseSpillRef,
|
|
14
15
|
writeResponseSpillDurably,
|
|
15
16
|
} from "./spill-store";
|
|
@@ -24,6 +25,7 @@ export const MAX_STORED_RESPONSE_BYTES = 64 * 1024 * 1024;
|
|
|
24
25
|
/** Legacy snapshot selection only. Spill demotion is governed solely by the RAM cap above. */
|
|
25
26
|
const SNAPSHOT_ENTRY_MAX_BYTES = 2 * 1024 * 1024;
|
|
26
27
|
const SNAPSHOT_TOTAL_MAX_BYTES = 24 * 1024 * 1024;
|
|
28
|
+
const SNAPSHOT_FILE_MAX_BYTES = 32 * 1024 * 1024;
|
|
27
29
|
const STALE_TEMP_GRACE_MS = 15 * 60 * 1_000;
|
|
28
30
|
const STALE_TEMP_MAX_ENTRIES = 4_096;
|
|
29
31
|
const STALE_TEMP_MAX_CLEANUPS = 512;
|
|
@@ -57,7 +59,7 @@ type ResidentInput = Omit<ResidentResponseState, "kind" | "sizeBytes">;
|
|
|
57
59
|
|
|
58
60
|
export type PreviousResponseReplayFailure = {
|
|
59
61
|
code: "previous_response_not_found";
|
|
60
|
-
reason: "spill_missing" | "spill_corrupt" | "spill_failed";
|
|
62
|
+
reason: "spill_missing" | "spill_corrupt" | "spill_failed" | "spill_too_large";
|
|
61
63
|
};
|
|
62
64
|
|
|
63
65
|
const states = new Map<string, StoredResponseState>();
|
|
@@ -68,6 +70,11 @@ let oldestResidentAt: number | null = null;
|
|
|
68
70
|
let byteCapOverride: number | null = null;
|
|
69
71
|
let stateRevision = 0;
|
|
70
72
|
const spillCounters = { writes: 0, writeFailures: 0, readFailures: 0 };
|
|
73
|
+
const admissionCounters = { directSpills: 0, oversizedDrops: 0, snapshotOversizedRefusals: 0 };
|
|
74
|
+
|
|
75
|
+
export function responseAdmissionCountersForTests(): Readonly<typeof admissionCounters> {
|
|
76
|
+
return admissionCounters;
|
|
77
|
+
}
|
|
71
78
|
// Superseded spill generations awaiting a durable snapshot before unlink
|
|
72
79
|
// (review C1-1: unlinking at swap time races a crash against the debounced
|
|
73
80
|
// snapshot — the reloaded OLD stub would point at a deleted file).
|
|
@@ -178,12 +185,25 @@ function deleteEntry(id: string, options: { deleteSpill?: boolean } = {}): void
|
|
|
178
185
|
if (options.deleteSpill !== false) deleteOwnedSpills(existing);
|
|
179
186
|
}
|
|
180
187
|
|
|
181
|
-
function replaceWithSpillFailure(
|
|
188
|
+
function replaceWithSpillFailure(
|
|
189
|
+
id: string,
|
|
190
|
+
expected?: StoredResponseState,
|
|
191
|
+
options: { deferSpillUnlink?: boolean } = {},
|
|
192
|
+
): void {
|
|
182
193
|
const existing = states.get(id);
|
|
183
194
|
if (expected && existing !== expected) return;
|
|
184
195
|
const failed = tombstone(id, expected?.createdAt ?? existing?.createdAt ?? now());
|
|
185
196
|
if (replaceMapEntry(id, failed, expected)) {
|
|
186
|
-
if (existing)
|
|
197
|
+
if (existing) {
|
|
198
|
+
if (options.deferSpillUnlink && existing.kind === "spill") {
|
|
199
|
+
pendingSpillUnlinks.push(existing.spill);
|
|
200
|
+
while (pendingSpillUnlinks.length > PENDING_SPILL_UNLINKS_MAX) {
|
|
201
|
+
deleteResponseSpill(pendingSpillUnlinks.shift()!);
|
|
202
|
+
}
|
|
203
|
+
} else {
|
|
204
|
+
deleteOwnedSpills(existing);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
187
207
|
}
|
|
188
208
|
}
|
|
189
209
|
|
|
@@ -237,7 +257,7 @@ function replaceSpillEntryAtomically(
|
|
|
237
257
|
}
|
|
238
258
|
} catch {
|
|
239
259
|
spillCounters.writeFailures += 1;
|
|
240
|
-
replaceWithSpillFailure(id, expected);
|
|
260
|
+
replaceWithSpillFailure(id, expected, { deferSpillUnlink: true });
|
|
241
261
|
}
|
|
242
262
|
}
|
|
243
263
|
|
|
@@ -252,6 +272,11 @@ function setResidentEntry(id: string, entry: ResidentInput): void {
|
|
|
252
272
|
pruneResponses();
|
|
253
273
|
return;
|
|
254
274
|
}
|
|
275
|
+
if (candidate.sizeBytes > byteCap()) {
|
|
276
|
+
admitOversizedCandidate(id, candidate, expected);
|
|
277
|
+
pruneResponses();
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
255
280
|
if (expected?.kind === "spill") {
|
|
256
281
|
replaceSpillEntryAtomically(id, expected, candidate);
|
|
257
282
|
pruneResponses();
|
|
@@ -261,6 +286,55 @@ function setResidentEntry(id: string, entry: ResidentInput): void {
|
|
|
261
286
|
pruneResponses();
|
|
262
287
|
}
|
|
263
288
|
|
|
289
|
+
/** Admit a candidate that can never fit in the resident map directly to spill. */
|
|
290
|
+
function admitOversizedCandidate(
|
|
291
|
+
id: string,
|
|
292
|
+
candidate: ResidentResponseState,
|
|
293
|
+
expected?: StoredResponseState,
|
|
294
|
+
): void {
|
|
295
|
+
if (candidate.sizeBytes > responseSpillPayloadCap()) {
|
|
296
|
+
admissionCounters.oversizedDrops += 1;
|
|
297
|
+
replaceWithSpillFailure(id, expected, { deferSpillUnlink: true });
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
try {
|
|
301
|
+
const ref = writeResponseSpillDurably(id, {
|
|
302
|
+
createdAt: candidate.createdAt,
|
|
303
|
+
items: candidate.items,
|
|
304
|
+
...(candidate.providers ? { providers: candidate.providers } : {}),
|
|
305
|
+
});
|
|
306
|
+
if (ref.payloadBytes > responseSpillPayloadCap()) {
|
|
307
|
+
deleteResponseSpill(ref);
|
|
308
|
+
admissionCounters.oversizedDrops += 1;
|
|
309
|
+
replaceWithSpillFailure(id, expected, { deferSpillUnlink: true });
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
const base: Omit<SpilledResponseState, "sizeBytes"> = {
|
|
313
|
+
kind: "spill",
|
|
314
|
+
createdAt: candidate.createdAt,
|
|
315
|
+
...(candidate.providers ? { providers: candidate.providers } : {}),
|
|
316
|
+
spill: ref,
|
|
317
|
+
};
|
|
318
|
+
const next: SpilledResponseState = { ...base, sizeBytes: stubSize(id, base) };
|
|
319
|
+
if (!replaceMapEntry(id, next, expected)) {
|
|
320
|
+
deleteResponseSpill(ref);
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
spillCounters.writes += 1;
|
|
324
|
+
admissionCounters.directSpills += 1;
|
|
325
|
+
noteStubSwapForTest();
|
|
326
|
+
if (expected?.kind === "spill") {
|
|
327
|
+
pendingSpillUnlinks.push(expected.spill);
|
|
328
|
+
while (pendingSpillUnlinks.length > PENDING_SPILL_UNLINKS_MAX) {
|
|
329
|
+
deleteResponseSpill(pendingSpillUnlinks.shift()!);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
} catch {
|
|
333
|
+
spillCounters.writeFailures += 1;
|
|
334
|
+
replaceWithSpillFailure(id, expected, { deferSpillUnlink: true });
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
264
338
|
// Expansion provenance must stay proxy-private: a WeakMap distinguishes replayed history from the
|
|
265
339
|
// newly appended input suffix without adding an unknown field that native passthrough could send
|
|
266
340
|
// upstream. The parser uses this boundary to acknowledge historical compaction markers exactly once.
|
|
@@ -335,8 +409,15 @@ function loadSnapshotEntry(id: string, value: unknown): void {
|
|
|
335
409
|
items: rec.items,
|
|
336
410
|
...(providers ? { providers } : {}),
|
|
337
411
|
});
|
|
338
|
-
if (resident)
|
|
339
|
-
|
|
412
|
+
if (!resident) {
|
|
413
|
+
replaceMapEntry(id, tombstone(id, rec.createdAt));
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
if (resident.sizeBytes > byteCap()) {
|
|
417
|
+
admitOversizedCandidate(id, resident, undefined);
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
replaceMapEntry(id, resident);
|
|
340
421
|
}
|
|
341
422
|
|
|
342
423
|
export interface ResponseStateTempRecoveryResult {
|
|
@@ -462,11 +543,20 @@ function ensureLoaded(): void {
|
|
|
462
543
|
}
|
|
463
544
|
try {
|
|
464
545
|
if (existsSync(path)) {
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
546
|
+
// Follow the target because readFileSync follows it too, but parse only a
|
|
547
|
+
// bounded regular file (never a FIFO/device or an oversized planted file).
|
|
548
|
+
const stat = statSync(path);
|
|
549
|
+
if (!stat.isFile()) {
|
|
550
|
+
// Unsupported target: start empty.
|
|
551
|
+
} else if (stat.size > SNAPSHOT_FILE_MAX_BYTES) {
|
|
552
|
+
admissionCounters.snapshotOversizedRefusals += 1;
|
|
553
|
+
} else {
|
|
554
|
+
const raw = JSON.parse(readFileSync(path, "utf-8")) as { version?: unknown; states?: unknown };
|
|
555
|
+
if ((raw.version === 1 || raw.version === 2) && Array.isArray(raw.states)) {
|
|
556
|
+
for (const entry of raw.states) {
|
|
557
|
+
if (!Array.isArray(entry) || entry.length !== 2 || typeof entry[0] !== "string") continue;
|
|
558
|
+
loadSnapshotEntry(entry[0], entry[1]);
|
|
559
|
+
}
|
|
470
560
|
}
|
|
471
561
|
}
|
|
472
562
|
}
|
|
@@ -505,7 +595,7 @@ async function writeBoundedSnapshot(path: string): Promise<SnapshotWriteOutcome>
|
|
|
505
595
|
persistable = smallState;
|
|
506
596
|
}
|
|
507
597
|
const persistEntry: [string, unknown] = [id, persistable];
|
|
508
|
-
const size = JSON.stringify(persistEntry)
|
|
598
|
+
const size = Buffer.byteLength(JSON.stringify(persistEntry), "utf8");
|
|
509
599
|
if (state.kind === "resident" && size > SNAPSHOT_ENTRY_MAX_BYTES) continue;
|
|
510
600
|
if (total + size > SNAPSHOT_TOTAL_MAX_BYTES) break;
|
|
511
601
|
total += size;
|
|
@@ -685,7 +775,11 @@ function materializeEntry(
|
|
|
685
775
|
spillCounters.readFailures += 1;
|
|
686
776
|
const failure: PreviousResponseReplayFailure = {
|
|
687
777
|
code: "previous_response_not_found",
|
|
688
|
-
reason: result.reason === "missing"
|
|
778
|
+
reason: result.reason === "missing"
|
|
779
|
+
? "spill_missing"
|
|
780
|
+
: result.reason === "too_large"
|
|
781
|
+
? "spill_too_large"
|
|
782
|
+
: "spill_corrupt",
|
|
689
783
|
};
|
|
690
784
|
replaceWithSpillFailure(id, entry);
|
|
691
785
|
schedulePersist();
|
package/src/server/index.ts
CHANGED
|
@@ -41,9 +41,11 @@ import { setStorageCleanupPolicyJobLiveApply } from "../storage/policy-job";
|
|
|
41
41
|
import { scheduleStorageCleanupStartupRun, startStorageCleanupScheduler } from "../storage/policy-scheduler";
|
|
42
42
|
import { runOpenAiTierStartupMigration } from "../providers/openai-tier-startup";
|
|
43
43
|
import { runAlibabaRegionStartupMigration } from "../providers/alibaba-region-startup";
|
|
44
|
+
import { runModelRenameStartupMigration } from "../providers/model-rename-startup";
|
|
44
45
|
import { isCanonicalOpenAiForwardProvider } from "../providers/openai-tiers";
|
|
45
46
|
import { providerCodexAccountMode } from "../providers/registry";
|
|
46
47
|
import type { StorageCleanupPolicy } from "../types";
|
|
48
|
+
import { MAX_DECOMPRESSED_BODY_BYTES } from "./request-decompress";
|
|
47
49
|
import {
|
|
48
50
|
CodexAccountCooldownError,
|
|
49
51
|
cooldownErrorMessage,
|
|
@@ -272,7 +274,9 @@ function attachLiveSidebandUpstream(ws: ServerWebSocket<WsData>): void {
|
|
|
272
274
|
// export function relaySseWithHeartbeat
|
|
273
275
|
|
|
274
276
|
export function startServer(port?: number) {
|
|
275
|
-
const config =
|
|
277
|
+
const config = runModelRenameStartupMigration(
|
|
278
|
+
runAlibabaRegionStartupMigration(runOpenAiTierStartupMigration(loadConfig())),
|
|
279
|
+
);
|
|
276
280
|
setLiveStateStoreConfig(config);
|
|
277
281
|
applyProxyEnv(config);
|
|
278
282
|
assertServerAuthConfig(config);
|
|
@@ -392,6 +396,10 @@ export function startServer(port?: number) {
|
|
|
392
396
|
port: listenPort,
|
|
393
397
|
hostname: bindHost,
|
|
394
398
|
idleTimeout: 255,
|
|
399
|
+
// Keep Bun's listener admission ceiling aligned with the bounded
|
|
400
|
+
// decompression reader. Bun otherwise rejects valid large Codex turns at
|
|
401
|
+
// its lower default before the application can enforce the 256 MiB cap.
|
|
402
|
+
maxRequestBodySize: MAX_DECOMPRESSED_BODY_BYTES,
|
|
395
403
|
async fetch(req, requestServer): Promise<Response> {
|
|
396
404
|
const url = new URL(req.url);
|
|
397
405
|
markActivity(`${req.method} ${url.pathname}`);
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
getLoginStatus,
|
|
20
20
|
isPublicOAuthProvider,
|
|
21
21
|
listOAuthProviders,
|
|
22
|
+
publicOAuthAuthenticationErrorMessage,
|
|
22
23
|
startLoginFlow,
|
|
23
24
|
submitManualLoginCode,
|
|
24
25
|
} from "../../oauth";
|
|
@@ -169,7 +170,13 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
|
|
|
169
170
|
return jsonResponse({ url: authUrl, instructions, deviceCode });
|
|
170
171
|
} catch (err) {
|
|
171
172
|
if (err instanceof OAuthMutationBusyError) throw err;
|
|
172
|
-
|
|
173
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
174
|
+
const duplicateLoginMessage = `A login for ${provider} is already in progress`;
|
|
175
|
+
return jsonResponse({
|
|
176
|
+
error: message === duplicateLoginMessage
|
|
177
|
+
? duplicateLoginMessage
|
|
178
|
+
: publicOAuthAuthenticationErrorMessage(err),
|
|
179
|
+
}, 409);
|
|
173
180
|
}
|
|
174
181
|
}
|
|
175
182
|
|
package/src/server/relay.ts
CHANGED
|
@@ -6,7 +6,6 @@ import {
|
|
|
6
6
|
addFinalRequestLog,
|
|
7
7
|
httpStatusForRequestLogTerminal,
|
|
8
8
|
inspectResponseLogJson,
|
|
9
|
-
inspectResponseLogSsePayload,
|
|
10
9
|
inspectResponseLogSsePayloadParsed,
|
|
11
10
|
recordFirstOutput,
|
|
12
11
|
type RequestLogContext,
|
|
@@ -245,10 +244,7 @@ export function trackSseForRequestLog(
|
|
|
245
244
|
onFirstOutput?: () => void,
|
|
246
245
|
): ReadableStream<Uint8Array> {
|
|
247
246
|
const reader = body.getReader();
|
|
248
|
-
const decoder = new TextDecoder();
|
|
249
|
-
let buffer = "";
|
|
250
247
|
let terminalReported = false;
|
|
251
|
-
const reportFirstOutput = createFirstOutputReporter(onFirstOutput);
|
|
252
248
|
|
|
253
249
|
const reportTerminal = (status: ResponsesTerminalStatus) => {
|
|
254
250
|
if (terminalReported) return;
|
|
@@ -256,42 +252,29 @@ export function trackSseForRequestLog(
|
|
|
256
252
|
onTerminal(status);
|
|
257
253
|
};
|
|
258
254
|
|
|
259
|
-
const
|
|
260
|
-
if (!payload) return;
|
|
261
|
-
if (logCtx) inspectResponseLogSsePayload(logCtx, payload);
|
|
262
|
-
reportFirstOutput.payload(payload);
|
|
263
|
-
const status = terminalStatusFromSsePayload(payload);
|
|
264
|
-
if (status) reportTerminal(status);
|
|
265
|
-
};
|
|
266
|
-
|
|
267
|
-
const inspectChunk = (value: Uint8Array) => {
|
|
268
|
-
buffer += decoder.decode(value, { stream: true });
|
|
269
|
-
let next: { block: string; rest: string } | null;
|
|
270
|
-
while ((next = nextSseBlock(buffer))) {
|
|
271
|
-
buffer = next.rest;
|
|
272
|
-
inspectPayload(sseDataPayload(next.block));
|
|
273
|
-
}
|
|
274
|
-
};
|
|
255
|
+
const inspector = createSseInspector({ onTerminal: reportTerminal, logCtx, onFirstOutput });
|
|
275
256
|
|
|
276
257
|
return new ReadableStream<Uint8Array>({
|
|
277
258
|
async pull(controller) {
|
|
278
259
|
try {
|
|
279
260
|
const { done, value } = await reader.read();
|
|
280
261
|
if (done) {
|
|
281
|
-
|
|
282
|
-
if (buffer.trim()) inspectPayload(sseDataPayload(buffer));
|
|
262
|
+
inspector.finish();
|
|
283
263
|
if (!terminalReported) reportTerminal("incomplete");
|
|
264
|
+
inspector.dispose();
|
|
284
265
|
controller.close();
|
|
285
266
|
return;
|
|
286
267
|
}
|
|
287
|
-
|
|
268
|
+
inspector.feed(value);
|
|
288
269
|
controller.enqueue(value);
|
|
289
270
|
} catch (err) {
|
|
290
271
|
if (!terminalReported) reportTerminal("incomplete");
|
|
272
|
+
inspector.dispose();
|
|
291
273
|
try { controller.error(err); } catch { /* already torn down */ }
|
|
292
274
|
}
|
|
293
275
|
},
|
|
294
276
|
cancel(reason) {
|
|
277
|
+
inspector.dispose();
|
|
295
278
|
onCancel();
|
|
296
279
|
reader.cancel(reason).catch(() => {});
|
|
297
280
|
},
|
|
@@ -402,13 +385,11 @@ export function relaySseWithHeartbeat(
|
|
|
402
385
|
): ReadableStream<Uint8Array> | null {
|
|
403
386
|
if (!body) return null;
|
|
404
387
|
const reader = body.getReader();
|
|
405
|
-
const decoder = new TextDecoder();
|
|
406
388
|
const heartbeat = new TextEncoder().encode(": opencodex keepalive\n\n");
|
|
407
389
|
let timer: ReturnType<typeof setInterval> | undefined;
|
|
408
390
|
let closed = false;
|
|
409
391
|
let clientCancelled = false;
|
|
410
392
|
let terminalReported = false;
|
|
411
|
-
let buffer = "";
|
|
412
393
|
|
|
413
394
|
const reportTerminal = (status: ResponsesTerminalStatus) => {
|
|
414
395
|
if (terminalReported || clientCancelled || closed) return;
|
|
@@ -416,24 +397,12 @@ export function relaySseWithHeartbeat(
|
|
|
416
397
|
onTerminal?.(status);
|
|
417
398
|
};
|
|
418
399
|
|
|
419
|
-
const
|
|
420
|
-
if (!payload) return;
|
|
421
|
-
const status = terminalStatusFromSsePayload(payload);
|
|
422
|
-
if (status) reportTerminal(status);
|
|
423
|
-
};
|
|
424
|
-
|
|
425
|
-
const inspectChunk = (value: Uint8Array) => {
|
|
426
|
-
buffer += decoder.decode(value, { stream: true });
|
|
427
|
-
let next: { block: string; rest: string } | null;
|
|
428
|
-
while ((next = nextSseBlock(buffer))) {
|
|
429
|
-
buffer = next.rest;
|
|
430
|
-
inspectPayload(sseDataPayload(next.block));
|
|
431
|
-
}
|
|
432
|
-
};
|
|
400
|
+
const inspector = createSseInspector({ onTerminal: reportTerminal });
|
|
433
401
|
|
|
434
402
|
const cleanup = () => {
|
|
435
403
|
if (closed) return;
|
|
436
404
|
closed = true;
|
|
405
|
+
inspector.dispose();
|
|
437
406
|
if (timer) clearInterval(timer);
|
|
438
407
|
timer = undefined;
|
|
439
408
|
options?.onDone?.();
|
|
@@ -455,14 +424,13 @@ export function relaySseWithHeartbeat(
|
|
|
455
424
|
try {
|
|
456
425
|
const { done, value } = await reader.read();
|
|
457
426
|
if (done) {
|
|
458
|
-
|
|
459
|
-
if (buffer.trim()) inspectPayload(sseDataPayload(buffer));
|
|
427
|
+
inspector.finish();
|
|
460
428
|
if (!terminalReported && !clientCancelled) reportTerminal("incomplete");
|
|
461
429
|
cleanup();
|
|
462
430
|
controller.close();
|
|
463
431
|
return;
|
|
464
432
|
}
|
|
465
|
-
|
|
433
|
+
inspector.feed(value);
|
|
466
434
|
controller.enqueue(value);
|
|
467
435
|
} catch (err) {
|
|
468
436
|
if (!clientCancelled) reportTerminal("incomplete");
|
|
@@ -9,9 +9,10 @@ import { CODEX_CONFIG_PATH, readRootTomlString } from "../codex/paths";
|
|
|
9
9
|
import { readCodexCatalogPath } from "../codex/catalog";
|
|
10
10
|
import type { OcxUsage } from "../types";
|
|
11
11
|
import type { AdapterRequest } from "../adapters/base";
|
|
12
|
-
import { redactSecretString } from "../lib/redact";
|
|
12
|
+
import { redactSecretString, sanitizeLogMetadataString } from "../lib/redact";
|
|
13
13
|
import {
|
|
14
14
|
appendUsageEntry,
|
|
15
|
+
isCodexUsageAccountLogLabel,
|
|
15
16
|
isKnownAdmissionKind,
|
|
16
17
|
isKnownInboundProtocol,
|
|
17
18
|
isKnownUsageSurface,
|
|
@@ -37,6 +38,8 @@ import { enforceAppOwnedMemoryBudget, type RetainedStoreSnapshot } from "../lib/
|
|
|
37
38
|
export interface RequestLogContext {
|
|
38
39
|
model: string;
|
|
39
40
|
provider: string;
|
|
41
|
+
/** Stable non-PII Codex Pool account identity for durable usage attribution. */
|
|
42
|
+
accountLogLabel?: string;
|
|
40
43
|
/** TTFT: ms from request start to the first non-empty model output delta (WP4, devlog 040). */
|
|
41
44
|
firstOutputMs?: number;
|
|
42
45
|
/** Best-effort chat/session correlation for Logs grouping (#330). Opaque; omit when unknown. */
|
|
@@ -53,6 +56,8 @@ export interface RequestLogContext {
|
|
|
53
56
|
* since both leave it undefined. */
|
|
54
57
|
inboundProtocol?: "responses" | "chat" | "messages";
|
|
55
58
|
requestedModel?: string;
|
|
59
|
+
/** Original helper model when shadow-call interception rewrote the request. */
|
|
60
|
+
shadowCallRewrittenFrom?: string;
|
|
56
61
|
/** Internal structural combo identity; omitted from RequestLogEntry/JSONL. */
|
|
57
62
|
comboId?: string;
|
|
58
63
|
requestedEffort?: string;
|
|
@@ -102,6 +107,7 @@ export interface RequestLogEntry {
|
|
|
102
107
|
timestamp: number;
|
|
103
108
|
model: string;
|
|
104
109
|
provider: string;
|
|
110
|
+
accountLogLabel?: string;
|
|
105
111
|
/** TTFT: ms from request start to the first non-empty model output delta; unset for non-streaming/tool-only. */
|
|
106
112
|
firstOutputMs?: number;
|
|
107
113
|
surface?: "claude" | "claude-desktop" | "grok";
|
|
@@ -118,6 +124,8 @@ export interface RequestLogEntry {
|
|
|
118
124
|
/** Best-effort chat/session correlation for Logs grouping (#330). */
|
|
119
125
|
conversationId?: string;
|
|
120
126
|
requestedModel?: string;
|
|
127
|
+
/** Original helper model when shadow-call interception rewrote the request. */
|
|
128
|
+
shadowCallRewrittenFrom?: string;
|
|
121
129
|
requestedEffort?: string;
|
|
122
130
|
effectiveEffort?: string;
|
|
123
131
|
reasoningWireField?: string;
|
|
@@ -219,10 +227,16 @@ export function requestLogEntryFromPersistedUsage(entry: PersistedUsageEntry): R
|
|
|
219
227
|
timestamp: entry.timestamp,
|
|
220
228
|
model: entry.model,
|
|
221
229
|
provider: entry.provider,
|
|
230
|
+
...(isCodexUsageAccountLogLabel(entry.accountLogLabel)
|
|
231
|
+
? { accountLogLabel: entry.accountLogLabel }
|
|
232
|
+
: {}),
|
|
222
233
|
...(entry.firstOutputMs !== undefined ? { firstOutputMs: entry.firstOutputMs } : {}),
|
|
223
234
|
...(isKnownUsageSurface(entry.surface) ? { surface: entry.surface } : {}),
|
|
224
235
|
...(entry.conversationId ? { conversationId: entry.conversationId } : {}),
|
|
225
236
|
...(entry.requestedModel ? { requestedModel: entry.requestedModel } : {}),
|
|
237
|
+
...(entry.shadowCallRewrittenFrom
|
|
238
|
+
? { shadowCallRewrittenFrom: entry.shadowCallRewrittenFrom }
|
|
239
|
+
: {}),
|
|
226
240
|
...(entry.requestedEffort ? { requestedEffort: entry.requestedEffort } : {}),
|
|
227
241
|
...(entry.effectiveEffort ? { effectiveEffort: entry.effectiveEffort } : {}),
|
|
228
242
|
...(entry.reasoningWireField ? { reasoningWireField: entry.reasoningWireField } : {}),
|
|
@@ -281,6 +295,14 @@ export function hydrateRequestLogsFromDisk(
|
|
|
281
295
|
}
|
|
282
296
|
|
|
283
297
|
export function addRequestLog(entry: RequestLogEntry) {
|
|
298
|
+
const safeEntry = { ...entry };
|
|
299
|
+
if (safeEntry.accountLogLabel !== undefined && !isCodexUsageAccountLogLabel(safeEntry.accountLogLabel)) {
|
|
300
|
+
delete safeEntry.accountLogLabel;
|
|
301
|
+
}
|
|
302
|
+
const shadowCallRewrittenFrom = sanitizeLogMetadataString(safeEntry.shadowCallRewrittenFrom);
|
|
303
|
+
if (shadowCallRewrittenFrom) safeEntry.shadowCallRewrittenFrom = shadowCallRewrittenFrom;
|
|
304
|
+
else delete safeEntry.shadowCallRewrittenFrom;
|
|
305
|
+
entry = safeEntry;
|
|
284
306
|
retainRequestLogEntry(entry);
|
|
285
307
|
try {
|
|
286
308
|
// Failure diagnostics survive the 200-entry ring buffer by riding the persisted
|
|
@@ -299,6 +321,9 @@ export function addRequestLog(entry: RequestLogEntry) {
|
|
|
299
321
|
timestamp: entry.timestamp,
|
|
300
322
|
provider: entry.provider,
|
|
301
323
|
model: entry.model,
|
|
324
|
+
...(isCodexUsageAccountLogLabel(entry.accountLogLabel)
|
|
325
|
+
? { accountLogLabel: entry.accountLogLabel }
|
|
326
|
+
: {}),
|
|
302
327
|
...(isKnownUsageSurface(entry.surface) ? { surface: entry.surface } : {}),
|
|
303
328
|
// This function REBUILDS the persisted row field by field rather than
|
|
304
329
|
// spreading it, so a field missing here reaches /api/logs and never
|
|
@@ -309,6 +334,9 @@ export function addRequestLog(entry: RequestLogEntry) {
|
|
|
309
334
|
...(entry.conversationId ? { conversationId: entry.conversationId } : {}),
|
|
310
335
|
...(entry.resolvedModel ? { resolvedModel: entry.resolvedModel } : {}),
|
|
311
336
|
...(entry.requestedModel ? { requestedModel: entry.requestedModel } : {}),
|
|
337
|
+
...(entry.shadowCallRewrittenFrom
|
|
338
|
+
? { shadowCallRewrittenFrom: entry.shadowCallRewrittenFrom }
|
|
339
|
+
: {}),
|
|
312
340
|
...(entry.requestedEffort ? { requestedEffort: entry.requestedEffort } : {}),
|
|
313
341
|
...(entry.effectiveEffort ? { effectiveEffort: entry.effectiveEffort } : {}),
|
|
314
342
|
...(entry.reasoningWireField ? { reasoningWireField: entry.reasoningWireField } : {}),
|
|
@@ -786,12 +814,18 @@ export function addFinalRequestLog(
|
|
|
786
814
|
timestamp: start,
|
|
787
815
|
model: isCombo ? logCtx.requestedModel! : logCtx.model,
|
|
788
816
|
provider: isCombo ? "combo" : logCtx.provider,
|
|
817
|
+
...(isCodexUsageAccountLogLabel(logCtx.accountLogLabel)
|
|
818
|
+
? { accountLogLabel: logCtx.accountLogLabel }
|
|
819
|
+
: {}),
|
|
789
820
|
...(logCtx.surface ? { surface: logCtx.surface } : {}),
|
|
790
821
|
...(logCtx.apiKeyId ? { apiKeyId: logCtx.apiKeyId } : {}),
|
|
791
822
|
...(logCtx.admissionKind ? { admissionKind: logCtx.admissionKind } : {}),
|
|
792
823
|
...(logCtx.inboundProtocol ? { inboundProtocol: logCtx.inboundProtocol } : {}),
|
|
793
824
|
...(logCtx.conversationId ? { conversationId: logCtx.conversationId } : {}),
|
|
794
825
|
...(logCtx.requestedModel ? { requestedModel: logCtx.requestedModel } : {}),
|
|
826
|
+
...(sanitizeLogMetadataString(logCtx.shadowCallRewrittenFrom)
|
|
827
|
+
? { shadowCallRewrittenFrom: sanitizeLogMetadataString(logCtx.shadowCallRewrittenFrom)! }
|
|
828
|
+
: {}),
|
|
795
829
|
...(logCtx.requestedEffort ? { requestedEffort: logCtx.requestedEffort } : {}),
|
|
796
830
|
...(logCtx.effectiveEffort ? { effectiveEffort: logCtx.effectiveEffort } : {}),
|
|
797
831
|
...(logCtx.reasoningWireField ? { reasoningWireField: logCtx.reasoningWireField } : {}),
|
|
@@ -850,6 +884,10 @@ export function filterRequestLogs(logs: RequestLogEntry[], params: URLSearchPara
|
|
|
850
884
|
? filtered.filter(entry => Math.floor(entry.status / 100) === Number(status[0]))
|
|
851
885
|
: filtered.filter(entry => String(entry.status) === status);
|
|
852
886
|
}
|
|
887
|
+
const helper = params.get("helper")?.trim().toLowerCase();
|
|
888
|
+
if (helper === "intercepted" || helper === "1" || helper === "true") {
|
|
889
|
+
filtered = filtered.filter(entry => Boolean(entry.shadowCallRewrittenFrom));
|
|
890
|
+
}
|
|
853
891
|
const tailRaw = params.get("tail")?.trim();
|
|
854
892
|
if (tailRaw) {
|
|
855
893
|
const tail = Number.parseInt(tailRaw, 10);
|
|
@@ -939,10 +977,13 @@ export function sealRequestAttemptIdentity(
|
|
|
939
977
|
attempt: PersistedUsageAttempt | undefined,
|
|
940
978
|
provider: string,
|
|
941
979
|
adapter: string,
|
|
980
|
+
accountLogLabel?: string,
|
|
942
981
|
): void {
|
|
943
982
|
if (!attempt) return;
|
|
944
983
|
attempt.provider = provider;
|
|
945
984
|
attempt.adapter = adapter;
|
|
985
|
+
if (isCodexUsageAccountLogLabel(accountLogLabel)) attempt.accountLogLabel = accountLogLabel;
|
|
986
|
+
else delete attempt.accountLogLabel;
|
|
946
987
|
}
|
|
947
988
|
|
|
948
989
|
export function noteAttemptSend(
|
|
@@ -54,6 +54,7 @@ import {
|
|
|
54
54
|
codexProbeQuotaScope,
|
|
55
55
|
type CodexAuthContext,
|
|
56
56
|
} from "../../codex/auth-context";
|
|
57
|
+
import { codexAuthContextLogLabel } from "../../codex/account-label";
|
|
57
58
|
import {
|
|
58
59
|
formatCodexProviderForLog,
|
|
59
60
|
recordCodexUpstreamOutcome,
|
|
@@ -184,6 +185,10 @@ export async function handleResponsesCompact(
|
|
|
184
185
|
return formatErrorResponse(404, "invalid_request_error", err instanceof Error ? err.message : String(err));
|
|
185
186
|
}
|
|
186
187
|
const selectedModelId = route.modelId;
|
|
188
|
+
const qualifiedRoute = route as typeof route & {
|
|
189
|
+
codexAccountId?: string;
|
|
190
|
+
codexAccountNamespace?: string;
|
|
191
|
+
};
|
|
187
192
|
logCtx.requestedModel = raw.model;
|
|
188
193
|
logCtx.model = selectedModelId;
|
|
189
194
|
logCtx.provider = route.providerName;
|
|
@@ -197,7 +202,7 @@ export async function handleResponsesCompact(
|
|
|
197
202
|
logCtx.resolvedModel = route.modelId;
|
|
198
203
|
}
|
|
199
204
|
|
|
200
|
-
if (route.codexAccountMode === "direct") {
|
|
205
|
+
if (route.codexAccountMode === "direct" && qualifiedRoute.codexAccountId === undefined) {
|
|
201
206
|
try { validateForwardAdmissionCredential(req.headers, config); }
|
|
202
207
|
catch (err) {
|
|
203
208
|
if (err instanceof ForwardAdmissionCredentialError) return formatErrorResponse(401, "authentication_error", err.message);
|
|
@@ -218,7 +223,11 @@ export async function handleResponsesCompact(
|
|
|
218
223
|
const headers = new Headers({ "content-type": "application/json" });
|
|
219
224
|
try {
|
|
220
225
|
if (route.codexAccountMode) {
|
|
221
|
-
authCtx = await resolveCodexAuthContext(req.headers, config, route.codexAccountMode, {
|
|
226
|
+
authCtx = await resolveCodexAuthContext(req.headers, config, route.codexAccountMode, {
|
|
227
|
+
accountId: qualifiedRoute.codexAccountId,
|
|
228
|
+
modelId: selectedModelId,
|
|
229
|
+
});
|
|
230
|
+
logCtx.accountLogLabel = codexAuthContextLogLabel(authCtx, config);
|
|
222
231
|
const selected = headersForCodexAuthContext(req.headers, authCtx);
|
|
223
232
|
compactProvider = applyCodexAuthContextToProvider(route.provider, authCtx, route.codexAccountMode);
|
|
224
233
|
for (const name of FORWARD_HEADERS) {
|
|
@@ -233,7 +242,7 @@ export async function handleResponsesCompact(
|
|
|
233
242
|
}
|
|
234
243
|
} catch (err) {
|
|
235
244
|
if (err instanceof CodexAccountCooldownError) {
|
|
236
|
-
return cooldownErrorResponse(err);
|
|
245
|
+
return cooldownErrorResponse(err, Date.now(), qualifiedRoute.codexAccountNamespace);
|
|
237
246
|
}
|
|
238
247
|
if (err instanceof CodexThreadAffinityExpiredError) {
|
|
239
248
|
return formatErrorResponse(409, "invalid_request_error", "Codex thread account affinity expired; start a new session");
|
|
@@ -247,7 +256,9 @@ export async function handleResponsesCompact(
|
|
|
247
256
|
throw err;
|
|
248
257
|
}
|
|
249
258
|
const base = (compactProvider.baseUrl ?? "").replace(/\/$/, "");
|
|
250
|
-
if (compactProvider.
|
|
259
|
+
if (compactProvider.authMode !== "forward" && compactProvider.apiKey) {
|
|
260
|
+
headers.set("authorization", `Bearer ${resolveEnvValue(compactProvider.apiKey)}`);
|
|
261
|
+
}
|
|
251
262
|
const { reasoning: _reasoning, ...compactBodyRaw } = raw as typeof raw & { reasoning?: unknown };
|
|
252
263
|
// The regular /v1/responses path applies sanitizeReasoningInputContent via the adapter's
|
|
253
264
|
// buildRequest, but the compact endpoint forwards directly. Apply the same sanitizer here
|
|
@@ -264,6 +275,7 @@ export async function handleResponsesCompact(
|
|
|
264
275
|
recordCodexUpstreamOutcome(config, authCtx.accountId, outcome, {
|
|
265
276
|
...meta,
|
|
266
277
|
threadId: compactThreadId,
|
|
278
|
+
fixedAccount: authCtx.fixedAccount,
|
|
267
279
|
modelId: selectedModelId,
|
|
268
280
|
probeLeaseId: codexProbeLeaseId(authCtx),
|
|
269
281
|
probeQuotaScope: codexProbeQuotaScope(authCtx),
|