fix(api): stop rejecting long chat histories by default - #9494
Conversation
The structural admission guard rejected any /v1/chat/completions request with more than 800 messages with a terminal 413 (chat_history_too_large), before compression, translation, or provider dispatch, and without consulting any memory signal. That threshold was introduced as one deployment's local policy for a 16 GiB container running with --max-old-space-size=12288, and shipped as a universal default. It is not a universal property of a request: the same conversation is trivial on a large host and fatal in a small container. Three consequences: - It fires before OmniRoute's own compression pipeline, the component that exists to make oversized conversations servable, can run at all. - messages.length is measured after protocol translation, which expands one logical turn into several messages[] entries, so the capped metric is partly produced by OmniRoute itself. - 413 is terminal. A client has no retry that makes it succeed, unlike the retryable 503 the same module already returns for genuine backpressure. The cap is now opt-in: OMNIROUTE_CHAT_HARD_MAX_MESSAGES defaults to 0 (disabled), and a positive value keeps the previous behaviour unchanged for memory-constrained deployments that want a hard ceiling. Heap growth remains bounded for every request by the existing heavyweight admission lease (OMNIROUTE_CHAT_MAX_HEAVY_IN_FLIGHT) and by the heap-pressure shed in the chat handler, neither of which this change touches.
|
Closing: not yet validated on a live deployment. Will reopen after runtime verification. |
CI note: the 4 red checks are pre-existing on the base branch, not caused by this PR
1. The failing tests are byte-identical to those on an unrelated open PR. Comparing this PR's last completed run (
#9510 shows the same three shards red as well. 1b. The failures are deterministic, not flaky. The three shards were re-run on the same head. All three failed again with 2. None of the failing tests touch anything this PR changes. This branch's own commits (
So shard 3/4 is red around this change, not because of it. There is no import path from the changed middleware to the vision bridge, the IP filter, the provider validators, or the Qwen config. 3. The shard-3 failures trace to #8430 on the base branch. Every That throw was added by 4. "The base branch is green" is not evidence to the contrary. Base-branch CI runs report I've deliberately left these alone rather than fixing them inside this PR — #8430's test-fixture gap is a separate change and folding it in would mix two unrelated concerns. Happy to open a follow-up for the One note on scope overlap: |
20a4ab6
into
diegosouzapw:release/v3.8.50
Summary
admitChatStructure()rejects anyPOST /v1/chat/completionswhosemessagesarray is longer than 800 with a terminal413 chat_history_too_large. The rejection happens before compression, translation, and provider dispatch, and without consulting any memory signal. This PR makes that cap opt-in (default0= disabled) and leaves every other admission control untouched.Where the 800 came from
The threshold entered the codebase as one deployment's local operating policy. #8276 states it explicitly:
The originating incident (#7849) was a V8 OOM in a 16 GiB container running with
--max-old-space-size=12288. #8296 then shipped those deployment-specific numbers as universal defaults, so every OmniRoute instance — including ones with far more headroom, or far less — inherited a ceiling calibrated for a single environment.Why a message count is the wrong gate
messages.lengthis read after protocol translation. A Responses- or Anthropic-format request whoseinput[]holds a modest number of turns expands into many moremessages[]entries during translation, so a client can cross a limit it never approached in its own representation.413is terminal. There is no retry a client can perform that makes it succeed. The same module already has the right shape for capacity problems — a retryable503withRetry-After— and the message cap bypasses it.src/app/api/v1/chat/completions/route.ts;src/app/api/v1/responses/route.tsandsrc/app/api/v1/messages/route.tshave no admission wiring at all. The same process and the same heap are reachable through those routes with no message-count limit, so the cap does not close the hole it was added for while it does refuse legitimate traffic on the one route it covers.checkHeapPressureGuard()(open-sse/utils/heapPressure.ts, used byopen-sse/handlers/chatCore.ts) already auto-calibrates to 85% of the actual V8 heap ceiling and adapts across 1 GB / 2 GB / large hosts. The message cap consults none of it and is a fixed number regardless of available heap — the same class of mistake as the fixed 200 MB threshold that caused the v3.8.8 "resource pressure" outage documented in that module's own comment.Observed impact
On an instance with 15 GB RAM, no cgroup limit, no
--max-old-space-size, and ~1.66 GB RSS — nowhere near the OOM condition the cap was designed for — 81 requests over a ~2h45m window were rejected with this413. All of them arrived through a gateway that translates Responses-format traffic into/v1/chat/completions, i.e. the translation-inflation path in point 2. Rejected bodies were 819 KB–1.06 MB, well under the 50 MB byte cap; successful requests on the same route reached 652 KB.Change
CHAT_HARD_MAX_MESSAGESnow defaults to0, and the check is skipped when it is not positive:Deployments that want a hard ceiling set
OMNIROUTE_CHAT_HARD_MAX_MESSAGESto a positive value and get byte-identical previous behaviour, including the same413body andreason: "message_limit".Nothing else about admission changes. Large conversations are still classified heavyweight by
OMNIROUTE_CHAT_HEAVY_MESSAGE_COUNT/OMNIROUTE_CHAT_HEAVY_TOOL_COUNT/ the conservative token estimate, still require an atomic heavyweight lease, still get a retryable503+Retry-Afterwhen concurrency capacity is occupied, and are still subject to the 50 MB actual-byte cap and the heap-pressure shed. The concurrency bound plus the heap-pressure shed are what actually keep the allocation-heavy path from exhausting the heap, and both remain in force for every request.Related Issues
--max-old-space-size=12288)Validation
tests/unit/chat-body-admission.test.ts— 25/25 passPOST /v1/chat/completionsnow returns200through two independent providers (prompt_tokens10,129 and 5,974) where it previously returned413 chat_history_too_large. No413 chat_history_too_largein the service log since the deploy.npx prettier --check— the two changed TypeScript files report the same formatting deviations on the pristine base commit as they do with this patch applied (1 hunk in the middleware, 3 in the test file), so this change introduces none. There is no prettier gate in CI.npm run lint— cannot run in this environment (eslint-config-nextunavailable); deferred to CInpm run test:unit— deferred to CInpm run test:coverage— deferred to CITests Added Or Updated
tests/unit/chat-body-admission.test.ts:no history cap is enforced by default; long conversations are admitted: assertsCHAT_HARD_MAX_MESSAGES === 0and that a 5,000-message conversation is admitted through heavyweight capacity rather than rejected.an uncapped oversized conversation still yields to occupied heavyweight capacity: the same oversized conversation gets a retryable503+Retry-After: 1+chat_admission_busywhen the lease is held, proving backpressure still applies without the cap.maxMessages: 0 explicitly disables the history cap.an opt-in history cap still returns the structured compact-required 413and keeps asserting the413/chat_history_too_large/reason: "message_limit"contract via an explicitmaxMessages, so the opt-in path stays locked.Coverage Notes
src/shared/middleware/chatBodyAdmission.tsis the only production file changed; the new and retained tests cover both branches of the guard (cap disabled → admit, cap configured →413) plus the backpressure path.Reviewer Notes
This is deliberately scoped to the default value and the guard condition — it does not attempt to redesign structural admission, add heap-awareness to
admitChatStructure(), or extend admission to/v1/responsesand/v1/messages. Points 4 and 5 above describe real gaps but are separate changes.Suggested follow-ups, if wanted: wire admission into the other two chat-ingress routes so the OOM protection is actually process-wide, and let structural admission consult
HEAP_PRESSURE_THRESHOLD_MBso heavy-request classification scales with the live heap ceiling instead of fixed counts.