Skip to content

fix(sidecar): map provider input overflow to context_length_exceeded - #7352

Merged
lidge-jun merged 6 commits into
devfrom
fix/sidecar-error-mapping
Oct 19, 2026
Merged

lidge-jun merged 6 commits into
devfrom
fix/sidecar-error-mapping

Conversation

@lidge-jun

@lidge-jun lidge-jun commented Oct 19, 2026 •

Copy link
Copy Markdown
Owner

Summary

When an upstream answered HTTP 413 behind the web-search or image sidecar, the turn failed with a generic upstream_error (Provider error 413). Codex treats that as a retryable transport failure, so it resent the same oversized payload four times and never compacted. The main path has long mapped the same 413 to a terminal context_length_exceeded. This PR applies that mapping to both sidecars.

Before (streaming turn with web_search, relay answers 413 with a plain-text body): HTTP 413 {"error":{"message":"Provider error 413","type":"upstream_error","code":null}}, which Codex retries.
After: HTTP 200 SSE ending in response.failed with {"type":"invalid_request_error","code":"context_length_exceeded"} and retryable: false, the same frame the main path emits. Codex then compacts.

One classifier, two call sites. isSidecarContextOverflow in src/server/responses/context-overflow.ts decides for both loops:

  • HTTP 413 counts as an overflow by status alone, matching the main path.
  • A 400 or 422 counts only when it carries definite overflow wording. This reuses isDefiniteContextOverflow, now exported from src/combos/failover.ts, so the sidecar and combo failover agree on what an overflow looks like. Rate-window wording such as "too many tokens per minute" and an upstream rate_limit_exceeded code are excluded. The loops read that code from the bounded, display-safe error body.
  • An adapter's own context_length_exceeded event keeps its code. The web-search progress stream and both loops' adapter-terminal paths previously dropped it to a generic 502.
  • The local translation_buffer_limit is excluded, because compacting cannot shrink a buffer the proxy owns.

Where it applies:

  • First iteration (HTTP response): the loop classifies the failure while it still has the provider's status, wording, error code and replay verdict, and tags its JSON error response. sidecar-execution.ts maps only a tagged response to streamingContextOverflowResponse or jsonContextOverflowResponse, with the proxy-owned message so upstream body text is never echoed.
  • Later iterations (already streaming): both loops end the turn with sidecarContextOverflowEvent.

Decisions and their reasons:

  • Combo attempts keep their raw failure. core-combo reads it for comboFailureDecision and already maps a final 413 itself, so combo routing is unchanged.
  • Provider wording is kept mid-stream. A worded mid-stream verdict (a 400, a 422, or a 413 with definite overflow wording) keeps its status and wording next to the overflow code. Only a 413 without such wording gets the proxy message. Combo stream preflight reads that wording to decide whether a larger-context target can still take the turn. Replacing it would end the chain at the first target that was merely too small. This came out of review.
  • Non-replayable failures are left alone. A failure from a response marked non-replayable, or one answered with x-should-retry: false, keeps its own failure both before and during streaming, because the provider may already have run that turn. This keeps the existing contract in responses-web-search-thinking-signature-recovery.test.ts. An adapter event marked retryable: false under a different code is likewise left as the adapter reported it. When such a kept failure's wording would match the bridge's context-window heuristics, the in-stream event carries an explicit code so the wire never renames it context_length_exceeded.
  • The web-search mid-stream overflow frame carries no usage. The loop already reports that iteration's usage through onUsage before ending the turn, and attaching it to the event would count it twice. This matches the previous behavior.

Absorbed community PRs (cherry-picked with original authorship):

Related, already closed: #4112 (the main-path non-streaming 413) and #3675. I searched open issues for 413, Payload Too Large, sidecar and context_length_exceeded and found no open duplicates of #7316.

Closes #7316

Co-authored-by: Vadym O bolein95@gmail.com

