Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions docs-site/src/content/docs/reference/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -200,3 +200,28 @@ The internal model lives in `types.ts`: `OcxParsedRequest`, `OcxContext`, the `O
`OcxContentPart` (text / image), `OcxToolCall`, `OcxTool`, `AdapterEvent`, and the config types
(`OcxConfig`, `OcxProviderConfig`). Two helpers are widely used: `namespacedToolName()` and
`modelInList()` (tolerant `:size`-tag matching for `noVisionModels` / `noReasoningModels`).


### Incomplete quota terminals

A native forward response that ends with quota or rate-limit evidence in an
`incomplete` terminal records account quota failure and spawn-fallback health.
Structured `incomplete_details.reason` and error codes are accepted without a
message; ordinary output-limit, filtering, steering and stall incompletes do not
cool an account. Cyber-policy classification retains precedence. The terminal is
not replayed after output, and fixed-account request selection remains fixed.

Remote compact requests can buffer their response for longer than the server's
request-idle timeout. That listener timeout is disabled after the request body is
accepted; client cancellation and upstream operation deadlines still apply.

Buffered routed compaction treats nonempty text and reasoning deltas as progress
without exposing partial summary text. Comments, empty deltas and gateway
keepalives do not reset the adapter-event stall watchdog. The default stall
timeout stays 300 seconds; encrypted compaction content is preserved unchanged.

Native compact response buffering also enforces a body-byte inactivity deadline
using `stallTimeoutSec` (300 seconds by default). Nonempty chunks reset that
deadline; a stalled body returns HTTP 504, client cancellation retains HTTP 499,
and cleanup does not wait for a stuck upstream cancellation promise. The 32 MiB
response ceiling and the original body bytes are preserved.
2 changes: 2 additions & 0 deletions scripts/test-layout/layout.json
Original file line number Diff line number Diff line change
Expand Up @@ -492,6 +492,7 @@
"command-code-quota.test.ts": "providers",
"command-code-workspace-cache.test.ts": "providers",
"commandcode-provider.test.ts": "providers",
"compaction-progress.test.ts": "responses",
"compatibility-manifest.test.ts": "codex-integration",
"compatibility-provider-equivalence.test.ts": "routing",
"compatibility-version.test.ts": "ci-workflows",
Expand Down Expand Up @@ -1021,6 +1022,7 @@
"responses-context-overflow.test.ts": "responses",
"responses-custom-tool-guidance.test.ts": "responses",
"responses-custom-tool-repair.test.ts": "responses",
"responses-forward-incomplete-quota.test.ts": "responses",
"responses-function-tool-repair.test.ts": "responses",
"responses-fetch-helpers-boundary.test.ts": "responses",
"responses-field-backfill.test.ts": "responses",
Expand Down
14 changes: 14 additions & 0 deletions src/adapters/openai-responses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2546,6 +2546,7 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
let snapshot = "";
let usage: OcxUsage | undefined;
let compactionEncryptedContent: string | undefined;
let completedSeen = false;
for await (const event of decodeServerSentEvents(response.body, { translatorBudget: budget })) {
let payload: unknown;
try { payload = JSON.parse(event.data); } catch { continue; }
Expand Down Expand Up @@ -2580,6 +2581,7 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
return;
case "response.completed":
{
completedSeen = true;
const responsePayload = isPlainObject(payload.response) ? payload.response : undefined;
const output = Array.isArray(responsePayload?.output) ? responsePayload.output : [];
const compaction = output.find(item => isPlainObject(item) && item.type === "compaction");
Expand Down Expand Up @@ -2620,6 +2622,18 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
}
break;
}
// Buffered text is still upstream progress, but gateway keepalives are not.
// Yield after accounting, directly to the consumer: no progress queue or content leak.
if (
!completedSeen
&& (payload.type === "response.output_text.delta"
|| payload.type === "response.reasoning_summary_text.delta"
|| payload.type === "response.reasoning_text.delta")
&& typeof payload.delta === "string"
&& payload.delta.length > 0
) {
yield { type: "heartbeat" };
}
}
// Gateways differ in which of these they emit; prefer the authoritative
// completed snapshot so text is never double-counted.
Expand Down
4 changes: 3 additions & 1 deletion src/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1765,7 +1765,9 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
return runAdmittedHttpTurn(req, policy, async turnAdmissionLease => {
let response: Response;
try {
response = await handleResponsesCompact(req, config, logCtx, turnAdmissionLease, admission);
response = await handleResponsesCompact(req, config, logCtx, turnAdmissionLease, admission, {
onRequestBodyRead: () => disableResponsesRequestTimeout(req, requestServer),
});
} catch {
response = formatErrorResponse(500, "server_error", "Unexpected compact request failure");
}
Expand Down
33 changes: 31 additions & 2 deletions src/server/request-log.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
isClientClosedMessage,
isCyberPolicyCode,
isCyberPolicyMessage,
isRateLimitOrQuotaFailureMessage,
upstreamErrorMessageFromPayload,
} from "../lib/errors";
import { CODEX_CONFIG_PATH, readRootTomlString } from "../codex/paths";
Expand Down Expand Up @@ -852,7 +853,7 @@ function captureTerminalHttpStatus(
last_error?: { type?: unknown; code?: unknown; message?: unknown };
response?: {
error?: { type?: unknown; code?: unknown; message?: unknown };
incomplete_details?: { code?: unknown; message?: unknown };
incomplete_details?: { code?: unknown; message?: unknown; reason?: unknown };
};
},
): void {
Expand All @@ -861,7 +862,9 @@ function captureTerminalHttpStatus(
if (type !== "response.failed" && type !== "response.incomplete" && type !== "error") return;
const responseError = json.response?.error;
const responseDetails = json.response?.incomplete_details;
const candidates = [json.error, json.last_error, responseError, responseDetails, json];
const candidates: Array<{ type?: unknown; code?: unknown; message?: unknown } | undefined> = [
json.error, json.last_error, responseError, responseDetails, json,
];
const policy = candidates.some(candidate => (
candidate?.code === null || typeof candidate?.code === "string"
) && isCyberPolicyCode(candidate.code as string | null | undefined))
Expand All @@ -875,6 +878,29 @@ function captureTerminalHttpStatus(
logCtx.terminalHttpStatus = 400;
return;
}
// A quota terminal can carry only a structured reason, without an error message.
// Keep this separate from normal output limits and from the policy precedence above.
const quotaTag = (value: unknown): boolean => value === "usage_limit_reached"
|| value === "rate_limit_exceeded" || value === "insufficient_quota";
const structuredRefusal = candidates.some(candidate => [400, 401, 403, 499].includes(
httpStatusFromTerminalError({
type: typeof candidate?.type === "string" ? candidate.type : undefined,
code: typeof candidate?.code === "string" ? candidate.code : undefined,
}),
));
const ordinaryIncompleteReason = typeof responseDetails?.reason === "string"
&& ["max_output_tokens", "content_filter", "steered", "upstream_stall_timeout", "adapter_eof"].includes(responseDetails.reason);
if (type === "response.incomplete" && !structuredRefusal && (quotaTag(responseDetails?.reason) || candidates.some(candidate =>
quotaTag(candidate?.code)
|| quotaTag(candidate?.type) || candidate?.type === "rate_limit_error"
|| (!ordinaryIncompleteReason && typeof candidate?.message === "string" && isRateLimitOrQuotaFailureMessage(candidate.message))
))) {
// The shared quota classifier also accepts a numeric HTTP status as its message.
// Preserve explicit payment-required evidence rather than relabeling it as 429.
logCtx.terminalHttpStatus = candidates.some(candidate => typeof candidate?.message === "string"
&& Number(candidate.message.trim()) === 402) ? 402 : 429;
return;
}
if (type !== "response.failed" || !responseError || typeof responseError !== "object") return;
const responseCode = responseError.code === null || typeof responseError.code === "string"
? responseError.code
Expand Down Expand Up @@ -903,6 +929,9 @@ export function httpStatusForRequestLogTerminal(
status: ResponsesTerminalStatus,
logCtx?: RequestLogContext,
): number {
if (status === "incomplete" && (logCtx?.terminalHttpStatus === 429 || logCtx?.terminalHttpStatus === 402)) {
return logCtx.terminalHttpStatus;
}
/**
* [Decision Log]
* - 목적과 의도: Keep request logs aligned with the successful HTTP/SSE contract.
Expand Down
72 changes: 39 additions & 33 deletions src/server/responses/compact.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,8 @@ import type { WsData } from "../ws-bridge";
import { codexAccountSelectionForTurn, registerTurn, trackStreamLifetime, unregisterTurn } from "../lifecycle";
import type { AdmissionLease } from "../../lib/admission";
import { redactSecretString } from "../../lib/redact";
import { readBoundedResponseBody } from "../../lib/bounded-body";
import { readBoundedResponseBytes } from "../../lib/bounded-body";
import { resolveStallTimeoutSec } from "../../stall-timeout";
import { isRateLimitOrQuotaFailureMessage } from "../../lib/errors";
import { supportedLadderFor } from "../effort-policy";
import {
Expand Down Expand Up @@ -212,6 +213,8 @@ function compactHandoffRoute(req: Request, previousModel: string, now = Date.now

export interface HandleResponsesCompactOptions {
nativeMainRefreshDependencies?: NativeMainRefreshDependencies;
/** Release the listener's idle guard only after the complete request body is accepted. */
onRequestBodyRead?: () => void;
}

export function compactResponseTooLargeError(): Response {
Expand Down Expand Up @@ -464,43 +467,45 @@ function compactResponseHeaders(upstream: Response): Headers {
return headers;
}

export async function bufferCompactResponse(upstream: Response, signal: AbortSignal): Promise<Response> {
const reader = upstream.body?.getReader();
export async function bufferCompactResponse(
upstream: Response,
signal: AbortSignal,
stallTimeoutSec?: number,
): Promise<Response> {
const headers = compactResponseHeaders(upstream);
if (!reader) return new Response(null, { status: upstream.status, statusText: upstream.statusText, headers });
const declaredLength = Number(upstream.headers.get("content-length"));
if (Number.isFinite(declaredLength) && declaredLength > COMPACT_RESPONSE_MAX_BYTES) {
await reader.cancel("compact_response_too_large").catch(() => undefined);
return compactResponseTooLargeError();
}
const chunks: Uint8Array[] = [];
let total = 0;
try {
while (true) {
if (signal.aborted) {
await reader.cancel(signal.reason).catch(() => undefined);
return formatErrorResponse(499, "client_cancelled", "Client cancelled compact request");
}
const { done, value } = await reader.read();
if (done) break;
total += value.byteLength;
if (total > COMPACT_RESPONSE_MAX_BYTES) {
await reader.cancel("compact_response_too_large").catch(() => undefined);
return compactResponseTooLargeError();
}
chunks.push(value);
if (signal.aborted) {
// No reader is attached yet. Cancellation must not wait for a broken source's cleanup.
void upstream.body?.cancel(signal.reason).catch(() => undefined);
return formatErrorResponse(499, "client_cancelled", "Client cancelled compact request");
}
} catch {
if (!upstream.body) return new Response(null, { status: upstream.status, statusText: upstream.statusText, headers });
const declaredLength = Number(upstream.headers.get("content-length"));
if (Number.isFinite(declaredLength) && declaredLength > COMPACT_RESPONSE_MAX_BYTES) {
void upstream.body.cancel("compact_response_too_large").catch(() => undefined);
return compactResponseTooLargeError();
}
// Header admission has finished; only non-empty body chunks re-arm this deadline.
// The raw reader preserves bytes and cancels/releases without awaiting source cleanup.
const result = await readBoundedResponseBytes(upstream, {
signal,
maxBytes: COMPACT_RESPONSE_MAX_BYTES,
inactivityTimeoutMs: resolveStallTimeoutSec(stallTimeoutSec) * 1_000,
});
if (signal.aborted) return formatErrorResponse(499, "client_cancelled", "Client cancelled compact request");
if (result.oversized) return compactResponseTooLargeError();
return new Response(result.bytes, { status: upstream.status, statusText: upstream.statusText, headers });
} catch (error) {
if (signal.aborted) return formatErrorResponse(499, "client_cancelled", "Client cancelled compact request");
if (error instanceof DOMException && error.name === "TimeoutError") {
return Response.json({ error: {
message: "Compact response body stalled",
type: "upstream_stall_timeout",
code: "upstream_stall_timeout",
} }, { status: 504 });
}
return formatErrorResponse(502, "upstream_error", "Failed to read compact response");
}
const body = new Uint8Array(total);
let offset = 0;
for (const chunk of chunks) {
body.set(chunk, offset);
offset += chunk.byteLength;
}
return new Response(body, { status: upstream.status, statusText: upstream.statusText, headers });
}


Expand All @@ -526,6 +531,7 @@ export async function handleResponsesCompact(
if (typeof raw.model !== "string" || raw.model.length === 0) {
return formatErrorResponse(400, "invalid_request_error", "compaction request requires a model");
}
options.onRequestBodyRead?.();
Comment thread
lidge-jun marked this conversation as resolved.
// Correct the IDENTITY before routing, or the synthetic id does not route at all. Held in
// a local rather than written back to `raw.model`: assigning to the property widens it out
// of the `string` narrowing the guard above just established.
Expand Down Expand Up @@ -1037,7 +1043,7 @@ export async function handleResponsesCompact(
upstream.headers.get("x-codex-secondary-reset-at"),
upstream.headers.get("x-codex-tertiary-reset-at"),
].filter(Boolean);
const buffered = await bufferCompactResponse(upstream, req.signal);
const buffered = await bufferCompactResponse(upstream, req.signal, config.stallTimeoutSec);
const bufferedErrorText = buffered.ok
? ""
: await buffered.clone().text().catch(() => "");
Expand Down
48 changes: 27 additions & 21 deletions src/server/responses/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1534,7 +1534,9 @@ export function codexForwardTerminalOutcomeRecorder(
): ((status: ResponsesTerminalStatus, httpStatusOverride?: number) => void) | undefined {
if (!usesCodexForwardPoolAuth(authCtx, provider)) return undefined;
return (status, httpStatusOverride) => {
if (status === "incomplete") {
const quotaStatus = [httpStatusOverride, logCtx?.terminalHttpStatus]
.find(value => value === 429 || value === 402);
if (status === "incomplete" && quotaStatus === undefined) {
// Normal limit/content-filter/stall terminal — the account served the
// request. Don't penalize account health; record success to clear any
// prior soft-avoid so a healthy account isn't stuck avoided.
Expand All @@ -1559,7 +1561,7 @@ export function codexForwardTerminalOutcomeRecorder(
// the parent's terminalHttpStatus so the semantic status is not lost.
const outcome = status === "completed"
? 200
: (httpStatusOverride ?? logCtx?.terminalHttpStatus ?? 502);
: (quotaStatus ?? httpStatusOverride ?? logCtx?.terminalHttpStatus ?? 502);
recordCodexUpstreamOutcome(config, authCtx.accountId, outcome, {
threadId: authCtx.affinityKey,
fixedAccount: authCtx.fixedAccount,
Expand Down Expand Up @@ -2668,7 +2670,20 @@ export async function handleComboResponses(
attemptRetained = true;
};
let consumedChildFailure: ConsumedComboFailure | undefined;
const callbackGate = createChildPassthroughCallbackGate(options);
const callbackGate = createChildPassthroughCallbackGate({
...options,
onNativePassthroughTerminal: status => {
// A committed stream can acquire terminal metadata after preflight copied
// the child log. Publish it before the outer logger finalizes, but only
// through the gate: discarded attempts must never affect the parent.
// Undefined child fields must preserve metadata already inspected by WS.
if (childLog.terminalHttpStatus !== undefined) logCtx.terminalHttpStatus = childLog.terminalHttpStatus;
if (childLog.terminalIncompleteReason !== undefined) logCtx.terminalIncompleteReason = childLog.terminalIncompleteReason;
if (childLog.terminalErrorCode !== undefined) logCtx.terminalErrorCode = childLog.terminalErrorCode;
if (childLog.upstreamError !== undefined) logCtx.upstreamError = childLog.upstreamError;
options.onNativePassthroughTerminal?.(status);
},
});
let response: Response;
try {
const currentTargetProvider = pick.target.provider;
Expand Down Expand Up @@ -5359,12 +5374,9 @@ async function handleResponsesInner(
if (terminalBodyWillRecord) {
options.setTerminalOutcomeRecorder?.((status, httpStatusOverride) => {
terminalRecorder(status, httpStatusOverride);
if (status === "failed") {
const quotaFailureMessage = httpStatusOverride === 429 || httpStatusOverride === 402
|| logCtx.terminalHttpStatus === 429
|| logCtx.terminalHttpStatus === 402
? (httpStatusOverride ?? logCtx.terminalHttpStatus)
: undefined;
if (status === "failed" || status === "incomplete") {
const quotaFailureMessage = [httpStatusOverride, logCtx.terminalHttpStatus]
.find(value => value === 429 || value === 402);
if (!isFixedCodexAccount(authCtx) && quotaFailureMessage !== undefined) {
recordSubagentQuotaFailureForThreadSpawn(
req.headers,
Expand Down Expand Up @@ -5570,12 +5582,9 @@ async function handleResponsesInner(
const reportNativeTerminal = recordTerminalOutcomes
? (status: ResponsesTerminalStatus, httpStatusOverride?: number) => {
terminalRecorder?.(status, httpStatusOverride);
if (status === "failed") {
const quotaFailureMessage = httpStatusOverride === 429 || httpStatusOverride === 402
|| logCtx.terminalHttpStatus === 429
|| logCtx.terminalHttpStatus === 402
? (httpStatusOverride ?? logCtx.terminalHttpStatus)
: undefined;
if (status === "failed" || status === "incomplete") {
const quotaFailureMessage = [httpStatusOverride, logCtx.terminalHttpStatus]
.find(value => value === 429 || value === 402);
if (!isFixedCodexAccount(authCtx) && quotaFailureMessage !== undefined) {
recordSubagentQuotaFailureForThreadSpawn(
req.headers,
Expand Down Expand Up @@ -5663,12 +5672,9 @@ async function handleResponsesInner(
// client-cancel (no terminal seen) is finalized separately via consumeForInspection's onCancel.
const reportNativeTerminal = (status: ResponsesTerminalStatus, httpStatusOverride?: number) => {
terminalRecorder?.(status, httpStatusOverride);
if (status === "failed") {
const quotaFailureMessage = httpStatusOverride === 429 || httpStatusOverride === 402
|| logCtx.terminalHttpStatus === 429
|| logCtx.terminalHttpStatus === 402
? (httpStatusOverride ?? logCtx.terminalHttpStatus)
: undefined;
if (status === "failed" || status === "incomplete") {
const quotaFailureMessage = [httpStatusOverride, logCtx.terminalHttpStatus]
.find(value => value === 429 || value === 402);
if (!isFixedCodexAccount(authCtx) && quotaFailureMessage !== undefined) {
recordSubagentQuotaFailureForThreadSpawn(
req.headers,
Expand Down
Loading
Loading