mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-04 06:12:10 +03:00
fix(translator): sanitize Read tool args from non-Anthropic models (#4451)
Integrated into release/v3.8.32
This commit is contained in:
committed by
GitHub
parent
bda88db555
commit
0225a0a02d
@@ -15,6 +15,7 @@ _In development — bullets added per PR; finalized at release._
|
||||
### 🐛 Fixed
|
||||
|
||||
- **fix(embeddings):** forward output dimensions to Gemini for consistent embedding dims. (thanks @nguyenha935)
|
||||
- **fix(translator):** sanitize Read tool args from non-Anthropic models to prevent retry loops. (thanks @GodrezJr2)
|
||||
- **fix(combo): round-robin members fail over faster under concurrency saturation via a configurable queue depth** — when a round-robin combo member was saturated, requests sat in the per-model semaphore's **unbounded** queue and only failed over to the next member after the full `queueTimeoutMs` (default 30s) elapsed — so a burst of agentic requests deep-queued one hot member instead of spilling to healthy ones. The per-model semaphore now accepts a bounded queue depth and emits `SEMAPHORE_QUEUE_FULL` once it is full (the round-robin loop already cascades on that code), so a configured low depth fails over immediately. A new `queueDepth` combo-config knob (global default / provider override / per-combo, default **20** for backward compatibility; **0** = never queue → fail over now) is exposed in Settings → Combo Defaults. ([#3872](https://github.com/diegosouzapw/OmniRoute/issues/3872) — thanks @KooshaPari)
|
||||
|
||||
---
|
||||
|
||||
@@ -27,16 +27,51 @@ function coerceToArray(v: unknown): unknown[] {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Claude Code's Read tool caps `limit` at 2000 lines per call. Non-Anthropic models
|
||||
// (GPT-5.5, DeepSeek …) occasionally emit absurd values (e.g. `limit: 25999999999999999`)
|
||||
// that Claude Code rejects, causing a retry loop that wastes tokens. Clamp here.
|
||||
const READ_MAX_LIMIT = 2000;
|
||||
|
||||
// `pages` is only meaningful for PDFs and only as `"N"` or `"N-M"` (1-based).
|
||||
// Reference: claude-code-tools docs + upstream decolua/9router#1144.
|
||||
function isValidPdfPagesArg(filePath: unknown, pages: unknown): boolean {
|
||||
return (
|
||||
typeof filePath === "string" &&
|
||||
filePath.toLowerCase().endsWith(".pdf") &&
|
||||
typeof pages === "string" &&
|
||||
/^\d+(?:-\d+)?$/.test(pages)
|
||||
);
|
||||
}
|
||||
|
||||
function sanitizeReadArgs(args: Record<string, unknown>): void {
|
||||
// Coerce numeric-string limit/offset (some non-Anthropic models stringify everything).
|
||||
if (typeof args.limit === "string" && /^\d+$/.test(args.limit)) {
|
||||
args.limit = Number(args.limit);
|
||||
}
|
||||
if (typeof args.offset === "string" && /^-?\d+$/.test(args.offset)) {
|
||||
args.offset = Number(args.offset);
|
||||
}
|
||||
|
||||
if (typeof args.limit === "number") {
|
||||
if (args.limit > READ_MAX_LIMIT) args.limit = READ_MAX_LIMIT;
|
||||
if (args.limit < 1) delete args.limit;
|
||||
}
|
||||
if (typeof args.offset === "number" && args.offset < 0) args.offset = 0;
|
||||
|
||||
if ("pages" in args && !isValidPdfPagesArg(args.file_path, args.pages)) {
|
||||
delete args.pages;
|
||||
}
|
||||
}
|
||||
|
||||
const TOOL_SHIMS: Record<string, ShimFn> = {
|
||||
// Claude Code Read accepts `pages` only for PDFs and rejects an empty string.
|
||||
// Some non-Anthropic models emit optional `pages: ""` for ordinary files.
|
||||
// Buffer and emit one cleaned JSON delta so the client never sees the bad field.
|
||||
// Claude Code Read rejects bad params and retries — wasting tokens with non-Anthropic
|
||||
// models that emit oversized limits, negative offsets, stringified numbers, or stray
|
||||
// `pages` on non-PDF files. Buffer and emit one cleaned JSON delta so the client never
|
||||
// sees the bad fields. See `sanitizeReadArgs` for the per-field rules.
|
||||
Read: (input) => {
|
||||
if (typeof input !== "object" || input === null || Array.isArray(input)) return input;
|
||||
const patched = { ...(input as Record<string, unknown>) };
|
||||
if (patched.pages === "" || (Array.isArray(patched.pages) && patched.pages.length === 0)) {
|
||||
delete patched.pages;
|
||||
}
|
||||
sanitizeReadArgs(patched);
|
||||
return patched;
|
||||
},
|
||||
submit_pr_review: (input) => {
|
||||
|
||||
@@ -75,6 +75,96 @@ test("applyToolCallShimToBuffer: Read removes empty pages but preserves valid ra
|
||||
assert.deepEqual(withValidPages, { file_path: "/tmp/a.pdf", pages: "1-5" });
|
||||
});
|
||||
|
||||
// Port of decolua/9router#1144: non-Anthropic models (GPT-5.5, DeepSeek …) sometimes
|
||||
// emit absurd Read-tool args (e.g. limit: 99999999999) that Claude Code rejects and
|
||||
// retries, wasting tokens. The shim clamps/normalizes those args before re-emitting.
|
||||
test("applyToolCallShimToBuffer: Read clamps limit to 2000 (non-Anthropic models)", () => {
|
||||
const out = JSON.parse(
|
||||
applyToolCallShimToBuffer(
|
||||
"Read",
|
||||
JSON.stringify({ file_path: "/etc/hosts", limit: 99999999999 })
|
||||
)
|
||||
);
|
||||
assert.equal(out.limit, 2000);
|
||||
});
|
||||
|
||||
test("applyToolCallShimToBuffer: Read drops non-positive limit", () => {
|
||||
const zero = JSON.parse(
|
||||
applyToolCallShimToBuffer("Read", JSON.stringify({ file_path: "/etc/hosts", limit: 0 }))
|
||||
);
|
||||
assert.equal("limit" in zero, false);
|
||||
|
||||
const negative = JSON.parse(
|
||||
applyToolCallShimToBuffer("Read", JSON.stringify({ file_path: "/etc/hosts", limit: -50 }))
|
||||
);
|
||||
assert.equal("limit" in negative, false);
|
||||
});
|
||||
|
||||
test("applyToolCallShimToBuffer: Read clamps negative offset to 0", () => {
|
||||
const out = JSON.parse(
|
||||
applyToolCallShimToBuffer("Read", JSON.stringify({ file_path: "/etc/hosts", offset: -5 }))
|
||||
);
|
||||
assert.equal(out.offset, 0);
|
||||
});
|
||||
|
||||
test("applyToolCallShimToBuffer: Read coerces numeric-string limit/offset", () => {
|
||||
const out = JSON.parse(
|
||||
applyToolCallShimToBuffer(
|
||||
"Read",
|
||||
JSON.stringify({ file_path: "/etc/hosts", limit: "100", offset: "5" })
|
||||
)
|
||||
);
|
||||
assert.equal(out.limit, 100);
|
||||
assert.equal(out.offset, 5);
|
||||
});
|
||||
|
||||
test("applyToolCallShimToBuffer: Read strips pages for non-PDF files", () => {
|
||||
const out = JSON.parse(
|
||||
applyToolCallShimToBuffer(
|
||||
"Read",
|
||||
JSON.stringify({ file_path: "/etc/hosts", pages: "1-3" })
|
||||
)
|
||||
);
|
||||
assert.equal("pages" in out, false);
|
||||
});
|
||||
|
||||
test("applyToolCallShimToBuffer: Read strips malformed pages even on PDFs", () => {
|
||||
const out = JSON.parse(
|
||||
applyToolCallShimToBuffer(
|
||||
"Read",
|
||||
JSON.stringify({ file_path: "/tmp/doc.pdf", pages: "abc" })
|
||||
)
|
||||
);
|
||||
assert.equal("pages" in out, false);
|
||||
});
|
||||
|
||||
test("applyToolCallShimToBuffer: Read accepts a single page on PDFs", () => {
|
||||
const out = JSON.parse(
|
||||
applyToolCallShimToBuffer(
|
||||
"Read",
|
||||
JSON.stringify({ file_path: "/tmp/doc.PDF", pages: "7" })
|
||||
)
|
||||
);
|
||||
assert.equal(out.pages, "7");
|
||||
});
|
||||
|
||||
test("applyToolCallShimToBuffer: Read combined absurd args from non-Anthropic model", () => {
|
||||
// Simulates the upstream issue exactly: GPT-5.5-style giant limit, negative offset,
|
||||
// and a stray empty-string pages on a non-PDF.
|
||||
const out = JSON.parse(
|
||||
applyToolCallShimToBuffer(
|
||||
"Read",
|
||||
JSON.stringify({
|
||||
file_path: "F:/repo/file.js",
|
||||
offset: -5,
|
||||
limit: 25999999999999999,
|
||||
pages: "",
|
||||
})
|
||||
)
|
||||
);
|
||||
assert.deepEqual(out, { file_path: "F:/repo/file.js", offset: 0, limit: 2000 });
|
||||
});
|
||||
|
||||
test("applyToolCallShimToBuffer: submit_pr_review with valid arrays preserved", () => {
|
||||
const raw = JSON.stringify({
|
||||
summary: "ok",
|
||||
|
||||
Reference in New Issue
Block a user