Verification

  • New tests/responses/responses-sidecar-context-overflow.test.ts runs end to end through startServer with an openai-chat relay and covers both sidecars. Cases: streaming plain-text 413 and streaming worded 400 both become a terminal context_length_exceeded; an unrelated 400 and a rate_limit_exceeded 400 keep their own failure; non-streaming 413 keeps HTTP 413 with the classification. It also includes classifier unit cases, with negatives for 429, rate-window wording on a 400, rate_limit_exceeded, and translation_buffer_limit. Run against the unmodified source, every positive case failed; with the fix, all pass.
  • Mid-stream regressions, each failing before the fix:
    • tests/web-search/web-search-loop-error-usage.test.ts: a later iteration's 413, an adapter-coded overflow, a worded 400 or 413 that keeps its evidence, a non-replayable 413 (bare or worded) that is left alone, and a rate_limit_exceeded 400 that keeps its rate verdict. tests/images/loop.test.ts also covers a worded non-replayable 413.
    • tests/images/loop.test.ts: a later iteration's 413, and a runTurn adapter's own overflow verdict.
  • bun run typecheck: passes.
  • bun run structure:check, bun run privacy:scan, bun scripts/file-size-ratchet.ts: pass.
  • bun test on the touched and neighboring files (sidecar overflow, web-search loop/pacing/thinking recovery, images loop, translator budget, main-path context overflow, lab boundary): all pass.
  • bun run test:changed with the pinned Bun 1.4.3 runner: 34662 pass / 43 skip / 2 fail across 1544 files.
    • The thinking-recovery failure was real. It was fixed by the non-replayable guard above and now passes.
    • The other, resolveCodexRuntime > treats missing persisted and resolved versions as the same selection, fails identically on a clean origin/dev checkout on this machine. It depends on the local environment and is unrelated.
  • Not run locally: the full bun run test. test:changed already covered 1544 files through the changed modules' import graph, and the rest is left to CI.
  • test-layout-tooling "launches isolate tests through the pinned runner" fails identically on the base commit here because of a local runner-path detail. It is unrelated.
  • Four rounds of independent subagent review (gpt-6.1-sol). Every finding was fixed or is answered above.

Checklist

  • Scope stays focused and avoids unrelated cleanup.
  • Docs or release notes were updated when needed. (structure/transports/responses.md documents the sidecar overflow contract; there is no user-facing setting to document.)
  • Security-sensitive changes were reviewed for secrets, auth, and unsafe defaults. (No auth or credential surface. The overflow paths emit proxy-owned messages for bare 413s, and the tests assert that the upstream sentinel text never reaches the client.)

vadymhimself and others added 4 commits October 19, 2026 15:11
… loops

When an image-bridge or web-search loop iteration hit a full request-pacing
queue (or an exhausted key pool), the header-phase catch wrapped the
RequestPacingQueueOverloadError into LoopError(502, "Provider unreachable").
The caller saw a 502 upstream failure instead of the 429 overload answer
with Retry-After that every other route returns. Rethrow the overload like
the shared transport classifier already does, so the handler's
requestPacingOverloadResponse maps it.

Co-Authored-By: Claude Code <noreply@anthropic.com>
When an adapter raised `translation_buffer_limit` as an error event, the bridge
handed it to `terminateForTranslatorOverflow`, which ignored its argument and
synthesized a usage-less `response.failed`. A turn the provider had already
billed was therefore logged with zero usage and cost.

The overflow terminal now reports the event's usage through `onUsage` and on
the `response.failed` frame, as the ordinary error terminal already does. The
client-facing error is unchanged.

