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
10 changes: 10 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,13 @@ 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.
1 change: 1 addition & 0 deletions scripts/test-layout/layout.json
Original file line number Diff line number Diff line change
Expand Up @@ -1021,6 +1021,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
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
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);
Comment thread
lidge-jun marked this conversation as resolved.
if (!isFixedCodexAccount(authCtx) && quotaFailureMessage !== undefined) {
recordSubagentQuotaFailureForThreadSpawn(
req.headers,
Expand Down
10 changes: 10 additions & 0 deletions structure/04_transports-and-sidecars.md
Original file line number Diff line number Diff line change
Expand Up @@ -1645,3 +1645,13 @@ dispatch. Selection revisions fence stale retries and reselection; request ident
actual committed account/key. Generic proactive selection is opt-in and preserves a healthy active
account, while reactive429 recovery remains enabled even with the pool off. Post-commit selection
events immediately invalidate dashboard roster state; see`05_gui-and-management-api.md`.


### 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.
1 change: 1 addition & 0 deletions tests/fixtures/test-layout-expected.json
Original file line number Diff line number Diff line change
Expand Up @@ -856,6 +856,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
Loading
Loading