fix(routing): recognize Kimi token-limit 400 as context overflow for combo fallback (#6637) (#6893)

combo.ts's isContextOverflow400() guard required the literal word
'context' in the 400 error body before letting a combo fall through to
the next target. Kimi's exact wording ('Your request exceeded model
token limit: 262144 (requested: 308458)') never says 'context', so the
guard misclassified it as a body-specific error and halted the whole
combo instead of trying the next (larger-context) target.

accountFallback.ts's CONTEXT_OVERFLOW_PATTERNS already recognized this
wording one layer below (via checkFallbackError -> shouldFallback), so
the two independently-maintained classifiers disagreed and the
stricter one won. Export CONTEXT_OVERFLOW_PATTERNS from
accountFallback.ts and reuse it inside combo.ts's
isContextOverflow400() so both layers share a single source of truth.

Regression test: tests/unit/repro-6637-kimi-token-limit.test.ts
(RED on unfixed code -> GREEN after the fix). Existing #4519 guard
tests (tests/unit/combo-param-validation-fallback-4519.test.ts) still
pass, including the negative case that a genuinely body-specific 400
is NOT misclassified as overflow.
This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-07-11 11:21:14 -03:00
committed by GitHub
parent a49ac1755d
commit afbd9361a7
4 changed files with 53 additions and 2 deletions

View File

@@ -0,0 +1 @@
- fix(routing): recognize Kimi-style "exceeded model token limit" 400 as context overflow so combo fallback continues to the next target (#6637)

View File

@@ -189,7 +189,10 @@ export const OAUTH_INVALID_TOKEN_SIGNALS = [
// Context overflow patterns — the prompt exceeds the model's maximum context length.
// Different providers phrase this differently. Used to decide whether a 400 error
// should trigger combo fallback (a different model may have a larger context window).
const CONTEXT_OVERFLOW_PATTERNS = [
// Exported so combo.ts's isContextOverflow400() guard (open-sse/services/combo.ts)
// can reuse this single source of truth instead of maintaining its own,
// independently-drifting pattern list (see issue #6637).
export const CONTEXT_OVERFLOW_PATTERNS = [
/\binput is too long\b/i,
/\binput too long\b/i,
/\bcontext.*(too long|exceeded|overflow|limit)/i,

View File

@@ -8,6 +8,7 @@
import {
checkFallbackError,
classifyLockoutReason,
CONTEXT_OVERFLOW_PATTERNS,
decayModelFailureCount,
formatRetryAfter,
getModelLockoutInfo,
@@ -663,7 +664,12 @@ export function isContextOverflow400(errorText) {
return (
/\bcontext.*(?:length_exceeded|too long|overflow|exceeded|window|limit)\b/i.test(errorText) ||
/exceeds.*context/i.test(errorText) ||
/your input exceeds/i.test(errorText)
/your input exceeds/i.test(errorText) ||
// Reuse accountFallback.ts's CONTEXT_OVERFLOW_PATTERNS (single source of truth)
// so wording like Kimi's "exceeded model token limit" — which never says the
// literal word "context" — is still recognized as an overflow/fallback-worthy
// 400 instead of halting the whole combo (issue #6637).
CONTEXT_OVERFLOW_PATTERNS.some((p) => p.test(errorText))
);
}
/** @param {string} errorText */

View File

@@ -0,0 +1,41 @@
// Repro probe for issue #6637: combo stops fallback after Kimi total token-limit 400.
//
// Observed provider response (Kimi, verbatim from the issue report):
// "Invalid request: Your request exceeded model token limit: 262144 (requested: 308458)"
//
// combo.ts's #2101 guard (handleComboChat) treats a 400 as a combo-halting
// "body-specific" error UNLESS isContextOverflow400() or isParamValidation400()
// recognizes it as a context/param overflow that should fall through to the next
// combo target. Kimi's exact wording ("... exceeded model token limit ...") does
// NOT contain the literal word "context", so isContextOverflow400() misses it even
// though accountFallback.ts's OWN CONTEXT_OVERFLOW_PATTERNS (used one layer below,
// to decide fallbackResult.shouldFallback) explicitly matches `/\btoken limit\b/i`
// and `/\bmax.*token/i`. The two classifiers disagree, and the stricter one wins,
// so the combo halts instead of trying the next (larger-context) target.
import assert from "node:assert/strict";
import test from "node:test";
import { isContextOverflow400, isParamValidation400 } from "../../open-sse/services/combo.ts";
const KIMI_ERROR_TEXT =
"Invalid request: Your request exceeded model token limit: 262144 (requested: 308458)";
test("#6637: Kimi's 'exceeded model token limit' 400 must be classified as context/token overflow (not body-specific)", () => {
// This is the exact predicate combo.ts checks before deciding to abort the
// whole combo instead of falling through to the next target (combo.ts ~L2038-2049):
// if (status === 400 && fallbackResult.shouldFallback &&
// !isContextOverflow400(errorText) && !isParamValidation400(errorText) && ...)
// -> "stopping combo"
//
// For the fallback to proceed to the next target, at least one of these must be true.
const isRecognizedAsOverflow = isContextOverflow400(KIMI_ERROR_TEXT) || isParamValidation400(KIMI_ERROR_TEXT);
assert.equal(
isRecognizedAsOverflow,
true,
`Expected Kimi's "exceeded model token limit" 400 to be classified as context/token ` +
`overflow so combo fallback continues to the next target, but neither ` +
`isContextOverflow400() nor isParamValidation400() matched it. This causes ` +
`handleComboChat's #2101 guard to treat it as a body-specific error and halt the ` +
`whole combo (bug #6637).`
);
});