@bitkyc08/opencodex 2.10.2 → 2.11.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.md +31 -0
- package/bin/ocx.mjs +10 -0
- package/gui/dist/assets/index-Bk-PN-70.css +1 -0
- package/gui/dist/assets/index-BynIEIV-.js +70 -0
- package/gui/dist/index.html +2 -2
- package/package.json +4 -2
- package/src/adapters/cursor/effort-map.ts +11 -0
- package/src/adapters/cursor/live-transport.ts +11 -0
- package/src/adapters/cursor/native-exec-fs.ts +9 -6
- package/src/adapters/cursor/native-exec.ts +4 -2
- package/src/adapters/cursor/protobuf-events.ts +176 -4
- package/src/adapters/cursor/request-builder.ts +15 -4
- package/src/adapters/cursor/tool-definitions.ts +118 -2
- package/src/adapters/google.ts +15 -5
- package/src/adapters/openai-chat.ts +24 -2
- package/src/adapters/openai-responses.ts +2 -1
- package/src/bridge.ts +9 -5
- package/src/chat/outbound.ts +4 -3
- package/src/claude/desktop-3p.ts +222 -2
- package/src/claude/outbound.ts +15 -6
- package/src/cli/account-api.ts +4 -0
- package/src/cli/account-extended.ts +112 -0
- package/src/cli/account.ts +23 -6
- package/src/cli/claude-desktop.ts +26 -3
- package/src/cli/config-command.ts +9 -0
- package/src/cli/help.ts +18 -2
- package/src/cli/index.ts +277 -55
- package/src/cli/models.ts +5 -1
- package/src/cli/provider.ts +8 -2
- package/src/cli/ready.ts +301 -0
- package/src/cli/system-restart-client.ts +146 -0
- package/src/cli/tray-proxy.ts +153 -6
- package/src/clients/config-export.ts +12 -19
- package/src/codex/account-lifecycle.ts +3 -0
- package/src/codex/account-namespaces.ts +49 -3
- package/src/codex/account-priority.ts +83 -0
- package/src/codex/auth-api.ts +83 -0
- package/src/codex/auth-context.ts +5 -2
- package/src/codex/catalog/provider-fetch.ts +11 -0
- package/src/codex/catalog/sync.ts +23 -1
- package/src/codex/codex-write-lock.ts +16 -4
- package/src/codex/desired-state.ts +37 -4
- package/src/codex/history-job.ts +15 -5
- package/src/codex/history-provider.ts +31 -14
- package/src/codex/history-worker.ts +28 -4
- package/src/codex/inject-coordination.ts +13 -1
- package/src/codex/inject.ts +360 -66
- package/src/codex/internal/history-writer.ts +1 -1
- package/src/codex/native-main-lock-file.ts +5 -1
- package/src/codex/native-main-owner.ts +17 -3
- package/src/codex/native-profile-manager.ts +19 -0
- package/src/codex/native-profile-startup.ts +8 -0
- package/src/codex/native-residue.ts +140 -27
- package/src/codex/pool-rotation.ts +74 -4
- package/src/codex/refresh.ts +7 -0
- package/src/codex/routing.ts +177 -36
- package/src/codex/subagent-model-fallback.ts +34 -4
- package/src/codex/sync.ts +61 -0
- package/src/codex/upstream-host-health.ts +329 -31
- package/src/combos/request.ts +2 -0
- package/src/config.ts +221 -2
- package/src/images/loop.ts +1 -1
- package/src/integrations/native/ownership-preflight.ts +39 -2
- package/src/lib/bun-stream-caps.ts +3 -3
- package/src/lib/sse-decoder.ts +41 -0
- package/src/lib/system-restart-contract.ts +73 -0
- package/src/lib/windows-secret-acl.ts +141 -39
- package/src/lib/windows-user-principal.ts +283 -0
- package/src/lib/winsw.ts +18 -2
- package/src/oauth/key-providers.ts +12 -0
- package/src/providers/derive.ts +54 -2
- package/src/providers/free-directory.ts +6 -5
- package/src/providers/model-discovery.ts +9 -3
- package/src/providers/quota.ts +592 -0
- package/src/providers/registry.ts +316 -13
- package/src/responses/parser.ts +26 -10
- package/src/responses/reasoning-replay-cache.ts +1 -0
- package/src/routing/profile-namespace.ts +15 -0
- package/src/routing/profile.ts +2 -1
- package/src/server/auth-cors.ts +44 -13
- package/src/server/chat-completions.ts +0 -4
- package/src/server/claude-messages.ts +73 -15
- package/src/server/github-copilot-responses-repair.ts +338 -0
- package/src/server/index.ts +328 -111
- package/src/server/lifecycle.ts +36 -0
- package/src/server/management/agent-settings-routes.ts +147 -56
- package/src/server/management/config-routes.ts +7 -2
- package/src/server/management/context.ts +4 -0
- package/src/server/management/native-integration-routes.ts +199 -20
- package/src/server/management/provider-routes.ts +41 -0
- package/src/server/management/routing-profile-routes.ts +234 -5
- package/src/server/management/system-restart.ts +12 -10
- package/src/server/management/system-routes.ts +20 -0
- package/src/server/management-auth.ts +51 -3
- package/src/server/ports.ts +41 -1
- package/src/server/proxy-liveness.ts +129 -4
- package/src/server/readiness.ts +99 -0
- package/src/server/relay.ts +113 -97
- package/src/server/request-log.ts +10 -4
- package/src/server/responses/compact.ts +107 -12
- package/src/server/responses/core.ts +220 -39
- package/src/server/responses-item-id-repair.ts +22 -3
- package/src/server/responses-model-rewrite.ts +29 -0
- package/src/server/sse-frame-buffer.ts +292 -0
- package/src/server/sse-payload-rewrite.ts +25 -14
- package/src/server/ws-bridge.ts +27 -22
- package/src/service-manager-probe.ts +520 -10
- package/src/service.ts +134 -2
- package/src/storage/worker-lifecycle.ts +14 -14
- package/src/tray/windows-tray.ps1 +74 -9
- package/src/types.ts +68 -2
- package/src/update/index.ts +12 -0
- package/src/update/job.ts +392 -18
- package/src/update/npm-cache-preflight.d.mts +47 -0
- package/src/update/npm-cache-preflight.mjs +201 -0
- package/src/usage/log.ts +1 -1
- package/src/vision/index.ts +77 -2
- package/src/web-search/loop.ts +1 -1
- package/src/web-search/parse.ts +4 -1
- package/gui/dist/assets/index-BKVqyYqT.js +0 -70
- package/gui/dist/assets/index-Ca_3269W.css +0 -1
package/src/server/relay.ts
CHANGED
|
@@ -6,17 +6,21 @@ import {
|
|
|
6
6
|
addFinalRequestLog,
|
|
7
7
|
httpStatusForRequestLogTerminal,
|
|
8
8
|
inspectResponseLogJson,
|
|
9
|
-
inspectResponseLogSsePayload,
|
|
10
9
|
inspectResponseLogSsePayloadParsed,
|
|
11
10
|
recordFirstOutput,
|
|
12
11
|
type RequestLogContext,
|
|
13
12
|
type RequestLogEntry,
|
|
14
13
|
} from "./request-log";
|
|
14
|
+
import {
|
|
15
|
+
BoundedSseFrameBuffer,
|
|
16
|
+
joinSseFrameBytes,
|
|
17
|
+
MAX_CLIENT_SSE_FRAME_BYTES,
|
|
18
|
+
} from "./sse-frame-buffer";
|
|
15
19
|
|
|
16
20
|
const nativePassthroughSseResponses = new WeakSet<Response>();
|
|
17
21
|
const eagerRelaySseResponses = new WeakSet<Response>();
|
|
18
22
|
|
|
19
|
-
export const MAX_INSPECTION_SSE_FRAME_BYTES =
|
|
23
|
+
export const MAX_INSPECTION_SSE_FRAME_BYTES = MAX_CLIENT_SSE_FRAME_BYTES;
|
|
20
24
|
export const MAX_COMPLETED_OUTPUT_ITEMS = 256;
|
|
21
25
|
export const MAX_COMPLETED_OUTPUT_ITEM_SOURCE_BYTES = 8 * 1024 * 1024;
|
|
22
26
|
export const MAX_TAIL_ERROR_MESSAGE_CHARS = 512;
|
|
@@ -105,30 +109,32 @@ export type SseTerminalOutputBoundary = {
|
|
|
105
109
|
|
|
106
110
|
/**
|
|
107
111
|
* Frame-aware client output boundary shared by both native Responses relays.
|
|
108
|
-
* It buffers only the current incomplete SSE block
|
|
109
|
-
*
|
|
112
|
+
* It buffers only the current incomplete SSE block under the same hard byte
|
|
113
|
+
* cap as inspection, forwards complete blocks through the first Responses
|
|
114
|
+
* terminal, and drops every later block/byte.
|
|
110
115
|
*/
|
|
111
116
|
export function createSseTerminalOutputBoundary(): SseTerminalOutputBoundary {
|
|
112
|
-
|
|
113
|
-
const
|
|
114
|
-
let buffer = "";
|
|
117
|
+
const decoder = new TextDecoder();
|
|
118
|
+
const framer = new BoundedSseFrameBuffer(MAX_INSPECTION_SSE_FRAME_BYTES);
|
|
115
119
|
let terminal = false;
|
|
116
120
|
let done = false;
|
|
117
121
|
let disposed = false;
|
|
118
122
|
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
123
|
+
const processFrames = (
|
|
124
|
+
frames: ReturnType<BoundedSseFrameBuffer["feed"]>,
|
|
125
|
+
): Uint8Array => {
|
|
126
|
+
if (disposed || terminal || frames.length === 0) return new Uint8Array(0);
|
|
127
|
+
const output: Uint8Array[] = [];
|
|
122
128
|
let responsesTerminal = false;
|
|
123
|
-
for (
|
|
124
|
-
const
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
if (
|
|
129
|
+
for (const frame of frames) {
|
|
130
|
+
const payload = sseDataPayload(decoder.decode(frame.block));
|
|
131
|
+
const isDone = payload === "[DONE]";
|
|
132
|
+
// Preserve every frame through the first Responses terminal. A [DONE]
|
|
133
|
+
// frame is also preserved when it immediately follows that terminal in
|
|
134
|
+
// the same upstream chunk; every later non-DONE frame is dropped.
|
|
135
|
+
if (!responsesTerminal || isDone) output.push(frame.block, frame.delimiter);
|
|
136
|
+
if (isDone) {
|
|
130
137
|
done = true;
|
|
131
|
-
if (responsesTerminal) output += next.block + next.delimiter;
|
|
132
138
|
continue;
|
|
133
139
|
}
|
|
134
140
|
if (!responsesTerminal && payload && terminalStatusFromSsePayload(payload)) {
|
|
@@ -137,33 +143,26 @@ export function createSseTerminalOutputBoundary(): SseTerminalOutputBoundary {
|
|
|
137
143
|
}
|
|
138
144
|
if (responsesTerminal) {
|
|
139
145
|
terminal = true;
|
|
140
|
-
|
|
141
|
-
}
|
|
142
|
-
if (flush && !terminal && buffer.length > 0) {
|
|
143
|
-
output += buffer;
|
|
144
|
-
buffer = "";
|
|
146
|
+
framer.dispose();
|
|
145
147
|
}
|
|
146
|
-
return
|
|
148
|
+
return joinSseFrameBytes(output);
|
|
147
149
|
};
|
|
148
150
|
|
|
149
151
|
return {
|
|
150
152
|
feed(chunk) {
|
|
151
153
|
if (disposed || terminal) return new Uint8Array(0);
|
|
152
|
-
|
|
153
|
-
return process(false);
|
|
154
|
+
return processFrames(framer.feed(chunk));
|
|
154
155
|
},
|
|
155
156
|
finish() {
|
|
156
157
|
if (disposed || terminal) return new Uint8Array(0);
|
|
157
|
-
|
|
158
|
-
return process(true);
|
|
158
|
+
return framer.finish();
|
|
159
159
|
},
|
|
160
160
|
terminalSeen: () => terminal,
|
|
161
161
|
doneSeen: () => done,
|
|
162
162
|
dispose() {
|
|
163
163
|
if (disposed) return;
|
|
164
164
|
disposed = true;
|
|
165
|
-
|
|
166
|
-
buffer = "";
|
|
165
|
+
framer.dispose();
|
|
167
166
|
},
|
|
168
167
|
};
|
|
169
168
|
}
|
|
@@ -228,7 +227,14 @@ export function relaySseWithFailedTail(
|
|
|
228
227
|
if (result !== "buffered") return;
|
|
229
228
|
}
|
|
230
229
|
} catch (err) {
|
|
231
|
-
|
|
230
|
+
let partial: Uint8Array = new Uint8Array(0);
|
|
231
|
+
try {
|
|
232
|
+
partial = terminalBoundary.finish();
|
|
233
|
+
} catch {
|
|
234
|
+
// A near-cap ambiguous delimiter tail may itself overflow at EOF.
|
|
235
|
+
// Preserve the original read/framing failure and continue emitting
|
|
236
|
+
// the bounded failed tail instead of letting cleanup throw again.
|
|
237
|
+
}
|
|
232
238
|
terminalBoundary.dispose();
|
|
233
239
|
if (closed) return;
|
|
234
240
|
const payload = buildFailedTailPayload(err);
|
|
@@ -359,53 +365,42 @@ export function trackSseForRequestLog(
|
|
|
359
365
|
onFirstOutput?: () => void,
|
|
360
366
|
): ReadableStream<Uint8Array> {
|
|
361
367
|
const reader = body.getReader();
|
|
362
|
-
const decoder = new TextDecoder();
|
|
363
|
-
let buffer = "";
|
|
364
368
|
let terminalReported = false;
|
|
365
|
-
const reportFirstOutput = createFirstOutputReporter(onFirstOutput);
|
|
366
369
|
|
|
367
370
|
const reportTerminal = (status: ResponsesTerminalStatus) => {
|
|
368
371
|
if (terminalReported) return;
|
|
369
372
|
terminalReported = true;
|
|
370
373
|
onTerminal(status);
|
|
371
374
|
};
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
};
|
|
380
|
-
|
|
381
|
-
const inspectChunk = (value: Uint8Array) => {
|
|
382
|
-
buffer += decoder.decode(value, { stream: true });
|
|
383
|
-
let next: { block: string; rest: string } | null;
|
|
384
|
-
while ((next = nextSseBlock(buffer))) {
|
|
385
|
-
buffer = next.rest;
|
|
386
|
-
inspectPayload(sseDataPayload(next.block));
|
|
387
|
-
}
|
|
388
|
-
};
|
|
375
|
+
// Reuse the byte-bounded inspector so translated responses cannot retain an
|
|
376
|
+
// unterminated upstream frame or parse the same event once per observer.
|
|
377
|
+
const inspector = createSseInspector({
|
|
378
|
+
onTerminal: reportTerminal,
|
|
379
|
+
logCtx,
|
|
380
|
+
onFirstOutput,
|
|
381
|
+
});
|
|
389
382
|
|
|
390
383
|
return new ReadableStream<Uint8Array>({
|
|
391
384
|
async pull(controller) {
|
|
392
385
|
try {
|
|
393
386
|
const { done, value } = await reader.read();
|
|
394
387
|
if (done) {
|
|
395
|
-
|
|
396
|
-
if (buffer.trim()) inspectPayload(sseDataPayload(buffer));
|
|
388
|
+
inspector.finish();
|
|
397
389
|
if (!terminalReported) reportTerminal("incomplete");
|
|
390
|
+
inspector.dispose();
|
|
398
391
|
controller.close();
|
|
399
392
|
return;
|
|
400
393
|
}
|
|
401
|
-
|
|
394
|
+
inspector.feed(value);
|
|
402
395
|
controller.enqueue(value);
|
|
403
396
|
} catch (err) {
|
|
404
397
|
if (!terminalReported) reportTerminal("incomplete");
|
|
398
|
+
inspector.dispose();
|
|
405
399
|
try { controller.error(err); } catch { /* already torn down */ }
|
|
406
400
|
}
|
|
407
401
|
},
|
|
408
402
|
cancel(reason) {
|
|
403
|
+
inspector.dispose();
|
|
409
404
|
onCancel();
|
|
410
405
|
reader.cancel(reason).catch(() => {});
|
|
411
406
|
},
|
|
@@ -516,38 +511,23 @@ export function relaySseWithHeartbeat(
|
|
|
516
511
|
): ReadableStream<Uint8Array> | null {
|
|
517
512
|
if (!body) return null;
|
|
518
513
|
const reader = body.getReader();
|
|
519
|
-
const decoder = new TextDecoder();
|
|
520
514
|
const heartbeat = new TextEncoder().encode(": opencodex keepalive\n\n");
|
|
521
515
|
let timer: ReturnType<typeof setInterval> | undefined;
|
|
522
516
|
let closed = false;
|
|
523
517
|
let clientCancelled = false;
|
|
524
518
|
let terminalReported = false;
|
|
525
|
-
let buffer = "";
|
|
526
519
|
|
|
527
520
|
const reportTerminal = (status: ResponsesTerminalStatus) => {
|
|
528
521
|
if (terminalReported || clientCancelled || closed) return;
|
|
529
522
|
terminalReported = true;
|
|
530
523
|
onTerminal?.(status);
|
|
531
524
|
};
|
|
532
|
-
|
|
533
|
-
const inspectPayload = (payload: string | null) => {
|
|
534
|
-
if (!payload) return;
|
|
535
|
-
const status = terminalStatusFromSsePayload(payload);
|
|
536
|
-
if (status) reportTerminal(status);
|
|
537
|
-
};
|
|
538
|
-
|
|
539
|
-
const inspectChunk = (value: Uint8Array) => {
|
|
540
|
-
buffer += decoder.decode(value, { stream: true });
|
|
541
|
-
let next: { block: string; rest: string } | null;
|
|
542
|
-
while ((next = nextSseBlock(buffer))) {
|
|
543
|
-
buffer = next.rest;
|
|
544
|
-
inspectPayload(sseDataPayload(next.block));
|
|
545
|
-
}
|
|
546
|
-
};
|
|
525
|
+
const inspector = createSseInspector({ onTerminal: reportTerminal });
|
|
547
526
|
|
|
548
527
|
const cleanup = () => {
|
|
549
528
|
if (closed) return;
|
|
550
529
|
closed = true;
|
|
530
|
+
inspector.dispose();
|
|
551
531
|
if (timer) clearInterval(timer);
|
|
552
532
|
timer = undefined;
|
|
553
533
|
options?.onDone?.();
|
|
@@ -569,14 +549,13 @@ export function relaySseWithHeartbeat(
|
|
|
569
549
|
try {
|
|
570
550
|
const { done, value } = await reader.read();
|
|
571
551
|
if (done) {
|
|
572
|
-
|
|
573
|
-
if (buffer.trim()) inspectPayload(sseDataPayload(buffer));
|
|
552
|
+
inspector.finish();
|
|
574
553
|
if (!terminalReported && !clientCancelled) reportTerminal("incomplete");
|
|
575
554
|
cleanup();
|
|
576
555
|
controller.close();
|
|
577
556
|
return;
|
|
578
557
|
}
|
|
579
|
-
|
|
558
|
+
inspector.feed(value);
|
|
580
559
|
controller.enqueue(value);
|
|
581
560
|
} catch (err) {
|
|
582
561
|
if (!clientCancelled) reportTerminal("incomplete");
|
|
@@ -615,6 +594,12 @@ export type SseInspectorHandlers = {
|
|
|
615
594
|
logCtx?: RequestLogContext;
|
|
616
595
|
onCompletedResponse?: (response: { id?: unknown; output?: unknown; status?: unknown }) => void;
|
|
617
596
|
onFirstOutput?: () => void;
|
|
597
|
+
/**
|
|
598
|
+
* Provider-scoped compatibility: persist the completed snapshot under the
|
|
599
|
+
* first response id exposed to the client when an upstream changes ids
|
|
600
|
+
* between `response.created` and `response.completed`.
|
|
601
|
+
*/
|
|
602
|
+
pinCompletedResponseIdToFirstSeen?: boolean;
|
|
618
603
|
};
|
|
619
604
|
|
|
620
605
|
type CompletedOutputItem = { item: unknown; sourceBytes: number };
|
|
@@ -644,17 +629,6 @@ function delimiterLengthAt(
|
|
|
644
629
|
return byteAt(index + 3) === 10 ? 4 : 0;
|
|
645
630
|
}
|
|
646
631
|
|
|
647
|
-
function joinedBytes(slices: readonly Uint8Array[], byteLength: number): Uint8Array {
|
|
648
|
-
if (slices.length === 1 && slices[0]!.byteLength === byteLength) return slices[0]!;
|
|
649
|
-
const joined = new Uint8Array(byteLength);
|
|
650
|
-
let offset = 0;
|
|
651
|
-
for (const slice of slices) {
|
|
652
|
-
joined.set(slice, offset);
|
|
653
|
-
offset += slice.byteLength;
|
|
654
|
-
}
|
|
655
|
-
return joined;
|
|
656
|
-
}
|
|
657
|
-
|
|
658
632
|
/**
|
|
659
633
|
* Per-chunk SSE inspection state machine shared by consumeForInspection,
|
|
660
634
|
* consumeForResponseLogMetadata, and the eager bounded relay (relay-eager.ts).
|
|
@@ -675,8 +649,8 @@ export function createSseInspector(handlers: SseInspectorHandlers): SseInspector
|
|
|
675
649
|
let reported = false;
|
|
676
650
|
let sawTerminal = false;
|
|
677
651
|
let disposed = false;
|
|
678
|
-
let delimiterTail = new Uint8Array(0);
|
|
679
|
-
let
|
|
652
|
+
let delimiterTail: Uint8Array = new Uint8Array(0);
|
|
653
|
+
let candidate: Uint8Array = new Uint8Array(0);
|
|
680
654
|
let candidateBytes = 0;
|
|
681
655
|
let discardingOversizedFrame = false;
|
|
682
656
|
const reportFirstOutput = createFirstOutputReporter(handlers.onFirstOutput);
|
|
@@ -686,10 +660,11 @@ export function createSseInspector(handlers: SseInspectorHandlers): SseInspector
|
|
|
686
660
|
: null;
|
|
687
661
|
let aggregateItemBytes = 0;
|
|
688
662
|
let reconstructionTainted = false;
|
|
663
|
+
let firstResponseId: string | undefined;
|
|
689
664
|
|
|
690
665
|
const clearFrameState = (): void => {
|
|
691
666
|
delimiterTail = new Uint8Array(0);
|
|
692
|
-
|
|
667
|
+
candidate = new Uint8Array(0);
|
|
693
668
|
candidateBytes = 0;
|
|
694
669
|
discardingOversizedFrame = false;
|
|
695
670
|
};
|
|
@@ -706,6 +681,31 @@ export function createSseInspector(handlers: SseInspectorHandlers): SseInspector
|
|
|
706
681
|
decoder = null;
|
|
707
682
|
clearFrameState();
|
|
708
683
|
clearCompletedItems();
|
|
684
|
+
firstResponseId = undefined;
|
|
685
|
+
};
|
|
686
|
+
|
|
687
|
+
const ensureCandidateCapacity = (requiredBytes: number): void => {
|
|
688
|
+
if (candidate.byteLength >= requiredBytes) return;
|
|
689
|
+
let capacity = candidate.byteLength === 0
|
|
690
|
+
? Math.min(MAX_INSPECTION_SSE_FRAME_BYTES, Math.max(requiredBytes, 4096))
|
|
691
|
+
: candidate.byteLength;
|
|
692
|
+
while (capacity < requiredBytes) {
|
|
693
|
+
capacity = Math.min(
|
|
694
|
+
MAX_INSPECTION_SSE_FRAME_BYTES,
|
|
695
|
+
Math.max(requiredBytes, capacity * 2),
|
|
696
|
+
);
|
|
697
|
+
}
|
|
698
|
+
const grown = new Uint8Array(capacity);
|
|
699
|
+
if (candidateBytes > 0) grown.set(candidate.subarray(0, candidateBytes));
|
|
700
|
+
candidate = grown;
|
|
701
|
+
};
|
|
702
|
+
|
|
703
|
+
const takeCandidate = (): Uint8Array => {
|
|
704
|
+
if (candidateBytes === 0) return new Uint8Array(0);
|
|
705
|
+
const frame = candidate.slice(0, candidateBytes);
|
|
706
|
+
candidate = new Uint8Array(0);
|
|
707
|
+
candidateBytes = 0;
|
|
708
|
+
return frame;
|
|
709
709
|
};
|
|
710
710
|
|
|
711
711
|
const retainCandidateSlice = (slice: Uint8Array): void => {
|
|
@@ -716,7 +716,7 @@ export function createSseInspector(handlers: SseInspectorHandlers): SseInspector
|
|
|
716
716
|
Math.min(nextBytes, MAX_INSPECTION_SSE_FRAME_BYTES),
|
|
717
717
|
);
|
|
718
718
|
if (nextBytes > MAX_INSPECTION_SSE_FRAME_BYTES) {
|
|
719
|
-
|
|
719
|
+
candidate = new Uint8Array(0);
|
|
720
720
|
candidateBytes = 0;
|
|
721
721
|
discardingOversizedFrame = true;
|
|
722
722
|
inspectionCounters.frameCapOverflows += 1;
|
|
@@ -726,10 +726,8 @@ export function createSseInspector(handlers: SseInspectorHandlers): SseInspector
|
|
|
726
726
|
reconstructionTainted = true;
|
|
727
727
|
return;
|
|
728
728
|
}
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
// chunk whose preceding frames have already been consumed.
|
|
732
|
-
candidateSlices.push(slice.slice());
|
|
729
|
+
ensureCandidateCapacity(nextBytes);
|
|
730
|
+
candidate.set(slice, candidateBytes);
|
|
733
731
|
candidateBytes = nextBytes;
|
|
734
732
|
};
|
|
735
733
|
|
|
@@ -800,6 +798,17 @@ export function createSseInspector(handlers: SseInspectorHandlers): SseInspector
|
|
|
800
798
|
const parsedEvent = parsed && typeof parsed === "object" && !Array.isArray(parsed)
|
|
801
799
|
? parsed as ParsedSseEvent
|
|
802
800
|
: null;
|
|
801
|
+
const responseRecord = parsedEvent
|
|
802
|
+
&& typeof parsedEvent.response === "object"
|
|
803
|
+
&& parsedEvent.response !== null
|
|
804
|
+
&& !Array.isArray(parsedEvent.response)
|
|
805
|
+
? parsedEvent.response as { id?: unknown }
|
|
806
|
+
: null;
|
|
807
|
+
if (handlers.pinCompletedResponseIdToFirstSeen
|
|
808
|
+
&& responseRecord
|
|
809
|
+
&& typeof responseRecord.id === "string") {
|
|
810
|
+
firstResponseId ??= responseRecord.id;
|
|
811
|
+
}
|
|
803
812
|
const doneItem = parsedEvent?.type === "response.output_item.done" ? parsedEvent.item : undefined;
|
|
804
813
|
if (parsedEvent
|
|
805
814
|
&& doneItem !== undefined
|
|
@@ -814,6 +823,11 @@ export function createSseInspector(handlers: SseInspectorHandlers): SseInspector
|
|
|
814
823
|
|
|
815
824
|
let response = completedResponseFromParsedEvent(parsedEvent);
|
|
816
825
|
if (response) {
|
|
826
|
+
if (handlers.pinCompletedResponseIdToFirstSeen
|
|
827
|
+
&& firstResponseId !== undefined
|
|
828
|
+
&& response.id !== firstResponseId) {
|
|
829
|
+
response = { ...response, id: firstResponseId };
|
|
830
|
+
}
|
|
817
831
|
// Authoritative output is a NON-EMPTY ARRAY only. Anything else
|
|
818
832
|
// (missing, null, scalar, object) keeps the historical backfill
|
|
819
833
|
// behavior so a malformed terminal cannot reach rememberResponseState
|
|
@@ -849,9 +863,7 @@ export function createSseInspector(handlers: SseInspectorHandlers): SseInspector
|
|
|
849
863
|
return;
|
|
850
864
|
}
|
|
851
865
|
const sourceBytes = candidateBytes;
|
|
852
|
-
const frame =
|
|
853
|
-
candidateSlices = [];
|
|
854
|
-
candidateBytes = 0;
|
|
866
|
+
const frame = takeCandidate();
|
|
855
867
|
if (reported && !handlers.onCompletedResponse) return;
|
|
856
868
|
const decoded = decoder!.decode(frame);
|
|
857
869
|
scanPayload(sseDataPayload(decoded), sourceBytes);
|
|
@@ -908,7 +920,7 @@ export function createSseInspector(handlers: SseInspectorHandlers): SseInspector
|
|
|
908
920
|
delimiterTail = new Uint8Array(0);
|
|
909
921
|
if (!discardingOversizedFrame && candidateBytes > 0 && !reported) {
|
|
910
922
|
const sourceBytes = candidateBytes;
|
|
911
|
-
const decoded = decoder!.decode(
|
|
923
|
+
const decoded = decoder!.decode(takeCandidate());
|
|
912
924
|
scanPayload(decoded.trim() ? sseDataPayload(decoded) : null, sourceBytes);
|
|
913
925
|
}
|
|
914
926
|
} finally {
|
|
@@ -929,6 +941,8 @@ export type InspectionConsumerOptions = {
|
|
|
929
941
|
drainBounds?: Partial<InspectionDrainBounds>;
|
|
930
942
|
upstream?: AbortController;
|
|
931
943
|
now?: () => number;
|
|
944
|
+
/** Forward provider-scoped response-id pinning to the owned inspector. */
|
|
945
|
+
pinCompletedResponseIdToFirstSeen?: boolean;
|
|
932
946
|
/** Test seam for proving both public consumers dispose their owned inspector. */
|
|
933
947
|
inspectorFactory?: (handlers: SseInspectorHandlers) => SseInspector;
|
|
934
948
|
};
|
|
@@ -1085,6 +1099,7 @@ export function consumeForInspection(
|
|
|
1085
1099
|
logCtx,
|
|
1086
1100
|
onCompletedResponse,
|
|
1087
1101
|
onFirstOutput,
|
|
1102
|
+
pinCompletedResponseIdToFirstSeen: options?.pinCompletedResponseIdToFirstSeen,
|
|
1088
1103
|
});
|
|
1089
1104
|
startBoundedInspectionPump({
|
|
1090
1105
|
...options,
|
|
@@ -1130,6 +1145,7 @@ export function consumeForResponseLogMetadata(
|
|
|
1130
1145
|
logCtx,
|
|
1131
1146
|
onCompletedResponse,
|
|
1132
1147
|
onFirstOutput,
|
|
1148
|
+
pinCompletedResponseIdToFirstSeen: options?.pinCompletedResponseIdToFirstSeen,
|
|
1133
1149
|
});
|
|
1134
1150
|
startBoundedInspectionPump({ ...options, reader, inspector, signal, onDone });
|
|
1135
1151
|
}
|
|
@@ -68,6 +68,8 @@ export interface RequestLogContext {
|
|
|
68
68
|
modelSupportsServiceTier?: boolean;
|
|
69
69
|
responseServiceTier?: string;
|
|
70
70
|
resolvedModel?: string;
|
|
71
|
+
/** Internal: client-facing response metadata must not replace the physical routed model. */
|
|
72
|
+
preserveResolvedModelFromRoute?: boolean;
|
|
71
73
|
usage?: OcxUsage;
|
|
72
74
|
usageLogInputTokens?: number;
|
|
73
75
|
attempts?: PersistedUsageAttempt[];
|
|
@@ -252,7 +254,7 @@ export function requestLogEntryFromPersistedUsage(entry: PersistedUsageEntry): R
|
|
|
252
254
|
usageStatus: entry.usageStatus,
|
|
253
255
|
...(entry.usage ? { usage: entry.usage } : {}),
|
|
254
256
|
...(entry.totalTokens !== undefined ? { totalTokens: entry.totalTokens } : {}),
|
|
255
|
-
...(entry.attempts
|
|
257
|
+
...(entry.attempts !== undefined ? { attempts: entry.attempts } : {}),
|
|
256
258
|
...(routeDecision ? { routeDecision } : {}),
|
|
257
259
|
};
|
|
258
260
|
}
|
|
@@ -346,7 +348,7 @@ export function addRequestLog(entry: RequestLogEntry) {
|
|
|
346
348
|
usageStatus: entry.usageStatus,
|
|
347
349
|
...(entry.usage ? { usage: entry.usage } : {}),
|
|
348
350
|
...(entry.totalTokens !== undefined ? { totalTokens: entry.totalTokens } : {}),
|
|
349
|
-
...(entry.attempts
|
|
351
|
+
...(entry.attempts !== undefined ? { attempts: entry.attempts } : {}),
|
|
350
352
|
...failureDiagnostics,
|
|
351
353
|
...(entry.routeDecision ? { routeDecision: entry.routeDecision } : {}),
|
|
352
354
|
});
|
|
@@ -512,7 +514,11 @@ export function applyResponseLogMetadata(logCtx: RequestLogContext, payload: unk
|
|
|
512
514
|
: payload;
|
|
513
515
|
if (!source || typeof source !== "object") return;
|
|
514
516
|
const model = (source as { model?: unknown }).model;
|
|
515
|
-
if (
|
|
517
|
+
if (
|
|
518
|
+
!logCtx.preserveResolvedModelFromRoute
|
|
519
|
+
&& typeof model === "string"
|
|
520
|
+
&& model.trim()
|
|
521
|
+
) logCtx.resolvedModel = model;
|
|
516
522
|
const serviceTier = (source as { service_tier?: unknown }).service_tier;
|
|
517
523
|
if (typeof serviceTier === "string" && serviceTier.trim()) logCtx.responseServiceTier = serviceTier;
|
|
518
524
|
const usage = usageFromResponsesPayload((source as { usage?: unknown }).usage);
|
|
@@ -832,7 +838,7 @@ export function addFinalRequestLog(
|
|
|
832
838
|
usageStatus,
|
|
833
839
|
...(loggedUsage ? { usage: loggedUsage } : {}),
|
|
834
840
|
...(totalTokens !== undefined ? { totalTokens } : {}),
|
|
835
|
-
...(attempts
|
|
841
|
+
...(attempts !== undefined ? { attempts } : {}),
|
|
836
842
|
...(logCtx.affinity ? { affinity: logCtx.affinity } : {}),
|
|
837
843
|
...(logCtx.transportPhase ? { transportPhase: logCtx.transportPhase } : {}),
|
|
838
844
|
...(logCtx.terminalSource ? { terminalSource: logCtx.terminalSource } : {}),
|
|
@@ -70,7 +70,16 @@ import {
|
|
|
70
70
|
type UpstreamSendRecovery,
|
|
71
71
|
} from "../../lib/upstream-retry";
|
|
72
72
|
import { classifyTransportFailureKind, transportErrorCode } from "../../lib/upstream-reachability";
|
|
73
|
-
import {
|
|
73
|
+
import {
|
|
74
|
+
acquireUpstreamHostAdmission,
|
|
75
|
+
disableUpstreamHostCircuitForKey,
|
|
76
|
+
normalizeUpstreamHostCircuitThreshold,
|
|
77
|
+
recordUpstreamHostFailure,
|
|
78
|
+
releaseUpstreamHostAdmission,
|
|
79
|
+
resetUpstreamHostHealth,
|
|
80
|
+
upstreamHostHealthKey,
|
|
81
|
+
type UpstreamHostAdmissionLease,
|
|
82
|
+
} from "../../codex/upstream-host-health";
|
|
74
83
|
import { ForwardAdmissionCredentialError, validateForwardAdmissionCredential } from "../auth-cors";
|
|
75
84
|
import { listOpenAiForwardSidecarCandidates, resolveFirstUsableOpenAiSidecar, type ResolvedOpenAiForwardSidecar } from "../../providers/openai-sidecar";
|
|
76
85
|
import { isCanonicalOpenAiForwardProvider, supportsNativeResponsesCompactEndpoint } from "../../providers/openai-tiers";
|
|
@@ -112,7 +121,13 @@ import {
|
|
|
112
121
|
import { hasResponsesItemIdRepair, relaySseWithResponsesItemIdRepair } from "../responses-item-id-repair";
|
|
113
122
|
import type { EffectiveSubagentRoster, SpawnAgentSurface } from "../../codex/catalog";
|
|
114
123
|
|
|
115
|
-
import {
|
|
124
|
+
import {
|
|
125
|
+
decodeRequestErrorResponse,
|
|
126
|
+
handleResponses,
|
|
127
|
+
preAuthUpstreamHostCircuitKey,
|
|
128
|
+
upstreamHostCircuitOpenResponse,
|
|
129
|
+
usesCodexForwardPoolAuth,
|
|
130
|
+
} from "./core";
|
|
116
131
|
import { fetchWithHeaderTimeout, providerFetch, safeHostLabel, safeOriginLabel } from "./fetch-helpers";
|
|
117
132
|
|
|
118
133
|
export const COMPACT_RESPONSE_MAX_BYTES = 32 * 1024 * 1024;
|
|
@@ -312,12 +327,33 @@ export async function handleResponsesCompact(
|
|
|
312
327
|
// official OpenAI API. Any other Responses-shaped gateway must take the routed
|
|
313
328
|
// summarizer path below, or compaction fails against an endpoint it never had (#422).
|
|
314
329
|
if (supportsNativeResponsesCompactEndpoint(route.providerName, route.provider)) {
|
|
330
|
+
if (req.signal.aborted) {
|
|
331
|
+
return formatErrorResponse(499, "client_cancelled", "Client cancelled compact request");
|
|
332
|
+
}
|
|
333
|
+
// The enclosing native-compact guard already restricts this path to
|
|
334
|
+
// supported backends, so compact intentionally does not require the
|
|
335
|
+
// regular Responses adapter check here.
|
|
336
|
+
const preAuthCompactHostKey = preAuthUpstreamHostCircuitKey(route, config, {
|
|
337
|
+
requireResponsesAdapter: false,
|
|
338
|
+
});
|
|
339
|
+
let compactHostAdmissionLease: UpstreamHostAdmissionLease | null = null;
|
|
340
|
+
let authCtx: CodexAuthContext = { kind: "main", accountId: null };
|
|
341
|
+
if (preAuthCompactHostKey) {
|
|
342
|
+
const admission = acquireUpstreamHostAdmission(
|
|
343
|
+
preAuthCompactHostKey,
|
|
344
|
+
config.upstreamHostCircuitThreshold,
|
|
345
|
+
);
|
|
346
|
+
if (admission.kind === "blocked") {
|
|
347
|
+
return upstreamHostCircuitOpenResponse(admission.retryAfterSeconds);
|
|
348
|
+
}
|
|
349
|
+
compactHostAdmissionLease = admission.lease;
|
|
350
|
+
}
|
|
351
|
+
try {
|
|
315
352
|
// Native ChatGPT/OpenAI model: forward the compact request verbatim to the real backend.
|
|
316
353
|
// Resolve the SAME pool/thread auth context as /v1/responses — forwarding the caller's raw
|
|
317
354
|
// headers would run compaction on the wrong account (or 401) whenever a pool account is
|
|
318
355
|
// active for this thread while normal turns succeed.
|
|
319
356
|
let compactProvider = route.provider;
|
|
320
|
-
let authCtx: CodexAuthContext = { kind: "main", accountId: null };
|
|
321
357
|
const headers = new Headers({ "content-type": "application/json" });
|
|
322
358
|
try {
|
|
323
359
|
if (route.codexAccountMode) {
|
|
@@ -364,6 +400,45 @@ export async function handleResponsesCompact(
|
|
|
364
400
|
// so routed-model reasoning items (reasoning_text content) don't 400 the ChatGPT backend.
|
|
365
401
|
const compactBody = sanitizeReasoningInputContent(compactBodyRaw) as typeof compactBodyRaw;
|
|
366
402
|
const compactUrl = `${base}/responses/compact`;
|
|
403
|
+
const actualCompactHostKey = upstreamHostHealthKey(
|
|
404
|
+
route.providerName,
|
|
405
|
+
safeOriginLabel(compactUrl),
|
|
406
|
+
);
|
|
407
|
+
const compactHostKey = compactProvider.authMode === "forward"
|
|
408
|
+
? actualCompactHostKey
|
|
409
|
+
: null;
|
|
410
|
+
const compactHostCircuitEnabled = compactHostKey !== null
|
|
411
|
+
&& normalizeUpstreamHostCircuitThreshold(config.upstreamHostCircuitThreshold) > 0;
|
|
412
|
+
if (compactHostKey !== null && !compactHostCircuitEnabled) {
|
|
413
|
+
disableUpstreamHostCircuitForKey(actualCompactHostKey);
|
|
414
|
+
}
|
|
415
|
+
if (compactHostAdmissionLease && compactHostAdmissionLease.key !== compactHostKey) {
|
|
416
|
+
releaseCodexAuthContextProbeLease(authCtx);
|
|
417
|
+
return formatErrorResponse(502, "upstream_error", "Provider host changed after circuit admission");
|
|
418
|
+
}
|
|
419
|
+
if (req.signal.aborted) {
|
|
420
|
+
releaseCodexAuthContextProbeLease(authCtx);
|
|
421
|
+
return formatErrorResponse(499, "client_cancelled", "Client cancelled compact request");
|
|
422
|
+
}
|
|
423
|
+
if (!compactHostAdmissionLease && compactHostCircuitEnabled) {
|
|
424
|
+
const admission = acquireUpstreamHostAdmission(
|
|
425
|
+
compactHostKey!,
|
|
426
|
+
config.upstreamHostCircuitThreshold,
|
|
427
|
+
);
|
|
428
|
+
if (admission.kind === "blocked") {
|
|
429
|
+
releaseCodexAuthContextProbeLease(authCtx);
|
|
430
|
+
return upstreamHostCircuitOpenResponse(admission.retryAfterSeconds);
|
|
431
|
+
}
|
|
432
|
+
compactHostAdmissionLease = admission.lease;
|
|
433
|
+
}
|
|
434
|
+
const settleObservedCompactHostResponse = (): void => {
|
|
435
|
+
if (compactHostCircuitEnabled) {
|
|
436
|
+
resetUpstreamHostHealth(actualCompactHostKey, compactHostAdmissionLease);
|
|
437
|
+
} else {
|
|
438
|
+
resetUpstreamHostHealth(actualCompactHostKey);
|
|
439
|
+
}
|
|
440
|
+
compactHostAdmissionLease = null;
|
|
441
|
+
};
|
|
367
442
|
const compactThreadId = req.headers.get("x-codex-parent-thread-id");
|
|
368
443
|
const connectMs = config.connectTimeoutMs ?? 200_000;
|
|
369
444
|
// Takes its context explicitly: the alternate-account flow below records a rejection
|
|
@@ -418,7 +493,7 @@ export async function handleResponsesCompact(
|
|
|
418
493
|
).then(res => {
|
|
419
494
|
// Every real attempt response — including an intermediate 5xx the retry
|
|
420
495
|
// wrapper replaces — proves the host was reached (#914 review).
|
|
421
|
-
|
|
496
|
+
settleObservedCompactHostResponse();
|
|
422
497
|
return res;
|
|
423
498
|
});
|
|
424
499
|
return recovery === "single"
|
|
@@ -442,11 +517,19 @@ export async function handleResponsesCompact(
|
|
|
442
517
|
const outcome = classifyTransportFailureKind(err);
|
|
443
518
|
// Host-level evidence stands regardless of pool membership (#914 review).
|
|
444
519
|
if (outcome === "connect_neutral") {
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
520
|
+
if (compactHostCircuitEnabled) {
|
|
521
|
+
recordUpstreamHostFailure(actualCompactHostKey, {
|
|
522
|
+
code: transportErrorCode(err),
|
|
523
|
+
threshold: config.upstreamHostCircuitThreshold,
|
|
524
|
+
lease: compactHostAdmissionLease,
|
|
525
|
+
});
|
|
526
|
+
} else {
|
|
527
|
+
recordUpstreamHostFailure(actualCompactHostKey, { code: transportErrorCode(err) });
|
|
528
|
+
}
|
|
529
|
+
} else {
|
|
530
|
+
releaseUpstreamHostAdmission(compactHostAdmissionLease);
|
|
449
531
|
}
|
|
532
|
+
compactHostAdmissionLease = null;
|
|
450
533
|
recordCompactPoolOutcome(outcomeCtx, outcome);
|
|
451
534
|
return formatErrorResponse(502, "upstream_error", "Failed to connect to compact upstream");
|
|
452
535
|
}
|
|
@@ -517,11 +600,19 @@ export async function handleResponsesCompact(
|
|
|
517
600
|
const outcome = classifyTransportFailureKind(err);
|
|
518
601
|
// Host-level evidence stands regardless of pool membership (#914 review).
|
|
519
602
|
if (outcome === "connect_neutral") {
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
603
|
+
if (compactHostCircuitEnabled) {
|
|
604
|
+
recordUpstreamHostFailure(actualCompactHostKey, {
|
|
605
|
+
code: transportErrorCode(err),
|
|
606
|
+
threshold: config.upstreamHostCircuitThreshold,
|
|
607
|
+
lease: compactHostAdmissionLease,
|
|
608
|
+
});
|
|
609
|
+
} else {
|
|
610
|
+
recordUpstreamHostFailure(actualCompactHostKey, { code: transportErrorCode(err) });
|
|
611
|
+
}
|
|
612
|
+
} else {
|
|
613
|
+
releaseUpstreamHostAdmission(compactHostAdmissionLease);
|
|
524
614
|
}
|
|
615
|
+
compactHostAdmissionLease = null;
|
|
525
616
|
recordCompactPoolOutcome(outcomeCtx, outcome);
|
|
526
617
|
return formatErrorResponse(502, "upstream_error", "Failed to connect to compact upstream");
|
|
527
618
|
}
|
|
@@ -548,6 +639,10 @@ export async function handleResponsesCompact(
|
|
|
548
639
|
// synthetic buffer errors are not upstream bodies and stay uninspected.
|
|
549
640
|
if (buffered.ok) inspectResponseLogJson(logCtx, await buffered.clone().text());
|
|
550
641
|
return buffered;
|
|
642
|
+
} finally {
|
|
643
|
+
releaseUpstreamHostAdmission(compactHostAdmissionLease);
|
|
644
|
+
releaseCodexAuthContextProbeLease(authCtx);
|
|
645
|
+
}
|
|
551
646
|
}
|
|
552
647
|
|
|
553
648
|
// ROUTED model: run the v2 synthetic-compaction turn internally (appends COMPACT_PROMPT, no
|