mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-06 07:12:12 +03:00
fix(types): validate chat context estimation inputs (#9084)
Validated in local merge-train (devbox-vm-06-dev002) @ combined-tip (FAST gates: typecheck/complexity/cognitive/changelog/vitest — only pre-existing audit.test.ts flake). Evidence: /home/diegosouzapw/dev/proxys/OmniRoute/.claude/worktrees/merge-train-20260805-222248-suite.log
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { injectMemoryAndSkills } from "./chatCore/memorySkillsInjection.ts";
|
||||
import { resolveChatCoreRequestSetup } from "./chatCore/requestSetup.ts";
|
||||
import { buildFailureUsageRecord } from "./chatCore/failureUsage.ts";
|
||||
import { estimateFinalInputTokens } from "./chatCore/contextEstimation.ts";
|
||||
import { extractSystemRoleMessages } from "./chatCore/claudeSystemRole.ts";
|
||||
export { extractSystemRoleMessages } from "./chatCore/claudeSystemRole.ts";
|
||||
import { checkIdempotencyCache } from "./chatCore/idempotency.ts";
|
||||
@@ -1822,27 +1823,6 @@ export async function handleChatCore({
|
||||
// filtering is advisory and may preserve an all-incompatible pool; this is the
|
||||
// hard boundary that prevents a too-large prompt (or a negative token budget)
|
||||
// from reaching an OpenAI-compatible upstream such as NVIDIA NIM.
|
||||
const estimateFinalInputTokens = (requestBody: Record<string, unknown> | null | undefined) => {
|
||||
const adapted = requestBody
|
||||
? adaptBodyForCompression(requestBody as Record<string, unknown>).body
|
||||
: null;
|
||||
const messages =
|
||||
adapted?.messages ||
|
||||
requestBody?.contents ||
|
||||
requestBody?.request?.contents ||
|
||||
(Array.isArray(requestBody?.input)
|
||||
? requestBody.input
|
||||
: requestBody?.input && typeof requestBody.input === "object"
|
||||
? requestBody.input
|
||||
: []);
|
||||
return (
|
||||
estimateTokens(messages) +
|
||||
(Array.isArray(requestBody?.tools) ? estimateTokens(requestBody.tools) : 0) +
|
||||
estimateTokens(requestBody?.system) +
|
||||
estimateTokens(requestBody?.instructions)
|
||||
);
|
||||
};
|
||||
|
||||
let finalEstimatedInputTokens = estimateFinalInputTokens(body as Record<string, unknown>);
|
||||
// Reuse the already-resolved `contextLimit` (may have been narrowed to the
|
||||
// per-target combo window above, resolveComboContextLimit) instead of a bare
|
||||
|
||||
29
open-sse/handlers/chatCore/contextEstimation.ts
Normal file
29
open-sse/handlers/chatCore/contextEstimation.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { adaptBodyForCompression } from "../../services/compression/bodyAdapter.ts";
|
||||
import { estimateTokens } from "../../services/contextManager.ts";
|
||||
|
||||
type JsonRecord = Record<string, unknown>;
|
||||
|
||||
function asJsonRecord(value: unknown): JsonRecord | null {
|
||||
return value && typeof value === "object" && !Array.isArray(value) ? (value as JsonRecord) : null;
|
||||
}
|
||||
|
||||
export function estimateFinalInputTokens(requestBody: JsonRecord | null | undefined): number {
|
||||
const adapted = requestBody ? adaptBodyForCompression(requestBody).body : null;
|
||||
const nestedRequest = asJsonRecord(requestBody?.request);
|
||||
const messages =
|
||||
adapted?.messages ||
|
||||
requestBody?.contents ||
|
||||
nestedRequest?.contents ||
|
||||
(Array.isArray(requestBody?.input)
|
||||
? requestBody.input
|
||||
: requestBody?.input && typeof requestBody.input === "object"
|
||||
? requestBody.input
|
||||
: []);
|
||||
|
||||
return (
|
||||
estimateTokens(messages) +
|
||||
(Array.isArray(requestBody?.tools) ? estimateTokens(requestBody.tools) : 0) +
|
||||
estimateTokens(requestBody?.system) +
|
||||
estimateTokens(requestBody?.instructions)
|
||||
);
|
||||
}
|
||||
@@ -254,7 +254,7 @@ function extractImageTokens(node: unknown, seen: Set<unknown>): { node: unknown;
|
||||
* budget instead of measuring its base64 payload as raw text, then the
|
||||
* remainder of the structure is measured normally via the char/4 heuristic.
|
||||
*/
|
||||
export function estimateTokens(text: string | object | null | undefined): number {
|
||||
export function estimateTokens(text: unknown): number {
|
||||
if (!text) return 0;
|
||||
if (typeof text === "string") {
|
||||
return Math.ceil(text.length / CHARS_PER_TOKEN);
|
||||
|
||||
40
tests/unit/chatcore-context-estimation.test.ts
Normal file
40
tests/unit/chatcore-context-estimation.test.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import { estimateFinalInputTokens } from "../../open-sse/handlers/chatCore/contextEstimation.ts";
|
||||
import { estimateTokens } from "../../open-sse/services/contextManager.ts";
|
||||
|
||||
test("estimateFinalInputTokens counts nested request contents and auxiliary fields", () => {
|
||||
const contents = [{ role: "user", parts: [{ text: "nested Gemini request" }] }];
|
||||
const tools = [{ name: "lookup", description: "find a record" }];
|
||||
const body = {
|
||||
request: { contents },
|
||||
tools,
|
||||
system: "system prompt",
|
||||
instructions: 42,
|
||||
};
|
||||
|
||||
assert.equal(
|
||||
estimateFinalInputTokens(body),
|
||||
estimateTokens(contents) +
|
||||
estimateTokens(tools) +
|
||||
estimateTokens(body.system) +
|
||||
estimateTokens(body.instructions)
|
||||
);
|
||||
});
|
||||
|
||||
test("estimateFinalInputTokens ignores a malformed nested request and uses input", () => {
|
||||
const input = [{ role: "user", content: "fallback input" }];
|
||||
const body = {
|
||||
request: "not an object",
|
||||
input,
|
||||
};
|
||||
|
||||
assert.equal(estimateFinalInputTokens(body), estimateTokens(input));
|
||||
});
|
||||
|
||||
test("estimateTokens accepts the unknown JSON values it already serializes", () => {
|
||||
assert.equal(estimateTokens(true), 1);
|
||||
assert.equal(estimateTokens(42), 1);
|
||||
assert.equal(estimateTokens(undefined), 0);
|
||||
});
|
||||
Reference in New Issue
Block a user