Co-Authored-By: Claude Code <noreply@anthropic.com>
An upstream 413 (or a 400/422 carrying a definite context-overflow message,
or an adapter's own context_length_exceeded event) behind the web-search or
image sidecar leaked out as a generic upstream failure, so Codex resent the
identical oversized payload through its retry budget instead of compacting.

One classifier in context-overflow.ts now serves both loops: the sidecar
boundary maps a first-iteration failure to the main path's streaming or JSON
overflow response, and both loops end a later iteration's overflow with the
same terminal event. Combo attempts keep their raw failure for the combo
layer, which already maps a final 413 itself.

Closes #7316
…flow

Review follow-up for #7316:
- A worded in-stream overflow keeps its status and provider wording beside
  context_length_exceeded, so combo stream preflight can still hop to a
  larger-context target; only a bare 413 gets the proxy-owned message.
- Rate-window wording ("too many tokens per minute") and an explicit
  rate_limit_exceeded code are never classified as an overflow.
- A first-iteration failure marked x-should-retry: false keeps its status
  and header instead of inviting a compacted resend.
- Document the sidecar overflow contract in structure/transports/responses.md.
@coderabbitai

coderabbitai Bot commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Note

Currently processing new changes in this PR. This may take a few minutes, please wait...

⚙️ Run configuration
  • Configuration used: Repository: lidge-jun/opencodex/.coderabbit.yaml
  • Review profile: ASSERTIVE
  • Plan: Advanced
  • Run ID: 1f4f507a-5327-420c-bd76-f9c9b3c1b4d9
📥 Commits

Reviewing files that changed from the base of the PR and between 1c5e963 and d1026ce.

📒 Files selected for processing: 15
  • scripts/test-layout/layout.json
  • src/bridge/sse.ts
  • src/combos/failover.ts
  • src/images/loop.ts
  • src/server/responses/context-overflow.ts
  • src/server/responses/sidecar-execution.ts
  • src/web-search/loop.ts
  • src/web-search/progress-stream.ts
  • structure/transports/responses.md
  • tests/adapters/translator-budget.test.ts
  • tests/fixtures/test-layout-expected.json
  • tests/images/loop.test.ts
  • tests/responses/responses-sidecar-context-overflow.test.ts
  • tests/web-search/web-search-loop-error-usage.test.ts
  • tests/web-search/web-search-pacing-deadline.test.ts
+---------------------------------------------------+
| Stealth mode activated. Bugs won't see me coming. |
+---------------------------------------------------+
  \
   \     /)/)
        ( '')
        /  |
      o(___)
✨ Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

Copy link
Copy Markdown
Contributor

✅ Deterministic PR hygiene checks passed.

Round-2 review follow-up for #7316:
- The loops classify each failure with its status, wording, upstream
  error.code and non-replayable mark, and tag a qualifying first-iteration
  JSON failure; the sidecar boundary maps only tagged responses instead of
  re-parsing the loop's envelope.
- A non-replayable failure (HTTP or adapter event) keeps its own verdict
  mid-stream too, so it never invites a compacted resend.
- An upstream rate_limit_exceeded code survives into classification, so a
  "too many tokens" rate refusal is not read as an overflow.
- A 413 with definite overflow wording keeps that wording for combo
  failover; only a status-only 413 gets the proxy message.
@github-actions github-actions Bot added the bug Something isn't working label Oct 19, 2026
Round-3 review follow-up for #7316: when the sidecar classifier keeps a
failure (non-replayable, or an upstream rate_limit_exceeded) but its wording
would match the bridge's context-window heuristics, the in-stream event now
carries an explicit code, so classifyError cannot rename it
context_length_exceeded on the wire.
@lidge-jun
lidge-jun marked this pull request as ready for review October 19, 2026 06:42
@lidge-jun
lidge-jun requested a review from Ingwannu as a code owner October 19, 2026 06:42
@lidge-jun
lidge-jun merged commit 0a995db into dev Oct 19, 2026
27 of 30 checks passed
@lidge-jun
lidge-jun deleted the fix/sidecar-error-mapping branch October 19, 2026 06:42
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 19, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-19T06:47:04.264688Z d1026ce Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d1026ce9f3

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

export function sidecarLoopOverflowEvent(
failure: SidecarLoopFailure,
): Extract<AdapterEvent, { type: "error" }> | undefined {
if (failure.nonReplayable) return undefined;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve replay refusal when rebuilding the first failure

When a sidecar's first response is marked non-replayable, returning here prevents overflow mapping but does not preserve that verdict on the new jsonError response subsequently created by either loop. That replacement has neither the non-replayable WeakSet marker nor x-should-retry: false, so a marked 413/400/422 (or a marked transport failure carrying an upstream context_length_exceeded code) reaches the client as an ordinary retryable HTTP error and Codex can resend a turn the provider may already be executing. Propagate the replay-refusal marker/header when rebuilding this first-iteration response, rather than only declining the overflow conversion.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants