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 src/combos/failover.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,16 @@ export function comboFailureDecision(
if (["origin_rejected", "context_length_exceeded", "invalid_request_error"].includes(error.code ?? "")) {
return "stop";
}
// A local input-admission refusal (#1524) says "this candidate cannot fit the request",
// not "the request is impossible". Hopping is exactly right: the next candidate may have a
// larger context window. Read it from the structured code OR the body text, because the
// generic classifier maps 413 to its own code and would otherwise swallow this signal.
// Upstream `context_length_exceeded` above still stops.
if (options?.code === "input_admission_refused"
|| error.code === "input_admission_refused"
|| message.includes("input_admission_refused")) {
Comment on lines +132 to +134

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 Honor the structured admission code before message inference

Even if the response is changed to preserve input_admission_refused, the real admission message still contains “context window,” so classifyError infers context_length_exceeded and the earlier stop branch returns before this new structured-code check runs. Consequently, comboFailureDecision(413, <real admission body>, { code: "input_admission_refused" }) still returns stop; check the authoritative structured code before the inferred context error and cover the real admission message in the regression test.

AGENTS.md reference: src/AGENTS.md:L24-L26

Useful? React with 👍 / 👎.

return "hop";
}
if ([401, 403, 404, 408, 429].includes(status) || status >= 500) return "hop";
if ([
"permission_denied",
Expand Down
7 changes: 6 additions & 1 deletion src/server/responses/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1969,9 +1969,14 @@ async function handleResponsesInner(
if (parsed._compactionRequest !== true) {
const inputAdmission = checkInputAdmission(parsed, route.provider, route.providerName, parsed.modelId);
if (!inputAdmission.admitted) {
// #1524: this is a LOCAL preflight refusal, not an upstream verdict. A policy or combo
// fallback must be able to skip this candidate and try one whose context window fits,
// instead of treating the first incompatible candidate as the end of the chain. The
// distinct code is what lets the fallback layer tell the two apart -- an upstream
// `context_length_exceeded` still stops, because retrying it elsewhere is guesswork.
return formatErrorResponse(
413,
"request_too_large",
"input_admission_refused",

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 the admission code in the emitted error

When this preflight rejects an oversized non-compaction request, formatErrorResponse passes the message through classifyError; because the message contains “context window,” the formatter emits code: "context_length_exceeded" rather than input_admission_refused. The combo and policy fallback layers therefore receive the existing stop code and never try the larger candidate. Preserve the explicit local-admission code in the serialized response and add focused coverage using the actual formatter or handler output rather than a hand-written error body.

AGENTS.md reference: src/AGENTS.md:L24-L26

Useful? React with 👍 / 👎.

`Estimated input (~${inputAdmission.estimatedTokens} tokens) is far past the context window `
+ `of ${parsed.modelId} (${inputAdmission.ceiling} tokens). Start a new session or choose a `
+ `model with a larger context window.`,
Expand Down
8 changes: 8 additions & 0 deletions tests/combos.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,14 @@ describe("combo failure policy and advancement", () => {
expect(comboFailureDecision(409, "conflict")).toBe("stop");
expect(comboFailureDecision(499, "client cancelled")).toBe("stop");
expect(comboFailureDecision(422, "invalid_api_key")).toBe("hop");
// #1524: a LOCAL input-admission refusal means "this candidate cannot fit the request",
// not "the request is impossible". The next candidate may have a larger context window,
// so the chain must continue instead of ending at the first incompatible target.
expect(comboFailureDecision(413, '{"code":"input_admission_refused"}')).toBe("hop");
// An UPSTREAM context verdict still stops: retrying that elsewhere is guesswork, and a
// generic 413 with no structured code keeps its existing conservative handling.
expect(comboFailureDecision(400, "context_length_exceeded")).toBe("stop");
expect(comboFailureDecision(413, "request too large")).toBe("stop");
});

test("failure clears the active sticky target without adding a success", () => {
Expand Down
Loading