@bitkyc08/opencodex 2.42.0 → 2.43.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/AGENTS_INSTALL.md +2 -2
- package/README.md +31 -0
- package/bin/ocx.mjs +10 -1
- package/gui/dist/assets/index-DS1NE4Jn.css +1 -0
- package/gui/dist/assets/index-Djowl68T.js +112 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic-image-codec.ts +304 -0
- package/src/adapters/anthropic-image-normalize.ts +8 -298
- package/src/adapters/anthropic.ts +30 -7
- package/src/adapters/command-code.ts +7 -5
- package/src/adapters/cursor/desktop-executor-contract.ts +15 -0
- package/src/adapters/cursor/images.ts +36 -6
- package/src/adapters/cursor/live-transport.ts +7 -2
- package/src/adapters/cursor/native-exec-desktop.ts +2 -15
- package/src/adapters/cursor/protobuf-request.ts +13 -3
- package/src/adapters/cursor/tool-definitions.ts +5 -670
- package/src/adapters/cursor/tool-guidance.ts +236 -0
- package/src/adapters/cursor/tool-naming.ts +252 -0
- package/src/adapters/cursor/tool-schemas.ts +195 -0
- package/src/adapters/cursor/types.ts +6 -3
- package/src/adapters/exec-tool-result-normalize.ts +1 -1
- package/src/adapters/google-errors.ts +9 -1
- package/src/adapters/google.ts +1 -0
- package/src/adapters/kiro-calibration.ts +181 -0
- package/src/adapters/kiro.ts +135 -3
- package/src/adapters/openai-responses.ts +216 -26
- package/src/adapters/responses-code-mode.ts +59 -0
- package/src/adapters/tool-catalog-nudge.ts +1 -1
- package/src/adapters/xai-schema-analysis.ts +86 -0
- package/src/adapters/xai-tool-schema.ts +2 -87
- package/src/adapters/xai-web-search.ts +1 -1
- package/src/bridge.ts +22 -10
- package/src/chat/inbound.ts +11 -3
- package/src/claude/inbound-content-options.ts +60 -0
- package/src/claude/inbound-model-options.ts +142 -0
- package/src/claude/inbound-records.ts +7 -0
- package/src/claude/inbound.ts +10 -202
- package/src/claude/model-info.ts +45 -0
- package/src/cli/account-auth.ts +21 -6
- package/src/cli/capabilities.ts +13 -2
- package/src/cli/claude.ts +232 -39
- package/src/cli/config-command.ts +9 -1
- package/src/cli/dispatch.ts +5 -1
- package/src/cli/doctor.ts +10 -0
- package/src/cli/effort.ts +372 -0
- package/src/cli/export-command.ts +3 -9
- package/src/cli/help.ts +1 -0
- package/src/cli/index.ts +13 -0
- package/src/cli/init.ts +4 -0
- package/src/cli/model-selection-guidance.ts +30 -0
- package/src/cli/models-runtime.ts +3 -2
- package/src/cli/models.ts +8 -3
- package/src/cli/opencode.ts +4 -1
- package/src/cli/provider-runtime.ts +65 -0
- package/src/cli/provider.ts +8 -0
- package/src/cli/registry.ts +16 -2
- package/src/cli/runtime-api.ts +3 -1
- package/src/cli/star-prompt.ts +22 -6
- package/src/cli/status-probes.ts +168 -0
- package/src/cli/status.ts +5 -168
- package/src/clients/config-export/constants.ts +69 -0
- package/src/clients/config-export/contracts.ts +154 -0
- package/src/clients/config-export/dsh.ts +132 -0
- package/src/clients/config-export/fast-models.ts +29 -0
- package/src/clients/config-export/mcode.ts +83 -0
- package/src/clients/config-export/model-metadata.ts +108 -0
- package/src/clients/config-export/omp.ts +104 -0
- package/src/clients/config-export/zcode.ts +92 -0
- package/src/clients/config-export.ts +18 -710
- package/src/codex/account-lifecycle.ts +20 -3
- package/src/codex/account-usability.ts +2 -0
- package/src/codex/auth-api.ts +170 -23
- package/src/codex/auth-context.ts +328 -24
- package/src/codex/catalog/effort.ts +30 -4
- package/src/codex/catalog/metadata.ts +39 -9
- package/src/codex/catalog/native-models.ts +53 -12
- package/src/codex/catalog/parsing.ts +119 -5
- package/src/codex/catalog/provider-fetch.ts +150 -20
- package/src/codex/catalog/reserve.ts +52 -0
- package/src/codex/catalog/sync.ts +89 -16
- package/src/codex/convergence-types.ts +1 -0
- package/src/codex/convergence.ts +2 -0
- package/src/codex/data/upstream-models.json +1 -1
- package/src/codex/forward-transport-headers.ts +25 -0
- package/src/codex/inject.ts +3 -28
- package/src/codex/legacy-config-keys.ts +68 -0
- package/src/codex/log-guard/inspect-schema.ts +137 -0
- package/src/codex/log-guard/inspect.ts +2 -134
- package/src/codex/loopback-target.ts +54 -0
- package/src/codex/main-account-cache.ts +63 -1
- package/src/codex/main-account-hard-lock.ts +52 -0
- package/src/codex/main-account.ts +3 -1
- package/src/codex/management-convergence.ts +3 -0
- package/src/codex/model-entitlements.ts +54 -4
- package/src/codex/project-config-warnings.ts +92 -2
- package/src/codex/prompt-layers/encoding.ts +80 -0
- package/src/codex/prompt-layers/paths.ts +54 -0
- package/src/codex/prompt-layers/revision.ts +55 -0
- package/src/codex/prompt-layers/toml-edit.ts +163 -0
- package/src/codex/prompt-layers/toml-read.ts +181 -0
- package/src/codex/prompt-layers.ts +14 -520
- package/src/codex/quota-auto-refresh-state.ts +16 -0
- package/src/codex/quota-auto-refresh.ts +219 -0
- package/src/codex/quota-types.ts +51 -0
- package/src/codex/quota.ts +252 -93
- package/src/codex/reserve-availability.ts +177 -0
- package/src/codex/routing.ts +28 -9
- package/src/codex/shim.ts +53 -11
- package/src/codex/subagent-model-fallback.ts +23 -3
- package/src/combos/failover.ts +125 -7
- package/src/combos/identifiers.ts +89 -0
- package/src/combos/index.ts +4 -0
- package/src/combos/resolve.ts +80 -9
- package/src/combos/types.ts +20 -93
- package/src/config/subagent-models.ts +24 -0
- package/src/config.ts +156 -13
- package/src/generated/compatibility-version.json +454 -166
- package/src/generated/model-metadata.ts +1 -1
- package/src/integrations/journal.ts +65 -4
- package/src/integrations/store.ts +5 -0
- package/src/lab/events/limits.ts +4 -0
- package/src/lib/destination-policy.ts +31 -2
- package/src/lib/errors.ts +39 -0
- package/src/lib/provider-outbound.ts +69 -3
- package/src/lib/proxy-env.ts +22 -0
- package/src/lib/redact-folding.ts +176 -0
- package/src/lib/redact.ts +2 -175
- package/src/lib/state-store-sweeper.ts +20 -6
- package/src/lib/token-estimate.ts +94 -27
- package/src/lib/windows-user-principal.ts +53 -5
- package/src/oauth/anthropic-routing.ts +99 -3
- package/src/oauth/generic-account-failover.ts +36 -13
- package/src/oauth/index.ts +127 -26
- package/src/oauth/login-cli.ts +5 -0
- package/src/oauth/meta-muse.ts +117 -15
- package/src/oauth/pool-settings-capability.ts +15 -4
- package/src/providers/api-keys.ts +8 -10
- package/src/providers/default-aliases.ts +39 -0
- package/src/providers/derive.ts +10 -2
- package/src/providers/fastwire.ts +36 -7
- package/src/providers/initial-model-selection-runtime.ts +90 -0
- package/src/providers/initial-model-selection.ts +120 -0
- package/src/providers/key-failover.ts +134 -54
- package/src/providers/key-store.ts +11 -1
- package/src/providers/label.ts +1 -1
- package/src/providers/model-discovery.ts +76 -0
- package/src/providers/model-rename-startup.ts +72 -8
- package/src/providers/openai-sidecar.ts +17 -5
- package/src/providers/openai-tiers-destination.ts +102 -0
- package/src/providers/openai-tiers.ts +2 -99
- package/src/providers/opencode-go-transport.ts +41 -0
- package/src/providers/quota-key-accounts.ts +141 -0
- package/src/providers/quota-types.ts +9 -0
- package/src/providers/quota.ts +535 -104
- package/src/providers/registry.ts +60 -17
- package/src/providers/xai-responses-opt-in.ts +31 -5
- package/src/quota/reset-activation.ts +81 -0
- package/src/quota/reset-detector.ts +305 -0
- package/src/quota/reset-notify-config.ts +162 -0
- package/src/quota/reset-observer.ts +125 -0
- package/src/quota/reset-poller.ts +160 -0
- package/src/quota/reset-seen-store.ts +385 -0
- package/src/quota/reset-sinks.ts +199 -0
- package/src/quota/window-mapping.ts +106 -0
- package/src/responses/apply-patch-envelope.ts +46 -0
- package/src/responses/code-mode-helper-compat.ts +39 -1
- package/src/responses/custom-tool-compat.ts +10 -4
- package/src/responses/hosted-tool-policy.ts +12 -4
- package/src/responses/parser-content.ts +133 -0
- package/src/responses/parser-text-format.ts +24 -0
- package/src/responses/parser-tools.ts +188 -0
- package/src/responses/parser.ts +3 -326
- package/src/responses/state.ts +124 -28
- package/src/router.ts +48 -13
- package/src/routing/analytics.ts +1 -0
- package/src/routing/capability.ts +17 -4
- package/src/server/auth-cors.ts +7 -1
- package/src/server/background-lifecycle.ts +23 -1
- package/src/server/chat-completions.ts +25 -3
- package/src/server/claude-messages.ts +62 -5
- package/src/server/effort-row.ts +1 -1
- package/src/server/fast-row.ts +295 -0
- package/src/server/gui-static.ts +30 -4
- package/src/server/index.ts +107 -21
- package/src/server/management/agent-settings-routes.ts +2 -2
- package/src/server/management/combo-routes.ts +37 -9
- package/src/server/management/config-routes.ts +93 -2
- package/src/server/management/integration-routes.ts +108 -0
- package/src/server/management/model-routes.ts +13 -3
- package/src/server/management/model-rows.ts +20 -1
- package/src/server/management/native-integration-routes.ts +4 -1
- package/src/server/management/oauth-account-routes.ts +38 -10
- package/src/server/management/provider-routes.ts +34 -3
- package/src/server/management/quota-reset-routes.ts +57 -0
- package/src/server/management/route-registry.ts +7 -4
- package/src/server/management/shared.ts +19 -5
- package/src/server/management/system-routes.ts +3 -2
- package/src/server/management-api.ts +14 -2
- package/src/server/ports.ts +12 -2
- package/src/server/relay-eager.ts +38 -23
- package/src/server/relay.ts +4 -0
- package/src/server/request-log.ts +6 -0
- package/src/server/responses/codex-ws-correlation.ts +65 -0
- package/src/server/responses/codex-ws-exchange.ts +261 -0
- package/src/server/responses/codex-ws-metadata.ts +134 -0
- package/src/server/responses/codex-ws-pool.ts +162 -0
- package/src/server/responses/codex-ws-request.ts +87 -0
- package/src/server/responses/codex-ws-session.ts +93 -0
- package/src/server/responses/codex-ws-wire.ts +144 -0
- package/src/server/responses/collaboration.ts +41 -1
- package/src/server/responses/compact.ts +105 -12
- package/src/server/responses/core.ts +476 -57
- package/src/server/responses/fetch-helpers.ts +10 -3
- package/src/server/responses/input-admission.ts +16 -9
- package/src/server/responses/responses-field-backfill.ts +1 -1
- package/src/server/responses/ws-upstream.ts +34 -318
- package/src/server/responses-custom-tool-repair.ts +20 -4
- package/src/server/responses-undeclared-tool-guard.ts +100 -8
- package/src/server/safe-response-headers.ts +23 -0
- package/src/server/search.ts +9 -0
- package/src/server/subagent-models-startup.ts +27 -0
- package/src/server/system-env-shell.ts +238 -0
- package/src/server/system-env.ts +7 -234
- package/src/server/ws-bridge.ts +3 -25
- package/src/server/xai-responses-startup.ts +21 -0
- package/src/service-manager-probe.ts +1 -1
- package/src/service.ts +55 -16
- package/src/types/config.ts +108 -12
- package/src/types/provider.ts +36 -7
- package/src/types/tools.ts +26 -1
- package/src/types.ts +2 -0
- package/src/update/notify.ts +8 -2
- package/src/usage/cost.ts +38 -28
- package/src/usage/expected-prices.ts +34 -15
- package/src/usage/log.ts +2 -0
- package/src/usage/model-identity.ts +26 -0
- package/src/usage/summary.ts +15 -1
- package/src/vision/describe.ts +6 -0
- package/src/vision/image-rewrite.ts +108 -0
- package/src/vision/index.ts +19 -306
- package/src/vision/plan.ts +205 -0
- package/src/web-search/executor.ts +6 -0
- package/src/web-search/index.ts +8 -1
- package/gui/dist/assets/index-BU1tE0sr.js +0 -112
- package/gui/dist/assets/index-DL9-iS6J.css +0 -1
|
@@ -71,6 +71,8 @@ export type EagerRelayHooks = {
|
|
|
71
71
|
};
|
|
72
72
|
|
|
73
73
|
export type EagerRelayOptions = {
|
|
74
|
+
/** Caller cancellation, independent of the turn/shutdown controller. */
|
|
75
|
+
clientGoneSignal?: AbortSignal;
|
|
74
76
|
/** Bounded client queue in bytes; producer pauses above it. Default 8 MiB. */
|
|
75
77
|
maxQueueBytes?: number;
|
|
76
78
|
/** Transient-budget owner for the inline-rewrite frame buffer. */
|
|
@@ -103,6 +105,7 @@ export function relaySseEagerBounded(
|
|
|
103
105
|
const drainMs = opts?.postCancelDrainMs ?? DEFAULT_DRAIN_MS;
|
|
104
106
|
const drainBytes = opts?.postCancelDrainBytes ?? DEFAULT_DRAIN_BYTES;
|
|
105
107
|
const now = opts?.now ?? Date.now;
|
|
108
|
+
const clientGoneSignal = opts?.clientGoneSignal;
|
|
106
109
|
|
|
107
110
|
const reader = body.getReader();
|
|
108
111
|
const terminalEncoder = new TextEncoder();
|
|
@@ -179,6 +182,8 @@ export function relaySseEagerBounded(
|
|
|
179
182
|
let queuedBytes = 0;
|
|
180
183
|
let cancelled = false;
|
|
181
184
|
let done = false;
|
|
185
|
+
let drainedBytes = 0;
|
|
186
|
+
let drainDeadline = Number.POSITIVE_INFINITY;
|
|
182
187
|
// Pause gate: resolved by client pull, client cancel, or upstream abort so a
|
|
183
188
|
// paused producer ALWAYS resumes (audit blocker 2 — no deadlock; onDone and
|
|
184
189
|
// turn unregistration stay reachable, drainAndShutdown never hangs).
|
|
@@ -202,6 +207,7 @@ export function relaySseEagerBounded(
|
|
|
202
207
|
const fireDone = () => {
|
|
203
208
|
if (doneFired) return;
|
|
204
209
|
doneFired = true;
|
|
210
|
+
clientGoneSignal?.removeEventListener("abort", markClientGone);
|
|
205
211
|
if (drainTimer) { clearTimeout(drainTimer); drainTimer = null; }
|
|
206
212
|
try { hooks.onDone(); } catch { /* lifecycle callbacks must not break teardown */ }
|
|
207
213
|
};
|
|
@@ -217,6 +223,20 @@ export function relaySseEagerBounded(
|
|
|
217
223
|
(drainTimer as { unref?: () => void }).unref?.();
|
|
218
224
|
};
|
|
219
225
|
|
|
226
|
+
const markClientGone = () => {
|
|
227
|
+
if (cancelled || doneFired) return;
|
|
228
|
+
cancelled = true;
|
|
229
|
+
drainDeadline = now() + drainMs;
|
|
230
|
+
armDrainTimer();
|
|
231
|
+
wakeUp();
|
|
232
|
+
};
|
|
233
|
+
const canDeliver = () => {
|
|
234
|
+
// A rejected fetch read can settle before every abort listener is dispatched.
|
|
235
|
+
// Also re-check after error serialization, which can re-enter caller abort.
|
|
236
|
+
if (clientGoneSignal?.aborted) markClientGone();
|
|
237
|
+
return !cancelled && !upstream.signal.aborted;
|
|
238
|
+
};
|
|
239
|
+
|
|
220
240
|
const producer = async () => {
|
|
221
241
|
let syntheticKind: "incomplete" | "failed" | null = null;
|
|
222
242
|
let deliveryFallbackSent = false;
|
|
@@ -237,6 +257,7 @@ export function relaySseEagerBounded(
|
|
|
237
257
|
for (;;) {
|
|
238
258
|
const result = await reader.read();
|
|
239
259
|
const { done: upstreamDone, value } = result;
|
|
260
|
+
if (clientGoneSignal?.aborted) markClientGone();
|
|
240
261
|
// A chunk that already settled is INSPECTED before abort is honored. A read
|
|
241
262
|
// can settle with a real chunk in the same tick the signal fires (post-cancel
|
|
242
263
|
// drain: the terminal frame arrives, then the drain timer aborts upstream).
|
|
@@ -262,7 +283,7 @@ export function relaySseEagerBounded(
|
|
|
262
283
|
}
|
|
263
284
|
if (rewriteFailed) {
|
|
264
285
|
const safeTail = encodeFailedTail(rewriteError);
|
|
265
|
-
if (safeTail &&
|
|
286
|
+
if (safeTail && canDeliver()) {
|
|
266
287
|
if (!hooks.sawTerminal()) syntheticKind = "failed";
|
|
267
288
|
queuedBytes += safeTail.byteLength;
|
|
268
289
|
try { controllerRef?.enqueue(safeTail); } catch { /* client already torn down */ }
|
|
@@ -279,7 +300,7 @@ export function relaySseEagerBounded(
|
|
|
279
300
|
queuedBytes += terminalSentinel.byteLength;
|
|
280
301
|
try { controllerRef?.enqueue(terminalSentinel); } catch { /* client already gone */ }
|
|
281
302
|
}
|
|
282
|
-
} else if (!hooks.sawTerminal() &&
|
|
303
|
+
} else if (!hooks.sawTerminal() && canDeliver()) {
|
|
283
304
|
// A clean 200 EOF without a Responses terminal must be visible to
|
|
284
305
|
// Codex as one incomplete turn, followed by the normal sentinel.
|
|
285
306
|
queuedBytes += adapterEofFrame.byteLength + terminalSentinel.byteLength;
|
|
@@ -322,9 +343,7 @@ export function relaySseEagerBounded(
|
|
|
322
343
|
controllerRef?.enqueue(outbound);
|
|
323
344
|
} catch {
|
|
324
345
|
// Controller already torn down (client went away without cancel()).
|
|
325
|
-
|
|
326
|
-
drainDeadline = now() + drainMs;
|
|
327
|
-
armDrainTimer();
|
|
346
|
+
markClientGone();
|
|
328
347
|
continue;
|
|
329
348
|
}
|
|
330
349
|
}
|
|
@@ -339,11 +358,12 @@ export function relaySseEagerBounded(
|
|
|
339
358
|
reader.cancel("Responses terminal event received").catch(() => {});
|
|
340
359
|
break;
|
|
341
360
|
}
|
|
342
|
-
while (queuedBytes > maxQueueBytes &&
|
|
361
|
+
while (queuedBytes > maxQueueBytes && canDeliver()) {
|
|
343
362
|
await paused();
|
|
344
363
|
}
|
|
345
364
|
}
|
|
346
365
|
} catch (err) {
|
|
366
|
+
if (clientGoneSignal?.aborted) markClientGone();
|
|
347
367
|
// Upstream read failure. Distinguish genuine mid-stream reset from
|
|
348
368
|
// abort-driven teardown (shutdown/cancel-expiry) — audit M3.
|
|
349
369
|
// A read can fail after delivering an unterminated terminal block. Flush
|
|
@@ -379,36 +399,36 @@ export function relaySseEagerBounded(
|
|
|
379
399
|
clientTail = new Uint8Array(0);
|
|
380
400
|
}
|
|
381
401
|
}
|
|
382
|
-
if (clientTail.byteLength > 0 &&
|
|
402
|
+
if (clientTail.byteLength > 0 && canDeliver()) {
|
|
383
403
|
queuedBytes += clientTail.byteLength;
|
|
384
404
|
try { controllerRef?.enqueue(clientTail); } catch { /* client already torn down */ }
|
|
385
405
|
}
|
|
386
|
-
if (rewriteFailed &&
|
|
406
|
+
if (rewriteFailed && canDeliver()) {
|
|
387
407
|
// Never bypass a client rewrite after it fails: boundedTail can contain
|
|
388
408
|
// provider metadata or content that the active rewrite was required to
|
|
389
409
|
// remove. Emit one safe failed envelope instead. When inspection has
|
|
390
410
|
// already reported the real upstream terminal, this is a delivery
|
|
391
411
|
// fallback only and must not create a second accounting outcome.
|
|
392
412
|
const safeTail = encodeFailedTail(rewriteError ?? err);
|
|
393
|
-
if (safeTail &&
|
|
413
|
+
if (safeTail && canDeliver()) {
|
|
394
414
|
if (!hooks.sawTerminal()) syntheticKind = "failed";
|
|
395
415
|
deliveryFallbackSent = true;
|
|
396
416
|
queuedBytes += safeTail.byteLength;
|
|
397
417
|
try { controllerRef?.enqueue(safeTail); } catch { /* client already torn down */ }
|
|
398
418
|
try { controllerRef?.close(); } catch { /* client already gone */ }
|
|
399
419
|
}
|
|
400
|
-
} else if (tailTerminal &&
|
|
420
|
+
} else if (tailTerminal && canDeliver()) {
|
|
401
421
|
if (!tailDone) {
|
|
402
422
|
queuedBytes += terminalSentinel.byteLength;
|
|
403
423
|
try { controllerRef?.enqueue(terminalSentinel); } catch { /* client already gone */ }
|
|
404
424
|
}
|
|
405
|
-
} else if (!tailTerminal &&
|
|
425
|
+
} else if (!tailTerminal && canDeliver()) {
|
|
406
426
|
// Serializing `err` can run user-defined accessors (Error.message
|
|
407
427
|
// getters, toString) that re-entrantly cancel the client or abort the
|
|
408
428
|
// upstream. Build the tail FIRST, then re-check eligibility before
|
|
409
429
|
// committing to the synthetic terminal (adversarial review blocker).
|
|
410
430
|
const tail = encodeFailedTail(err);
|
|
411
|
-
if (tail &&
|
|
431
|
+
if (tail && canDeliver()) {
|
|
412
432
|
// Inspection and client framing have separate bounded parsers. If
|
|
413
433
|
// inspection resynchronized after an oversized frame and observed a
|
|
414
434
|
// later real terminal, it still must not suppress a terminal delivery
|
|
@@ -429,7 +449,7 @@ export function relaySseEagerBounded(
|
|
|
429
449
|
frameBufferBytes = 0;
|
|
430
450
|
}
|
|
431
451
|
terminalBoundary.dispose();
|
|
432
|
-
if (syntheticKind) hooks.onSynthetic(syntheticKind);
|
|
452
|
+
if (syntheticKind && canDeliver()) hooks.onSynthetic(syntheticKind);
|
|
433
453
|
if (cancelled && !hooks.sawTerminal()) {
|
|
434
454
|
hooks.onClientCancel();
|
|
435
455
|
}
|
|
@@ -437,21 +457,19 @@ export function relaySseEagerBounded(
|
|
|
437
457
|
upstream.abort();
|
|
438
458
|
reader.cancel().catch(() => {});
|
|
439
459
|
}
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
}
|
|
460
|
+
// Signal-driven cancellation need not have invoked the body's cancel hook.
|
|
461
|
+
try { controllerRef?.close(); } catch { /* already closed/errored */ }
|
|
443
462
|
try { hooks.disposeInspection?.(); } catch { /* inspection teardown must not block lifecycle cleanup */ }
|
|
444
463
|
try { activeRewrite?.dispose?.(); } catch { /* rewrite teardown must not block lifecycle cleanup */ }
|
|
445
464
|
fireDone();
|
|
446
465
|
}
|
|
447
466
|
};
|
|
448
467
|
|
|
449
|
-
let drainedBytes = 0;
|
|
450
|
-
let drainDeadline = Number.POSITIVE_INFINITY;
|
|
451
|
-
|
|
452
468
|
return new ReadableStream<Uint8Array>({
|
|
453
469
|
start(controller) {
|
|
454
470
|
controllerRef = controller;
|
|
471
|
+
clientGoneSignal?.addEventListener("abort", markClientGone, { once: true });
|
|
472
|
+
if (clientGoneSignal?.aborted) markClientGone();
|
|
455
473
|
void producer();
|
|
456
474
|
},
|
|
457
475
|
pull() {
|
|
@@ -462,10 +480,7 @@ export function relaySseEagerBounded(
|
|
|
462
480
|
wakeUp();
|
|
463
481
|
},
|
|
464
482
|
cancel() {
|
|
465
|
-
|
|
466
|
-
drainDeadline = now() + drainMs;
|
|
467
|
-
armDrainTimer();
|
|
468
|
-
wakeUp();
|
|
483
|
+
markClientGone();
|
|
469
484
|
},
|
|
470
485
|
});
|
|
471
486
|
}
|
package/src/server/relay.ts
CHANGED
|
@@ -1275,6 +1275,7 @@ function startBoundedInspectionPump(options: InspectionPumpOptions): void {
|
|
|
1275
1275
|
try {
|
|
1276
1276
|
for (;;) {
|
|
1277
1277
|
const { done, value } = await reader.read();
|
|
1278
|
+
if (clientGoneSignal?.aborted) markClientGone();
|
|
1278
1279
|
if (drainStopped) {
|
|
1279
1280
|
// stopDrain() cancelled the reader; the settled read is the wake-up.
|
|
1280
1281
|
clientGoneWithoutTerminal = !inspector.terminalSeen();
|
|
@@ -1312,6 +1313,9 @@ function startBoundedInspectionPump(options: InspectionPumpOptions): void {
|
|
|
1312
1313
|
}
|
|
1313
1314
|
}
|
|
1314
1315
|
} catch {
|
|
1316
|
+
// Bun can settle a fetch body read before dispatching all abort listeners.
|
|
1317
|
+
// Observe the signal itself before classifying that rejection as upstream.
|
|
1318
|
+
if (clientGoneSignal?.aborted) markClientGone();
|
|
1315
1319
|
// A read error can follow a final SSE block without its blank-line
|
|
1316
1320
|
// delimiter. Flush that candidate before classifying the transport as a
|
|
1317
1321
|
// synthetic reset; otherwise a real completed/failed/policy terminal is
|
|
@@ -594,6 +594,12 @@ export function requestLogErrorCode(
|
|
|
594
594
|
export function requestLogSpeedLabel(serviceTier: string | undefined): string | undefined {
|
|
595
595
|
const normalized = serviceTier?.trim().toLowerCase();
|
|
596
596
|
if (normalized === "priority" || normalized === "fast") return "fast";
|
|
597
|
+
// Ultra Fast is labelled even though nothing in the shipped catalog advertises it: the
|
|
598
|
+
// reporter on #3429 reached it by hand-editing their own catalog, the request completed,
|
|
599
|
+
// and the Logs column stayed empty because this returned undefined. A tier the proxy
|
|
600
|
+
// forwarded but refuses to name is an observability hole, not a feature gate — the flag
|
|
601
|
+
// decides whether the tier survives, not whether we admit to carrying it.
|
|
602
|
+
if (normalized === "ultrafast") return "ultrafast";
|
|
597
603
|
return undefined;
|
|
598
604
|
}
|
|
599
605
|
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
export const CODEX_WS_ID_MAX_BYTES = 4096;
|
|
2
|
+
export const CODEX_WS_MAX_TRACKED_ITEMS = 10_000;
|
|
3
|
+
const MAX_TRACKED_ID_BYTES = 1024 * 1024;
|
|
4
|
+
|
|
5
|
+
function record(value: unknown): value is Record<string, unknown> {
|
|
6
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
7
|
+
}
|
|
8
|
+
function id(value: unknown): value is string {
|
|
9
|
+
return typeof value === "string" && value.length > 0 && !/[\u0000-\u001f\u007f]/.test(value)
|
|
10
|
+
&& Buffer.byteLength(value) <= CODEX_WS_ID_MAX_BYTES;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** A cold incompatible response may finish one-shot; a reused socket cannot mix owners. */
|
|
14
|
+
export class CodexWsCorrelation {
|
|
15
|
+
private responseId: string | null = null;
|
|
16
|
+
private reusable = true;
|
|
17
|
+
private readonly items = new Set<string>();
|
|
18
|
+
private itemBytes = 0;
|
|
19
|
+
|
|
20
|
+
constructor(private readonly strict: boolean, private readonly previouslyCompleted: (id: string) => boolean) {}
|
|
21
|
+
|
|
22
|
+
private mismatch(): void {
|
|
23
|
+
this.reusable = false;
|
|
24
|
+
if (this.strict) throw new Error("codex websocket response identity mismatch");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
accept(event: Record<string, unknown>): void {
|
|
28
|
+
if (event.stream_id !== undefined) { this.mismatch(); return; }
|
|
29
|
+
if (event.type === "error") return;
|
|
30
|
+
const response = record(event.response) ? event.response : undefined;
|
|
31
|
+
if (event.type === "response.created") {
|
|
32
|
+
const next = response?.id;
|
|
33
|
+
if (!id(next) || this.responseId !== null || this.previouslyCompleted(next)) {
|
|
34
|
+
this.mismatch();
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
this.responseId = next;
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (!this.responseId) { this.mismatch(); return; }
|
|
41
|
+
if ((response && response.id !== this.responseId)
|
|
42
|
+
|| (event.response_id !== undefined && event.response_id !== this.responseId)) this.mismatch();
|
|
43
|
+
const item = record(event.item) ? event.item : undefined;
|
|
44
|
+
if (event.type === "response.output_item.added") {
|
|
45
|
+
if (!id(item?.id) || this.items.has(item.id)) { this.mismatch(); return; }
|
|
46
|
+
this.itemBytes += Buffer.byteLength(item.id);
|
|
47
|
+
if (this.items.size >= CODEX_WS_MAX_TRACKED_ITEMS || this.itemBytes > MAX_TRACKED_ID_BYTES) {
|
|
48
|
+
throw new Error("codex websocket correlation exceeds its bounded item budget");
|
|
49
|
+
}
|
|
50
|
+
this.items.add(item.id);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const itemId = event.item_id ?? item?.id;
|
|
54
|
+
if (itemId !== undefined && (!id(itemId) || !this.items.has(itemId))) this.mismatch();
|
|
55
|
+
if (typeof event.type === "string" && event.type.endsWith(".delta") && itemId === undefined) this.mismatch();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
completed(event: Record<string, unknown>): string | null {
|
|
59
|
+
const response = record(event.response) ? event.response : undefined;
|
|
60
|
+
return this.reusable && event.type === "response.completed" && response?.status === "completed"
|
|
61
|
+
&& response.id === this.responseId ? this.responseId : null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
finish(): void { this.items.clear(); this.itemBytes = 0; }
|
|
65
|
+
}
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import { MAX_CLIENT_SSE_FRAME_BYTES } from "../sse-frame-buffer";
|
|
2
|
+
import { CodexWsMetadata, type CodexWsQuotaObserver } from "./codex-ws-metadata";
|
|
3
|
+
import { CODEX_RESPONSES_HTTP_URL, type PreparedCodexWsRequest } from "./codex-ws-request";
|
|
4
|
+
import { CodexWsCorrelation } from "./codex-ws-correlation";
|
|
5
|
+
import type { CodexWsSession } from "./codex-ws-session";
|
|
6
|
+
import { UPGRADE_DEADLINE_MS, CODEX_WS_RESPONSE_PRELUDE_TIMEOUT_MS, MAX_CODEX_WS_FRAME_BYTES,
|
|
7
|
+
MAX_CODEX_WS_QUEUE_BYTES, markCodexWsResponse, normalizeResponsesWsRelayEvent, closedBeforeTerminalMessage } from "./codex-ws-wire";
|
|
8
|
+
|
|
9
|
+
interface ExchangeOptions {
|
|
10
|
+
session: CodexWsSession;
|
|
11
|
+
url: string;
|
|
12
|
+
init: RequestInit;
|
|
13
|
+
prepared: PreparedCodexWsRequest;
|
|
14
|
+
sseFallback: typeof globalThis.fetch;
|
|
15
|
+
onQuota?: CodexWsQuotaObserver;
|
|
16
|
+
beforeDispatch?: (headers: Headers) => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** The sole SSE exchange state machine for both one-shot and retained sockets. */
|
|
20
|
+
export function codexWsExchange(options: ExchangeOptions): Promise<Response> {
|
|
21
|
+
const { session, url, init, prepared, sseFallback, onQuota, beforeDispatch } = options;
|
|
22
|
+
const { frameText, headers } = prepared;
|
|
23
|
+
const signal = init.signal ?? undefined;
|
|
24
|
+
return new Promise<Response>((resolve, reject) => {
|
|
25
|
+
const ws = session.socket;
|
|
26
|
+
|
|
27
|
+
let opened = session.opened;
|
|
28
|
+
let settledPreOpen = false;
|
|
29
|
+
let sent = false;
|
|
30
|
+
let received = false;
|
|
31
|
+
let responseCommitted = false;
|
|
32
|
+
let terminal = false;
|
|
33
|
+
let controller: ReadableStreamDefaultController<Uint8Array> | null = null;
|
|
34
|
+
const encoder = new TextEncoder();
|
|
35
|
+
const metadata = url === CODEX_RESPONSES_HTTP_URL ? new CodexWsMetadata(onQuota) : null;
|
|
36
|
+
const correlation = session.retainable ? new CodexWsCorrelation(session.reused, id => session.hasCompleted(id)) : null;
|
|
37
|
+
let detachOwner = () => {};
|
|
38
|
+
let preludeTimer: ReturnType<typeof setTimeout> | undefined;
|
|
39
|
+
const stream = new ReadableStream<Uint8Array>({
|
|
40
|
+
start(c) { controller = c; },
|
|
41
|
+
cancel() {
|
|
42
|
+
if (terminal) return;
|
|
43
|
+
terminal = true;
|
|
44
|
+
cleanup();
|
|
45
|
+
session.dispose();
|
|
46
|
+
},
|
|
47
|
+
}, new ByteLengthQueuingStrategy({ highWaterMark: MAX_CODEX_WS_QUEUE_BYTES }));
|
|
48
|
+
|
|
49
|
+
const cleanup = () => {
|
|
50
|
+
clearTimeout(upgradeTimer);
|
|
51
|
+
clearTimeout(preludeTimer);
|
|
52
|
+
signal?.removeEventListener("abort", onAbort);
|
|
53
|
+
metadata?.finish();
|
|
54
|
+
correlation?.finish();
|
|
55
|
+
detachOwner();
|
|
56
|
+
ws.removeEventListener("open", onOpen);
|
|
57
|
+
ws.removeEventListener("message", onMessage);
|
|
58
|
+
ws.removeEventListener("close", onClose);
|
|
59
|
+
ws.removeEventListener("error", onError);
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const commitResponse = () => {
|
|
63
|
+
if (responseCommitted) return;
|
|
64
|
+
responseCommitted = true;
|
|
65
|
+
clearTimeout(preludeTimer);
|
|
66
|
+
const responseHeaders = metadata?.snapshot() ?? new Headers();
|
|
67
|
+
responseHeaders.set("content-type", "text/event-stream; charset=utf-8");
|
|
68
|
+
const response = new Response(stream, { status: 200, headers: responseHeaders });
|
|
69
|
+
metadata?.commit();
|
|
70
|
+
markCodexWsResponse(response, Boolean(metadata && onQuota));
|
|
71
|
+
resolve(response);
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const failStream = (error: unknown) => {
|
|
75
|
+
if (terminal) return;
|
|
76
|
+
terminal = true;
|
|
77
|
+
// A frame may already be executing upstream. Settle as a body failure,
|
|
78
|
+
// never a fetch rejection/5xx that the pre-stream wrapper could resend.
|
|
79
|
+
if (sent) commitResponse();
|
|
80
|
+
cleanup();
|
|
81
|
+
try { controller?.error(typeof error === "string" ? new Error(error) : error); } catch { /* stream already done */ }
|
|
82
|
+
session.dispose();
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const upgradeTimer = setTimeout(() => {
|
|
86
|
+
if (opened || settledPreOpen) return;
|
|
87
|
+
settledPreOpen = true;
|
|
88
|
+
cleanup();
|
|
89
|
+
session.dispose();
|
|
90
|
+
resolve(sseFallback(url, init));
|
|
91
|
+
}, UPGRADE_DEADLINE_MS);
|
|
92
|
+
|
|
93
|
+
const cancelExchange = (reason: unknown) => {
|
|
94
|
+
if (terminal || settledPreOpen) return;
|
|
95
|
+
if (!sent) {
|
|
96
|
+
settledPreOpen = true;
|
|
97
|
+
terminal = true;
|
|
98
|
+
cleanup();
|
|
99
|
+
session.dispose();
|
|
100
|
+
reject(reason);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
failStream(reason);
|
|
104
|
+
};
|
|
105
|
+
const onAbort = () => cancelExchange(signal?.reason ?? new DOMException("The operation was aborted.", "AbortError"));
|
|
106
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
107
|
+
|
|
108
|
+
const onOpen = () => {
|
|
109
|
+
if (settledPreOpen) return;
|
|
110
|
+
clearTimeout(upgradeTimer);
|
|
111
|
+
opened = true;
|
|
112
|
+
try {
|
|
113
|
+
beforeDispatch?.(new Headers(headers));
|
|
114
|
+
} catch (error) {
|
|
115
|
+
// Settle and detach before close: a synchronous close event must not resend over SSE.
|
|
116
|
+
settledPreOpen = true;
|
|
117
|
+
terminal = true;
|
|
118
|
+
cleanup();
|
|
119
|
+
ws.removeEventListener("open", onOpen);
|
|
120
|
+
ws.removeEventListener("message", onMessage);
|
|
121
|
+
ws.removeEventListener("close", onClose);
|
|
122
|
+
ws.removeEventListener("error", onError);
|
|
123
|
+
session.dispose();
|
|
124
|
+
reject(error);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
if (terminal || settledPreOpen || signal?.aborted) return;
|
|
128
|
+
sent = true;
|
|
129
|
+
try {
|
|
130
|
+
ws.send(frameText);
|
|
131
|
+
} catch {
|
|
132
|
+
if (received || responseCommitted) {
|
|
133
|
+
if (terminal) session.dispose();
|
|
134
|
+
failStream("codex websocket send failed after response activity");
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
// send() throwing means the frame never left, so no upstream turn
|
|
138
|
+
// started and the SSE resend cannot double-generate. Falling back
|
|
139
|
+
// (instead of erroring a synthetic 200 body) keeps the pre-stream
|
|
140
|
+
// HTTP error/refresh/failover machinery in charge.
|
|
141
|
+
settledPreOpen = true;
|
|
142
|
+
sent = false;
|
|
143
|
+
cleanup();
|
|
144
|
+
session.dispose();
|
|
145
|
+
resolve(sseFallback(url, init));
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
if (!metadata) commitResponse();
|
|
149
|
+
else if (!responseCommitted && !terminal) {
|
|
150
|
+
preludeTimer = setTimeout(() => failStream("codex websocket response prelude timed out"), CODEX_WS_RESPONSE_PRELUDE_TIMEOUT_MS);
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const onMessage = (event: MessageEvent) => {
|
|
155
|
+
if (!controller || terminal) return;
|
|
156
|
+
received = true;
|
|
157
|
+
const text = typeof event.data === "string" ? event.data : "";
|
|
158
|
+
if (!text) return;
|
|
159
|
+
// UTF-8 byte length is always at least the JS string length. Reject this
|
|
160
|
+
// cheap lower bound before parsing so an obviously oversized frame does
|
|
161
|
+
// not create another large object graph.
|
|
162
|
+
if (text.length > MAX_CODEX_WS_FRAME_BYTES) {
|
|
163
|
+
failStream("codex websocket frame exceeds the response size limit");
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
const rawEncodedText = encoder.encode(text);
|
|
167
|
+
if (rawEncodedText.byteLength > MAX_CODEX_WS_FRAME_BYTES) {
|
|
168
|
+
failStream("codex websocket frame exceeds the response size limit");
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
const normalized = normalizeResponsesWsRelayEvent(text);
|
|
172
|
+
if (!normalized) return;
|
|
173
|
+
const { type } = normalized;
|
|
174
|
+
let relayText = normalized.text;
|
|
175
|
+
let controlFrame = false;
|
|
176
|
+
if (metadata) {
|
|
177
|
+
try {
|
|
178
|
+
const sanitized = metadata.consume(normalized.payload, rawEncodedText.byteLength);
|
|
179
|
+
if (sanitized !== null) {
|
|
180
|
+
relayText = sanitized;
|
|
181
|
+
controlFrame = true;
|
|
182
|
+
}
|
|
183
|
+
} catch (error) {
|
|
184
|
+
failStream(error);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
const encodedText = relayText === text ? rawEncodedText : encoder.encode(relayText);
|
|
189
|
+
if (encodedText.byteLength > MAX_CODEX_WS_FRAME_BYTES) {
|
|
190
|
+
failStream("codex websocket frame exceeds the response size limit");
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
if (!controlFrame && !type.startsWith("response.") && type !== "error") return;
|
|
194
|
+
if (!controlFrame) {
|
|
195
|
+
try { correlation?.accept(normalized.payload); } catch (error) { failStream(error); return; }
|
|
196
|
+
commitResponse();
|
|
197
|
+
}
|
|
198
|
+
const prefix = encoder.encode(`event: ${type}\ndata: `);
|
|
199
|
+
const suffix = encoder.encode("\n\n");
|
|
200
|
+
const frameBytes = prefix.byteLength + encodedText.byteLength + suffix.byteLength;
|
|
201
|
+
if (frameBytes > MAX_CLIENT_SSE_FRAME_BYTES) {
|
|
202
|
+
failStream("codex websocket frame exceeds the response size limit");
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
const availableBytes = controller.desiredSize ?? 0;
|
|
206
|
+
if (frameBytes > availableBytes) {
|
|
207
|
+
failStream("codex websocket response exceeded the buffered queue limit");
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
const sseFrame = new Uint8Array(frameBytes);
|
|
211
|
+
sseFrame.set(prefix);
|
|
212
|
+
sseFrame.set(encodedText, prefix.byteLength);
|
|
213
|
+
sseFrame.set(suffix, prefix.byteLength + encodedText.byteLength);
|
|
214
|
+
try {
|
|
215
|
+
controller.enqueue(sseFrame);
|
|
216
|
+
} catch {
|
|
217
|
+
failStream("codex websocket response stream closed while enqueueing");
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
if (type === "response.completed" || type === "response.failed" || type === "response.incomplete" || type === "error") {
|
|
221
|
+
const completedId = correlation?.completed(normalized.payload) ?? null;
|
|
222
|
+
terminal = true;
|
|
223
|
+
cleanup();
|
|
224
|
+
try { controller.close(); } catch { /* already closed */ }
|
|
225
|
+
session.release(completedId);
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
const onClose = (event: unknown) => {
|
|
230
|
+
cleanup();
|
|
231
|
+
if (!opened) {
|
|
232
|
+
if (settledPreOpen) return;
|
|
233
|
+
settledPreOpen = true;
|
|
234
|
+
// Upgrade rejected (401/403/429/5xx). Retry over plain SSE so the real
|
|
235
|
+
// HTTP status reaches the existing refresh/rotation handlers. No turn
|
|
236
|
+
// started upstream, so the resend cannot double-generate.
|
|
237
|
+
resolve(sseFallback(url, init));
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
if (sent && !terminal) failStream(closedBeforeTerminalMessage(event));
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
const onError = () => {
|
|
244
|
+
if (terminal || settledPreOpen) return;
|
|
245
|
+
if (!opened && !sent) {
|
|
246
|
+
settledPreOpen = true;
|
|
247
|
+
terminal = true;
|
|
248
|
+
cleanup();
|
|
249
|
+
session.dispose();
|
|
250
|
+
resolve(sseFallback(url, init));
|
|
251
|
+
} else failStream("codex websocket transport error");
|
|
252
|
+
};
|
|
253
|
+
detachOwner = session.bindOwner(reason => cancelExchange(reason));
|
|
254
|
+
ws.addEventListener("open", onOpen);
|
|
255
|
+
ws.addEventListener("message", onMessage);
|
|
256
|
+
ws.addEventListener("close", onClose);
|
|
257
|
+
ws.addEventListener("error", onError);
|
|
258
|
+
if (signal?.aborted) onAbort();
|
|
259
|
+
else if (session.opened) onOpen();
|
|
260
|
+
});
|
|
261
|
+
}
|