@bitkyc08/opencodex 2.52.0 → 2.53.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/gui/dist/assets/index-BBOZWGB6.css +1 -0
- package/gui/dist/assets/index-BlO4Yl6q.js +128 -0
- package/gui/dist/index.html +2 -2
- package/native/remote-workspace-helper/Cargo.lock +130 -0
- package/native/remote-workspace-helper/Cargo.toml +24 -0
- package/native/remote-workspace-helper/src/main.rs +49 -0
- package/native/remote-workspace-helper/src/protocol.rs +246 -0
- package/native/remote-workspace-helper/src/sandbox/macos.rs +19 -0
- package/native/remote-workspace-helper/src/sandbox/mod.rs +77 -0
- package/native/remote-workspace-helper/src/sandbox/windows.rs +15 -0
- package/package.json +6 -1
- package/src/adapters/anthropic-image-normalize.ts +30 -2
- package/src/adapters/anthropic.ts +1 -1
- package/src/adapters/base.ts +8 -2
- package/src/adapters/cursor/cursor-errors.ts +12 -0
- package/src/adapters/cursor/thread-continuity.ts +93 -0
- package/src/adapters/cursor.ts +104 -73
- package/src/adapters/devin/cloud-direct/chat.ts +312 -23
- package/src/adapters/devin/cloud-direct/metadata.ts +31 -2
- package/src/adapters/devin/live-models.ts +70 -3
- package/src/adapters/devin.ts +281 -21
- package/src/adapters/google-wire-compiler.ts +14 -6
- package/src/adapters/google.ts +22 -8
- package/src/adapters/kiro/adapter.ts +316 -0
- package/src/adapters/kiro/conversation.ts +136 -0
- package/src/adapters/kiro/payload.ts +432 -0
- package/src/adapters/kiro/reasoning.ts +56 -0
- package/src/adapters/kiro/stream.ts +1153 -0
- package/src/adapters/kiro/usage.ts +223 -0
- package/src/adapters/kiro/wire.ts +76 -0
- package/src/adapters/kiro.ts +8 -2319
- package/src/adapters/mimo-free.ts +1 -1
- package/src/adapters/openai-chat-images.ts +101 -0
- package/src/adapters/openai-chat.ts +201 -181
- package/src/adapters/openai-responses.ts +92 -224
- package/src/adapters/registry.ts +0 -7
- package/src/adapters/run-turn-queue.ts +13 -6
- package/src/bridge.ts +14 -15
- package/src/chat/inbound.ts +29 -4
- package/src/chat/outbound.ts +145 -107
- package/src/claude/desktop-profile.ts +4 -6
- package/src/cli/account-api.ts +14 -0
- package/src/cli/account-extended.ts +1 -1
- package/src/cli/account-history.ts +60 -0
- package/src/cli/account-main.ts +80 -0
- package/src/cli/account.ts +11 -3
- package/src/cli/capabilities.ts +113 -0
- package/src/cli/catalog.ts +109 -0
- package/src/cli/dispatch.ts +9 -0
- package/src/cli/help.ts +2 -0
- package/src/cli/index.ts +2 -2
- package/src/cli/observe.ts +28 -1
- package/src/cli/opencode.ts +42 -8
- package/src/cli/provider-runtime.ts +11 -1
- package/src/cli/provider.ts +22 -2
- package/src/cli/registry.ts +21 -0
- package/src/cli/remote-workspace.ts +154 -0
- package/src/cli/status.ts +39 -7
- package/src/cli/usage-report.ts +14 -2
- package/src/client/hub-client.ts +34 -0
- package/src/client/hub-state.ts +9 -1
- package/src/codex/account-store.ts +78 -0
- package/src/codex/auth-api.ts +81 -54
- package/src/codex/auth-context.ts +45 -16
- package/src/codex/catalog/effort.ts +1 -1
- package/src/codex/catalog/metadata.ts +3 -6
- package/src/codex/catalog/native-models.ts +4 -4
- package/src/codex/catalog/parsing.ts +2 -20
- package/src/codex/catalog/provider-fetch.ts +10 -1
- package/src/codex/catalog/remote.ts +233 -0
- package/src/codex/catalog/sync.ts +403 -35
- package/src/codex/convergence.ts +1 -1
- package/src/codex/history-manifest.ts +36 -0
- package/src/codex/history-provider.ts +32 -5
- package/src/codex/inject.ts +9 -0
- package/src/codex/main-account.ts +113 -0
- package/src/codex/main-device-reauth-api.ts +89 -0
- package/src/codex/main-device-reauth.ts +217 -0
- package/src/codex/native-residue.ts +9 -2
- package/src/codex/quota-auto-refresh.ts +3 -2
- package/src/codex/quota-capacity.ts +98 -0
- package/src/codex/quota-history.ts +160 -0
- package/src/codex/quota-types.ts +8 -0
- package/src/codex/quota.ts +118 -91
- package/src/codex/refresh.ts +2 -1
- package/src/codex/routing.ts +90 -17
- package/src/codex/sync.ts +33 -4
- package/src/combos/request.ts +19 -1
- package/src/config/multi-agent-surface.ts +61 -0
- package/src/config/provider-validation.ts +176 -0
- package/src/config.ts +213 -11
- package/src/generated/compatibility-version.json +436 -168
- package/src/images/loop.ts +119 -36
- package/src/lib/admission.ts +12 -6
- package/src/lib/redact.ts +7 -0
- package/src/lib/translator-budget.ts +4 -3
- package/src/lib/windows-atomic-replace.ts +1 -0
- package/src/lib/windows-elevation.ts +1 -1
- package/src/oauth/chatgpt-device.ts +62 -5
- package/src/oauth/devin/cli-import.ts +130 -0
- package/src/oauth/devin.ts +63 -8
- package/src/oauth/index.ts +29 -14
- package/src/oauth/kiro.ts +18 -6
- package/src/oauth/login-cli.ts +9 -1
- package/src/oauth/meta-muse-device.ts +464 -0
- package/src/oauth/meta-muse.ts +123 -32
- package/src/oauth/pool-kernel.ts +9 -0
- package/src/oauth/pool-settings-capability.ts +2 -2
- package/src/oauth/store.ts +57 -0
- package/src/oauth/types.ts +31 -0
- package/src/providers/derive.ts +13 -3
- package/src/providers/devin-cli-authmode-migration.ts +57 -35
- package/src/providers/devin-provider-merge-migration.ts +240 -0
- package/src/providers/muse-key-quota.ts +117 -0
- package/src/providers/muse-subscription-usage.ts +14 -2
- package/src/providers/openai-sidecar.ts +25 -3
- package/src/providers/opencode-zen-rate-limit.ts +58 -0
- package/src/providers/provider-id-rewrite.ts +20 -5
- package/src/providers/quota-types.ts +12 -0
- package/src/providers/quota.ts +143 -102
- package/src/providers/reasoning-metadata.ts +543 -0
- package/src/providers/registry.ts +80 -49
- package/src/reasoning-effort.ts +26 -2
- package/src/remote/hub-usage.ts +32 -0
- package/src/remote-control/index.ts +192 -41
- package/src/remote-control/workspace-activation.ts +9 -0
- package/src/remote-control/workspace-agent-connection.ts +366 -0
- package/src/remote-control/workspace-claude-runtime.ts +243 -0
- package/src/remote-control/workspace-codex-runtime.ts +531 -0
- package/src/remote-control/workspace-codex-sandbox.ts +115 -0
- package/src/remote-control/workspace-command-runner.ts +748 -0
- package/src/remote-control/workspace-coordinator.ts +231 -0
- package/src/remote-control/workspace-device.ts +585 -0
- package/src/remote-control/workspace-executable.ts +43 -0
- package/src/remote-control/workspace-executor.ts +397 -0
- package/src/remote-control/workspace-hub.ts +519 -0
- package/src/remote-control/workspace-pi-runtime.ts +382 -0
- package/src/remote-control/workspace-process.ts +129 -0
- package/src/remote-control/workspace-rpc.ts +304 -0
- package/src/remote-control/workspace-runtime.ts +60 -0
- package/src/remote-control/workspace-secret-store.ts +39 -0
- package/src/remote-control/workspace-sessions.ts +799 -0
- package/src/remote-control/workspace-tool-bridge.ts +192 -0
- package/src/responses/code-mode-helper-compat.ts +22 -3
- package/src/responses/hosted-tool-policy.ts +0 -1
- package/src/responses/muse-tool-name-alias.ts +379 -0
- package/src/responses/plaintext-v2-agent-messages.ts +902 -0
- package/src/router.ts +7 -0
- package/src/routing/compatibility/behavior.ts +0 -1
- package/src/server/audio-client.ts +64 -0
- package/src/server/audio-dictation.ts +91 -0
- package/src/server/audio-live.ts +185 -0
- package/src/server/audio-transcriptions.ts +183 -0
- package/src/server/audio-upstream.ts +153 -0
- package/src/server/auth-cors.ts +61 -2
- package/src/server/chat-completions.ts +1 -1
- package/src/server/chat-native-sse.ts +92 -48
- package/src/server/chat-native.ts +37 -15
- package/src/server/hub-usage.ts +57 -0
- package/src/server/images.ts +4 -0
- package/src/server/index.ts +722 -57
- package/src/server/lifecycle.ts +5 -6
- package/src/server/live-call-bindings.ts +60 -0
- package/src/server/live.ts +12 -1
- package/src/server/management/agent-settings-routes.ts +25 -4
- package/src/server/management/api-access.ts +37 -0
- package/src/server/management/api-key-usage.ts +7 -2
- package/src/server/management/config-routes.ts +1 -18
- package/src/server/management/context.ts +15 -0
- package/src/server/management/logs-usage-routes.ts +2 -0
- package/src/server/management/oauth-account-routes.ts +39 -12
- package/src/server/management/provider-routes.ts +125 -2
- package/src/server/management/remote-workspace-routes.ts +140 -0
- package/src/server/management/route-registry.ts +15 -0
- package/src/server/management/usage-aggregate-cache.ts +14 -15
- package/src/server/management/usage-summary-cache.ts +2 -0
- package/src/server/management-api.ts +23 -0
- package/src/server/ports.ts +17 -0
- package/src/server/relay-eager.ts +4 -1
- package/src/server/relay.ts +70 -10
- package/src/server/request-decompress.ts +6 -3
- package/src/server/responses/agent-task-recovery.ts +25 -32
- package/src/server/responses/codex-auth-error.ts +11 -0
- package/src/server/responses/codex-ws-exchange.ts +52 -3
- package/src/server/responses/codex-ws-wire.ts +55 -0
- package/src/server/responses/compact.ts +9 -1
- package/src/server/responses/core.ts +337 -73
- package/src/server/responses/encrypted-payload.ts +45 -2
- package/src/server/responses/ws-upstream.ts +4 -1
- package/src/server/responses-self-named-namespace-scrub.ts +1 -3
- package/src/server/responses-undeclared-tool-guard.ts +1 -1
- package/src/server/search.ts +3 -0
- package/src/server/sse-payload-rewrite.ts +136 -51
- package/src/server/ws-bridge.ts +35 -1
- package/src/service/cli.ts +372 -0
- package/src/service/diagnostics.ts +340 -0
- package/src/service/guards.ts +303 -0
- package/src/service/health.ts +222 -0
- package/src/service/launchd.ts +853 -0
- package/src/service/orchestration.ts +617 -0
- package/src/service/repair.ts +334 -0
- package/src/service/state.ts +363 -0
- package/src/service/systemd.ts +229 -0
- package/src/service/windows-ops.ts +690 -0
- package/src/service/windows-scheduler.ts +769 -0
- package/src/service/windows-taskxml.ts +613 -0
- package/src/service.ts +22 -5550
- package/src/storage/cleanup/db.ts +258 -0
- package/src/storage/cleanup/execute.ts +358 -0
- package/src/storage/cleanup/paths.ts +189 -0
- package/src/storage/cleanup/pending.ts +140 -0
- package/src/storage/cleanup/preview.ts +292 -0
- package/src/storage/cleanup/reconcile.ts +347 -0
- package/src/storage/cleanup/restore.ts +932 -0
- package/src/storage/cleanup/satellite.ts +474 -0
- package/src/storage/cleanup/staging.ts +129 -0
- package/src/storage/cleanup/types.ts +98 -0
- package/src/storage/cleanup.ts +49 -3127
- package/src/types/accounts.ts +2 -0
- package/src/types/config.ts +13 -12
- package/src/types/provider.ts +37 -0
- package/src/types/request.ts +2 -0
- package/src/types/tools.ts +17 -5
- package/src/types.ts +1 -0
- package/src/usage/expected-prices.ts +127 -0
- package/src/usage/log.ts +58 -1
- package/src/vision/eligibility.ts +13 -2
- package/src/web-search/loop.ts +56 -3
- package/gui/dist/assets/index-CWXut3rG.js +0 -115
- package/gui/dist/assets/index-EdoPnm9_.css +0 -1
- package/src/adapters/devin-cli/acp.ts +0 -204
- package/src/adapters/devin-cli/adapter.ts +0 -345
- package/src/adapters/devin-cli/binary.ts +0 -69
- package/src/adapters/devin-cli/models.ts +0 -57
- package/src/oauth/devin-cli.ts +0 -149
- package/src/server/responses-reasoning-summary-rewrite.ts +0 -178
package/src/server/relay.ts
CHANGED
|
@@ -183,7 +183,10 @@ export type SseTerminalOutputBoundary = {
|
|
|
183
183
|
* terminal, and drops every later block/byte. A premature [DONE] is held until
|
|
184
184
|
* a terminal arrives so clean EOF can synthesize one terminal and one sentinel.
|
|
185
185
|
*/
|
|
186
|
-
export function createSseTerminalOutputBoundary(
|
|
186
|
+
export function createSseTerminalOutputBoundary(
|
|
187
|
+
options?: CodexSafetyBufferingFilterOptions,
|
|
188
|
+
): SseTerminalOutputBoundary {
|
|
189
|
+
const dropSafetyBuffering = options?.dropCodexSafetyBuffering === true;
|
|
187
190
|
const decoder = new TextDecoder();
|
|
188
191
|
const encoder = new TextEncoder();
|
|
189
192
|
const framer = new BoundedSseFrameBuffer(MAX_INSPECTION_SSE_FRAME_BYTES);
|
|
@@ -207,15 +210,22 @@ export function createSseTerminalOutputBoundary(): SseTerminalOutputBoundary {
|
|
|
207
210
|
// behind EOF, so its log context cannot determine the outgoing terminal.
|
|
208
211
|
const message = boundedBareUpstreamErrorMessage(parsed);
|
|
209
212
|
if (message !== undefined) upstreamError = message;
|
|
213
|
+
const safetyBuffering = dropSafetyBuffering && parsed !== undefined
|
|
214
|
+
? codexSafetyBufferingBlockAction(parsed) : "keep";
|
|
215
|
+
if (safetyBuffering === "drop") continue;
|
|
210
216
|
const policyError = parsed !== undefined && isPolicyRewriteType(parsed)
|
|
211
217
|
? cyberPolicyTerminalError(parsed)
|
|
212
218
|
: undefined;
|
|
213
|
-
const
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
policyFailurePayload(policyError, parsed),
|
|
217
|
-
))
|
|
219
|
+
const policyPayload = policyError ? policyFailurePayload(policyError, parsed) : undefined;
|
|
220
|
+
let outboundBlock = policyPayload !== undefined
|
|
221
|
+
? encoder.encode(rewritePolicyTerminalBlock(decoder.decode(frame.block), policyPayload))
|
|
218
222
|
: frame.block;
|
|
223
|
+
if (safetyBuffering === "strip") {
|
|
224
|
+
outboundBlock = encoder.encode(stripCodexSafetyBufferingField(
|
|
225
|
+
decoder.decode(outboundBlock),
|
|
226
|
+
policyPayload !== undefined ? parseSsePayload(policyPayload) : parsed,
|
|
227
|
+
));
|
|
228
|
+
}
|
|
219
229
|
if (isDone) {
|
|
220
230
|
done = true;
|
|
221
231
|
if (responsesTerminal) {
|
|
@@ -287,11 +297,11 @@ export function relaySseWithFailedTail(
|
|
|
287
297
|
body: ReadableStream<Uint8Array>,
|
|
288
298
|
upstream: AbortController,
|
|
289
299
|
onClientGone?: (reason?: unknown) => void,
|
|
290
|
-
opts?: { upstreamError?: string },
|
|
300
|
+
opts?: { upstreamError?: string; terminalBoundary?: CodexSafetyBufferingFilterOptions },
|
|
291
301
|
): ReadableStream<Uint8Array> {
|
|
292
302
|
const reader = body.getReader();
|
|
293
303
|
const encoder = new TextEncoder();
|
|
294
|
-
const terminalBoundary = createSseTerminalOutputBoundary();
|
|
304
|
+
const terminalBoundary = createSseTerminalOutputBoundary(opts?.terminalBoundary);
|
|
295
305
|
let closed = false;
|
|
296
306
|
const relayChunk = (
|
|
297
307
|
controller: ReadableStreamDefaultController<Uint8Array>,
|
|
@@ -468,6 +478,29 @@ function isPolicyRewriteType(parsed: unknown): boolean {
|
|
|
468
478
|
return type === "response.failed" || type === "response.incomplete" || type === "error";
|
|
469
479
|
}
|
|
470
480
|
|
|
481
|
+
/**
|
|
482
|
+
* Codex emits its safety-buffering hint in the SSE body as well as in headers:
|
|
483
|
+
* a `response.metadata` event whose `metadata.type` is `safety_buffering`, or a
|
|
484
|
+
* `safety_buffering` field on another event. The metadata event is dropped whole;
|
|
485
|
+
* the field is stripped so the carrying event is otherwise relayed unchanged.
|
|
486
|
+
*/
|
|
487
|
+
function codexSafetyBufferingBlockAction(parsed: unknown): "keep" | "drop" | "strip" {
|
|
488
|
+
const root = asJsonRecord(parsed);
|
|
489
|
+
if (!root) return "keep";
|
|
490
|
+
if (root.type === "response.metadata") {
|
|
491
|
+
const metadata = asJsonRecord(root.metadata);
|
|
492
|
+
if (metadata?.type === "safety_buffering") return "drop";
|
|
493
|
+
}
|
|
494
|
+
return Object.hasOwn(root, "safety_buffering") ? "strip" : "keep";
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function stripCodexSafetyBufferingField(block: string, parsed: unknown): string {
|
|
498
|
+
const root = asJsonRecord(parsed);
|
|
499
|
+
if (!root) return block;
|
|
500
|
+
const { safety_buffering: _safetyBuffering, ...rest } = root;
|
|
501
|
+
return replaceSseDataPayload(block, JSON.stringify(rest));
|
|
502
|
+
}
|
|
503
|
+
|
|
471
504
|
function rewritePolicyTerminalBlock(block: string, payload: string): string {
|
|
472
505
|
const newline = block.includes("\r\n") ? "\r\n" : "\n";
|
|
473
506
|
const rewritten = replaceSseDataPayload(block, payload);
|
|
@@ -1461,7 +1494,31 @@ export function consumeForResponseLogMetadata(
|
|
|
1461
1494
|
* body makes the caller (Codex) double-decode / truncate → "stream error" on every gpt passthrough.
|
|
1462
1495
|
* Drop encoding + hop-by-hop headers; relay everything else (content-type, etc.) verbatim.
|
|
1463
1496
|
*/
|
|
1464
|
-
export
|
|
1497
|
+
export const CODEX_SAFETY_BUFFERING_HEADERS = [
|
|
1498
|
+
"x-codex-safety-buffering-enabled",
|
|
1499
|
+
"x-codex-safety-buffering-faster-model",
|
|
1500
|
+
] as const;
|
|
1501
|
+
|
|
1502
|
+
const CODEX_SAFETY_BUFFERING_HEADER_SET: ReadonlySet<string> = new Set(CODEX_SAFETY_BUFFERING_HEADERS);
|
|
1503
|
+
|
|
1504
|
+
export interface CodexSafetyBufferingFilterOptions {
|
|
1505
|
+
/**
|
|
1506
|
+
* Drop Codex safety-buffering hints: the `x-codex-safety-buffering-*` response
|
|
1507
|
+
* headers and the `safety_buffering` SSE metadata event / field. Absent and
|
|
1508
|
+
* `false` relay everything unchanged.
|
|
1509
|
+
*/
|
|
1510
|
+
dropCodexSafetyBuffering?: boolean;
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
/** Resolve the passthrough header policy from the loaded config (absent means "forward everything"). */
|
|
1514
|
+
export function codexSafetyBufferingFilterOptions(
|
|
1515
|
+
config: { dropCodexSafetyBuffering?: boolean },
|
|
1516
|
+
): CodexSafetyBufferingFilterOptions {
|
|
1517
|
+
return { dropCodexSafetyBuffering: config.dropCodexSafetyBuffering === true };
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
export function sanitizePassthroughHeaders(upstream: Headers, options?: CodexSafetyBufferingFilterOptions): Headers {
|
|
1521
|
+
const dropSafetyBuffering = options?.dropCodexSafetyBuffering === true;
|
|
1465
1522
|
const DROP = new Set([
|
|
1466
1523
|
"content-encoding",
|
|
1467
1524
|
"content-length",
|
|
@@ -1478,7 +1535,10 @@ export function sanitizePassthroughHeaders(upstream: Headers): Headers {
|
|
|
1478
1535
|
]);
|
|
1479
1536
|
const out = new Headers();
|
|
1480
1537
|
upstream.forEach((value, key) => {
|
|
1481
|
-
|
|
1538
|
+
const lower = key.toLowerCase();
|
|
1539
|
+
if (DROP.has(lower)) return;
|
|
1540
|
+
if (dropSafetyBuffering && CODEX_SAFETY_BUFFERING_HEADER_SET.has(lower)) return;
|
|
1541
|
+
out.set(key, value);
|
|
1482
1542
|
});
|
|
1483
1543
|
return out;
|
|
1484
1544
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Buffer } from "node:buffer";
|
|
1
2
|
import { gunzipSync, inflateRawSync, inflateSync, zstdDecompressSync } from "node:zlib";
|
|
2
3
|
import type { TranslatorBudget } from "../lib/translator-budget";
|
|
3
4
|
|
|
@@ -29,7 +30,7 @@ export const MAX_DECOMPRESSED_BODY_BYTES = 256 * 1024 * 1024;
|
|
|
29
30
|
* shrink and is stuck. An UNBOUNDED inbound cap is not an acceptable answer: this admission
|
|
30
31
|
* limit is the only thing standing between one request and the process heap, and
|
|
31
32
|
* `readBoundedJsonRequestBody` materializes the body several times over (retained wire bytes,
|
|
32
|
-
* decoded bytes, the decoded string, the
|
|
33
|
+
* decoded bytes, the decoded string, the serialized measurement string, and the parsed object
|
|
33
34
|
* graph), so peak RSS is a MULTIPLE of whatever is admitted here. 512 MiB is the largest value
|
|
34
35
|
* that keeps that multiple survivable on an ordinary machine, and it is what #3573 asked for.
|
|
35
36
|
*/
|
|
@@ -324,12 +325,14 @@ export async function readBoundedJsonRequestBody(
|
|
|
324
325
|
const decoded = decodeRequestBody(raw, encoding, maxBytes);
|
|
325
326
|
releaseDecoded = decoded === raw ? undefined : budget?.observeAcceptedRequestCopy(decoded.byteLength);
|
|
326
327
|
const text = new TextDecoder().decode(decoded);
|
|
327
|
-
|
|
328
|
+
// Count UTF-8 without allocating another request-sized byte array for diagnostics.
|
|
329
|
+
releaseText = budget?.observeAcceptedRequestCopy(Buffer.byteLength(text, "utf8"));
|
|
328
330
|
if (options && "emptyBodyFallback" in options && text.trim() === "") {
|
|
329
331
|
return options.emptyBodyFallback;
|
|
330
332
|
}
|
|
331
333
|
const parsed = JSON.parse(text);
|
|
332
|
-
|
|
334
|
+
// Keep the serialized-size contract: normalization can expand numeric literals.
|
|
335
|
+
budget?.observeAcceptedRequestCopy(Buffer.byteLength(JSON.stringify(parsed), "utf8"));
|
|
333
336
|
return parsed;
|
|
334
337
|
} finally {
|
|
335
338
|
releaseText?.();
|
|
@@ -3,7 +3,7 @@ import { decodeJwtPayload, extractAccountId } from "../../oauth/chatgpt";
|
|
|
3
3
|
import type { OcxConfig } from "../../types";
|
|
4
4
|
import { boundedBodyDecodeFailure, readBoundedResponseBody } from "../../lib/bounded-body";
|
|
5
5
|
import { isApiAuthRequired, isProxyAdmissionSecret } from "../auth-cors";
|
|
6
|
-
import { structurallyValidFernetTokens } from "./encrypted-payload";
|
|
6
|
+
import { MAX_AGENT_TASK_CIPHERTEXT_BYTES, MAX_AGENT_TASK_ENCRYPTED_PARTS, structurallyValidFernetTokens } from "./encrypted-payload";
|
|
7
7
|
import {
|
|
8
8
|
cachedAgentTaskRecovery,
|
|
9
9
|
discardCachedAgentTaskRecovery,
|
|
@@ -31,7 +31,6 @@ const CODEX_ORIGINATORS = new Set([
|
|
|
31
31
|
const CODEX_OAUTH_CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
|
|
32
32
|
const OPENAI_TOKEN_ISSUERS = new Set(["https://auth.openai.com", "https://auth.openai.com/"]);
|
|
33
33
|
const OPENAI_TOKEN_AUDIENCE = "https://api.openai.com/v1";
|
|
34
|
-
const MAX_CIPHERTEXT_BYTES = 2 * 1024 * 1024;
|
|
35
34
|
const MAX_ASSIGNMENT_BYTES = 2 * 1024 * 1024;
|
|
36
35
|
const MAX_RECOVERY_RESPONSE_BYTES = 4 * 1024 * 1024;
|
|
37
36
|
const CACHE_SCOPE_KEY = randomBytes(32);
|
|
@@ -73,12 +72,13 @@ export function agentTaskRecoveryConfig(config: OcxConfig): AgentTaskRecoveryOpt
|
|
|
73
72
|
|
|
74
73
|
interface AgentEnvelope {
|
|
75
74
|
itemIndex: number;
|
|
76
|
-
|
|
75
|
+
encryptedStartIndex: number;
|
|
76
|
+
inputSnapshot: string;
|
|
77
77
|
headerText: string;
|
|
78
78
|
messageType: "NEW_TASK" | "MESSAGE";
|
|
79
79
|
taskName: string;
|
|
80
80
|
sender: string;
|
|
81
|
-
|
|
81
|
+
ciphertexts: readonly string[];
|
|
82
82
|
author: string;
|
|
83
83
|
recipient: string;
|
|
84
84
|
}
|
|
@@ -107,10 +107,9 @@ function findEnvelope(input: unknown): AgentEnvelope | null {
|
|
|
107
107
|
let messageType: "NEW_TASK" | "MESSAGE" | null = null;
|
|
108
108
|
let taskName: string | null = null;
|
|
109
109
|
let sender: string | null = null;
|
|
110
|
-
let
|
|
111
|
-
|
|
112
|
-
let
|
|
113
|
-
let ciphertextCount = 0;
|
|
110
|
+
let encryptedStartIndex = -1;
|
|
111
|
+
const ciphertexts: string[] = [];
|
|
112
|
+
let ciphertextBytes = 0;
|
|
114
113
|
|
|
115
114
|
for (let index = 0; index < content.length; index += 1) {
|
|
116
115
|
const part = content[index] as { type?: unknown; text?: unknown; encrypted_content?: unknown } | null;
|
|
@@ -132,13 +131,15 @@ function findEnvelope(input: unknown): AgentEnvelope | null {
|
|
|
132
131
|
sender = match[3]!;
|
|
133
132
|
}
|
|
134
133
|
}
|
|
135
|
-
if (part.type !== "encrypted_content"
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
134
|
+
if (part.type !== "encrypted_content") continue;
|
|
135
|
+
if (typeof part.encrypted_content !== "string") return null;
|
|
136
|
+
ciphertextBytes += Buffer.byteLength(part.encrypted_content);
|
|
137
|
+
if (ciphertexts.length >= MAX_AGENT_TASK_ENCRYPTED_PARTS || ciphertextBytes > MAX_AGENT_TASK_CIPHERTEXT_BYTES) return null;
|
|
138
|
+
const tokens = structurallyValidFernetTokens(part.encrypted_content);
|
|
139
|
+
if (tokens.length !== 1 || tokens[0] !== part.encrypted_content) return null;
|
|
140
|
+
if (encryptedStartIndex < 0) encryptedStartIndex = index;
|
|
141
|
+
if (index !== encryptedStartIndex + ciphertexts.length) return null;
|
|
142
|
+
ciphertexts.push(part.encrypted_content);
|
|
142
143
|
}
|
|
143
144
|
|
|
144
145
|
if (
|
|
@@ -146,11 +147,8 @@ function findEnvelope(input: unknown): AgentEnvelope | null {
|
|
|
146
147
|
|| !messageType
|
|
147
148
|
|| !taskName
|
|
148
149
|
|| !sender
|
|
149
|
-
||
|
|
150
|
-
||
|
|
151
|
-
|| ciphertextCount !== 1
|
|
152
|
-
|| (content[encryptedIndex] as { encrypted_content?: unknown }).encrypted_content !== ciphertext
|
|
153
|
-
|| Buffer.byteLength(ciphertext) > MAX_CIPHERTEXT_BYTES
|
|
150
|
+
|| encryptedStartIndex < 0
|
|
151
|
+
|| ciphertexts.length === 0
|
|
154
152
|
) return null;
|
|
155
153
|
|
|
156
154
|
const itemRecord = item as { author?: unknown; recipient?: unknown };
|
|
@@ -159,12 +157,13 @@ function findEnvelope(input: unknown): AgentEnvelope | null {
|
|
|
159
157
|
|
|
160
158
|
return {
|
|
161
159
|
itemIndex,
|
|
162
|
-
|
|
160
|
+
encryptedStartIndex,
|
|
161
|
+
inputSnapshot: JSON.stringify(item),
|
|
163
162
|
headerText,
|
|
164
163
|
messageType,
|
|
165
164
|
taskName,
|
|
166
165
|
sender,
|
|
167
|
-
|
|
166
|
+
ciphertexts,
|
|
168
167
|
author: itemRecord.author,
|
|
169
168
|
recipient: itemRecord.recipient,
|
|
170
169
|
};
|
|
@@ -197,14 +196,8 @@ function injectAssignment(input: unknown, envelope: AgentEnvelope, assignment: s
|
|
|
197
196
|
if (!item || typeof item !== "object") return false;
|
|
198
197
|
const content = (item as { content?: unknown }).content;
|
|
199
198
|
if (!Array.isArray(content)) return false;
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
!part
|
|
203
|
-
|| part.type !== "encrypted_content"
|
|
204
|
-
|| part.encrypted_content !== envelope.ciphertext
|
|
205
|
-
) return false;
|
|
206
|
-
|
|
207
|
-
content[envelope.encryptedIndex] = { type: "input_text", text: assignment };
|
|
199
|
+
if (JSON.stringify(item) !== envelope.inputSnapshot) return false;
|
|
200
|
+
content.splice(envelope.encryptedStartIndex, envelope.ciphertexts.length, { type: "input_text", text: assignment });
|
|
208
201
|
const message = item as Record<string, unknown>;
|
|
209
202
|
message.type = "message";
|
|
210
203
|
message.role = "user";
|
|
@@ -313,7 +306,7 @@ function admittedRecovery(
|
|
|
313
306
|
.update("\0")
|
|
314
307
|
.update(envelope.sender)
|
|
315
308
|
.update("\0")
|
|
316
|
-
.update(envelope.
|
|
309
|
+
.update(JSON.stringify(envelope.ciphertexts))
|
|
317
310
|
.digest("hex");
|
|
318
311
|
return { admitted: true, recovery: { envelope, admission, cacheKey } };
|
|
319
312
|
}
|
|
@@ -343,7 +336,7 @@ function recoveryPayload(envelope: AgentEnvelope, model: string): string {
|
|
|
343
336
|
recipient: envelope.recipient,
|
|
344
337
|
content: [
|
|
345
338
|
{ type: "input_text", text: envelope.headerText },
|
|
346
|
-
{ type: "encrypted_content", encrypted_content
|
|
339
|
+
...envelope.ciphertexts.map(encrypted_content => ({ type: "encrypted_content", encrypted_content })),
|
|
347
340
|
],
|
|
348
341
|
}],
|
|
349
342
|
});
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
CodexDirectAuthenticationError,
|
|
8
8
|
CodexMainProfileDrainingError,
|
|
9
9
|
CodexMainSubstitutionUnavailableError,
|
|
10
|
+
CodexModelAvailabilityError,
|
|
10
11
|
CodexPoolAuthenticationError,
|
|
11
12
|
CodexThreadAffinityExpiredError,
|
|
12
13
|
} from "../../codex/auth-context";
|
|
@@ -22,6 +23,13 @@ export interface CodexAuthContextErrorResponseOptions {
|
|
|
22
23
|
now: number;
|
|
23
24
|
}
|
|
24
25
|
|
|
26
|
+
export function codexModelAvailabilityErrorResponse(error: CodexModelAvailabilityError): Response {
|
|
27
|
+
if (error.reason === "temporarily_unavailable") {
|
|
28
|
+
return formatErrorResponse(429, "rate_limit_error", error.message);
|
|
29
|
+
}
|
|
30
|
+
return formatErrorResponse(400, "invalid_request_error", error.message);
|
|
31
|
+
}
|
|
32
|
+
|
|
25
33
|
export function nativeMainRefreshFailureResponse(error: unknown): Response {
|
|
26
34
|
if (error instanceof MainAccountTokenRefreshError && error.reason === "reauth") {
|
|
27
35
|
return formatErrorResponse(401, "authentication_error", "Codex main account needs reauthentication");
|
|
@@ -86,6 +94,9 @@ export function mapCodexAuthContextErrorToResponse(
|
|
|
86
94
|
"Selected Codex account needs reauthentication",
|
|
87
95
|
);
|
|
88
96
|
}
|
|
97
|
+
if (error instanceof CodexModelAvailabilityError) {
|
|
98
|
+
return codexModelAvailabilityErrorResponse(error);
|
|
99
|
+
}
|
|
89
100
|
if (error instanceof CodexPoolAuthenticationError || error instanceof CodexDirectAuthenticationError) {
|
|
90
101
|
return formatErrorResponse(401, "authentication_error", error.message);
|
|
91
102
|
}
|
|
@@ -6,7 +6,8 @@ import { CodexWsCorrelation } from "./codex-ws-correlation";
|
|
|
6
6
|
import type { CodexWsSession } from "./codex-ws-session";
|
|
7
7
|
import { UPGRADE_DEADLINE_MS, CODEX_WS_LIVENESS_PING_INTERVAL_MS, CODEX_WS_RESPONSE_PRELUDE_TIMEOUT_MS, MAX_CODEX_WS_FRAME_BYTES,
|
|
8
8
|
MAX_CODEX_WS_QUEUE_BYTES, markCodexWsResponse, normalizeResponsesWsRelayEvent, closedBeforeTerminalMessage,
|
|
9
|
-
codexWsFailureDetail, codexWsPreResponseFailure,
|
|
9
|
+
codexWsFailureDetail, codexWsPreResponseFailure, markCodexWsStage, codexWsOcxVersion,
|
|
10
|
+
type CodexWsFailureStage, type CodexWsStageRecord } from "./codex-ws-wire";
|
|
10
11
|
|
|
11
12
|
interface ExchangeOptions {
|
|
12
13
|
session: CodexWsSession;
|
|
@@ -16,6 +17,8 @@ interface ExchangeOptions {
|
|
|
16
17
|
sseFallback: typeof globalThis.fetch;
|
|
17
18
|
onQuota?: CodexWsQuotaObserver;
|
|
18
19
|
beforeDispatch?: (headers: Headers) => void;
|
|
20
|
+
/** Bun version string the caller gated on; stamped onto the stage record. */
|
|
21
|
+
bunVersion?: string;
|
|
19
22
|
}
|
|
20
23
|
|
|
21
24
|
const HTTP_HEADER_TOKEN = /^[!#$%&'*+.^_`|~0-9a-z-]+$/i;
|
|
@@ -83,7 +86,7 @@ function wrappedRejectionResponse(payload: Record<string, unknown>, prelude: Hea
|
|
|
83
86
|
|
|
84
87
|
/** The sole SSE exchange state machine for both one-shot and retained sockets. */
|
|
85
88
|
export function codexWsExchange(options: ExchangeOptions): Promise<Response> {
|
|
86
|
-
const { session, url, init, prepared, sseFallback, onQuota, beforeDispatch } = options;
|
|
89
|
+
const { session, url, init, prepared, sseFallback, onQuota, beforeDispatch, bunVersion } = options;
|
|
87
90
|
const { frameText, headers } = prepared;
|
|
88
91
|
const signal = init.signal ?? undefined;
|
|
89
92
|
return new Promise<Response>((resolve, reject) => {
|
|
@@ -105,6 +108,9 @@ export function codexWsExchange(options: ExchangeOptions): Promise<Response> {
|
|
|
105
108
|
let pongs = 0;
|
|
106
109
|
let sentAt: number | null = null;
|
|
107
110
|
let firstFrameAt: number | null = null;
|
|
111
|
+
// Numeric close code for the durable stage record; the reason string stays
|
|
112
|
+
// out of it on purpose (#4191 content-free contract).
|
|
113
|
+
let closeCode: number | null = null;
|
|
108
114
|
let controller: ReadableStreamDefaultController<Uint8Array> | null = null;
|
|
109
115
|
const encoder = new TextEncoder();
|
|
110
116
|
const metadata = url === CODEX_RESPONSES_HTTP_URL ? new CodexWsMetadata(onQuota) : null;
|
|
@@ -115,6 +121,9 @@ export function codexWsExchange(options: ExchangeOptions): Promise<Response> {
|
|
|
115
121
|
// interval so a peer that answers pings can never trip the silence bound while alive.
|
|
116
122
|
let silenceTimer: ReturnType<typeof setTimeout> | undefined;
|
|
117
123
|
let pingTimer: ReturnType<typeof setTimeout> | undefined;
|
|
124
|
+
// The resolved 200, retained so a later body failure can replace its
|
|
125
|
+
// success-shaped stage record with the failure-shaped one (#4191).
|
|
126
|
+
let committedResponse: Response | null = null;
|
|
118
127
|
const stream = new ReadableStream<Uint8Array>({
|
|
119
128
|
start(c) { controller = c; },
|
|
120
129
|
cancel() {
|
|
@@ -157,6 +166,28 @@ export function codexWsExchange(options: ExchangeOptions): Promise<Response> {
|
|
|
157
166
|
pongs,
|
|
158
167
|
});
|
|
159
168
|
|
|
169
|
+
/**
|
|
170
|
+
* The durable twin of failureStage (#4191). `requestBytes` is an explicit
|
|
171
|
+
* parameter so the committed-success path can pass null instead of paying
|
|
172
|
+
* the UTF-8 walk of a megabyte replay frame; failure callers pass
|
|
173
|
+
* `failureStage().requestBytes`, which measures exactly once.
|
|
174
|
+
*/
|
|
175
|
+
const stageRecord = (requestBytes: number | null): CodexWsStageRecord => ({
|
|
176
|
+
requestBytes,
|
|
177
|
+
sent,
|
|
178
|
+
upstreamFrames,
|
|
179
|
+
controlFrames,
|
|
180
|
+
relayedEvents,
|
|
181
|
+
firstFrameMs: sentAt !== null && firstFrameAt !== null ? Math.max(0, firstFrameAt - sentAt) : null,
|
|
182
|
+
elapsedMs: sentAt !== null ? Math.max(0, Date.now() - sentAt) : null,
|
|
183
|
+
pings,
|
|
184
|
+
pongs,
|
|
185
|
+
closeCode,
|
|
186
|
+
reused: session.reused,
|
|
187
|
+
ocxVersion: codexWsOcxVersion(),
|
|
188
|
+
bunVersion: bunVersion ?? "unknown",
|
|
189
|
+
});
|
|
190
|
+
|
|
160
191
|
const commitResponse = () => {
|
|
161
192
|
if (responseCommitted) return;
|
|
162
193
|
responseCommitted = true;
|
|
@@ -167,6 +198,8 @@ export function codexWsExchange(options: ExchangeOptions): Promise<Response> {
|
|
|
167
198
|
const response = new Response(stream, { status: 200, headers: responseHeaders });
|
|
168
199
|
metadata?.commit();
|
|
169
200
|
markCodexWsResponse(response, Boolean(metadata && onQuota));
|
|
201
|
+
markCodexWsStage(response, stageRecord(null));
|
|
202
|
+
committedResponse = response;
|
|
170
203
|
resolve(response);
|
|
171
204
|
};
|
|
172
205
|
|
|
@@ -186,7 +219,9 @@ export function codexWsExchange(options: ExchangeOptions): Promise<Response> {
|
|
|
186
219
|
try { controller?.close(); } catch { /* unused stream already closed */ }
|
|
187
220
|
session.dispose();
|
|
188
221
|
const message = error instanceof Error ? error.message : String(error);
|
|
189
|
-
|
|
222
|
+
const failureResponse = codexWsPreResponseFailure(status, message, prelude);
|
|
223
|
+
markCodexWsStage(failureResponse, stageRecord(Buffer.byteLength(frameText, "utf8")));
|
|
224
|
+
resolve(failureResponse);
|
|
190
225
|
return;
|
|
191
226
|
}
|
|
192
227
|
// A response is already flowing (or this transport has no metadata channel and
|
|
@@ -196,6 +231,11 @@ export function codexWsExchange(options: ExchangeOptions): Promise<Response> {
|
|
|
196
231
|
cleanup();
|
|
197
232
|
try { controller?.error(typeof error === "string" ? new Error(error) : error); } catch { /* stream already done */ }
|
|
198
233
|
session.dispose();
|
|
234
|
+
// A body failure replaces the success-shaped record commitResponse wrote:
|
|
235
|
+
// this settle is a failure, and the frame size is evidence again.
|
|
236
|
+
if (committedResponse) {
|
|
237
|
+
markCodexWsStage(committedResponse, stageRecord(Buffer.byteLength(frameText, "utf8")));
|
|
238
|
+
}
|
|
199
239
|
};
|
|
200
240
|
|
|
201
241
|
/** (Re)start the silence bound; every inbound frame or pong is proof of life. */
|
|
@@ -408,12 +448,21 @@ export function codexWsExchange(options: ExchangeOptions): Promise<Response> {
|
|
|
408
448
|
terminal = true;
|
|
409
449
|
cleanup();
|
|
410
450
|
try { controller.close(); } catch { /* already closed */ }
|
|
451
|
+
// Refresh the success record with the final counters: the commit-time
|
|
452
|
+
// snapshot predates every relayed event, and the record is more useful
|
|
453
|
+
// when it says what the exchange actually delivered. Still no byte
|
|
454
|
+
// count — the happy path never pays it.
|
|
455
|
+
if (committedResponse) {
|
|
456
|
+
markCodexWsStage(committedResponse, stageRecord(null));
|
|
457
|
+
}
|
|
411
458
|
session.release(completedId);
|
|
412
459
|
}
|
|
413
460
|
};
|
|
414
461
|
|
|
415
462
|
const onClose = (event: unknown) => {
|
|
416
463
|
cleanup();
|
|
464
|
+
const code = (event as { code?: unknown } | null)?.code;
|
|
465
|
+
if (typeof code === "number") closeCode = code;
|
|
417
466
|
if (!opened) {
|
|
418
467
|
if (settledPreOpen) return;
|
|
419
468
|
settledPreOpen = true;
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
UPSTREAM_CLOSED_BEFORE_RESPONSE_CODE,
|
|
5
5
|
UPSTREAM_NO_RESPONSE_CODE,
|
|
6
6
|
} from "../../lib/upstream-retry";
|
|
7
|
+
import { readFileSync } from "node:fs";
|
|
7
8
|
// If the 101 never arrives (network black hole), give SSE a chance well before
|
|
8
9
|
// the caller's connect timeout (default 200s) would fire.
|
|
9
10
|
export const UPGRADE_DEADLINE_MS = 10_000;
|
|
@@ -64,6 +65,60 @@ export function markCodexWsResponse(response: Response, observed: boolean): void
|
|
|
64
65
|
if (observed) quotaObservedResponses.add(response);
|
|
65
66
|
}
|
|
66
67
|
|
|
68
|
+
/**
|
|
69
|
+
* The proxy's own version, stamped onto every stage record so a field report
|
|
70
|
+
* can be tied to the exact build that produced it (#4191). Computed locally
|
|
71
|
+
* with the same package.json IIFE management-api.ts / gui-static.ts use —
|
|
72
|
+
* importing management-api from the transport layer would invert the
|
|
73
|
+
* layering and pull the management surface into every WS exchange.
|
|
74
|
+
*/
|
|
75
|
+
const OCX_VERSION = (() => {
|
|
76
|
+
try {
|
|
77
|
+
return JSON.parse(readFileSync(new URL("../../../package.json", import.meta.url), "utf8")).version as string;
|
|
78
|
+
} catch {
|
|
79
|
+
return "0.0.0";
|
|
80
|
+
}
|
|
81
|
+
})();
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* The durable form of the stage counters, carried out of the exchange on the
|
|
85
|
+
* resolved Response so the logging layer can persist it without the exchange
|
|
86
|
+
* ever seeing a RequestLogContext (#4191).
|
|
87
|
+
*
|
|
88
|
+
* Everything here is a size, a count, a duration, a boolean, or a semver
|
|
89
|
+
* string: no request body, no header, no account identifier, no conversation
|
|
90
|
+
* text, and no close-reason text can reach a record built by the exchange.
|
|
91
|
+
* `requestBytes` is null on a committed success because the happy path never
|
|
92
|
+
* pays the UTF-8 walk of a megabyte replay frame; it is measured on failure,
|
|
93
|
+
* where its size is the evidence.
|
|
94
|
+
*/
|
|
95
|
+
export type CodexWsStageRecord = Omit<CodexWsFailureStage, "requestBytes"> & {
|
|
96
|
+
/** UTF-8 size of the create frame; null on the committed-success record. */
|
|
97
|
+
requestBytes: number | null;
|
|
98
|
+
/** Numeric upstream close code when the socket closed; null otherwise. */
|
|
99
|
+
closeCode: number | null;
|
|
100
|
+
/** True when the exchange ran on a pooled, previously used session. */
|
|
101
|
+
reused: boolean;
|
|
102
|
+
/** OpenCodex version that produced this record. */
|
|
103
|
+
ocxVersion: string;
|
|
104
|
+
/** Bun runtime version the exchange gated on. */
|
|
105
|
+
bunVersion: string;
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const codexWsStageByResponse = new WeakMap<Response, CodexWsStageRecord>();
|
|
109
|
+
|
|
110
|
+
export function markCodexWsStage(response: Response, record: CodexWsStageRecord): void {
|
|
111
|
+
codexWsStageByResponse.set(response, record);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function readCodexWsStage(response: Response): CodexWsStageRecord | undefined {
|
|
115
|
+
return codexWsStageByResponse.get(response);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function codexWsOcxVersion(): string {
|
|
119
|
+
return OCX_VERSION;
|
|
120
|
+
}
|
|
121
|
+
|
|
67
122
|
/**
|
|
68
123
|
* The honest settlement for an exchange that sent its create frame and never saw a
|
|
69
124
|
* response event.
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { capturePoolQuotaWriter } from "../../codex/account-store";
|
|
1
2
|
import type { Server } from "bun";
|
|
2
3
|
import { bridgeToResponsesSSE, buildResponseJSON, formatErrorResponse, type ResponsesTerminalStatus } from "../../bridge";
|
|
3
4
|
import {
|
|
@@ -356,6 +357,7 @@ async function refreshPoolCompactContext(args: {
|
|
|
356
357
|
accessToken: refreshed.accessToken,
|
|
357
358
|
chatgptAccountId: refreshed.chatgptAccountId,
|
|
358
359
|
generation: refreshed.generation,
|
|
360
|
+
poolQuotaWriter: capturePoolQuotaWriter(authCtx.accountId, refreshed),
|
|
359
361
|
};
|
|
360
362
|
const refreshedProvider = applyCodexAuthContextToProvider(
|
|
361
363
|
stripCodexRuntimeProviderFields(provider),
|
|
@@ -1037,7 +1039,7 @@ export async function handleResponsesCompact(
|
|
|
1037
1039
|
upstream.headers,
|
|
1038
1040
|
authCtx.writerGeneration,
|
|
1039
1041
|
authCtx.kind === "main-pool" ? authCtx.mainQuotaWriter : undefined,
|
|
1040
|
-
{ modelId: route.modelId },
|
|
1042
|
+
{ modelId: route.modelId, poolWriter: authCtx.kind === "pool" ? authCtx.poolQuotaWriter : undefined },
|
|
1041
1043
|
);
|
|
1042
1044
|
}
|
|
1043
1045
|
recordCompactPoolOutcome(authCtx, upstream.status, {
|
|
@@ -1078,6 +1080,12 @@ export async function handleResponsesCompact(
|
|
|
1078
1080
|
}
|
|
1079
1081
|
}
|
|
1080
1082
|
}
|
|
1083
|
+
// Capture the final serving account as well as an earlier rejected account, once per response.
|
|
1084
|
+
if (outcomeCtx.kind === "pool") {
|
|
1085
|
+
const { applyAccountQuotaFromUpstreamHeaders } = await import("../../codex/quota");
|
|
1086
|
+
applyAccountQuotaFromUpstreamHeaders(outcomeCtx.accountId, upstream.headers, outcomeCtx.writerGeneration,
|
|
1087
|
+
undefined, { modelId: route.modelId, poolWriter: outcomeCtx.poolQuotaWriter });
|
|
1088
|
+
}
|
|
1081
1089
|
const retryAfter = upstream.headers.get("retry-after");
|
|
1082
1090
|
const resetAt = [
|
|
1083
1091
|
upstream.headers.get("x-codex-primary-reset-at"),
|