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
28 changes: 17 additions & 11 deletions open-sse/handlers/chatCore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1312,6 +1312,14 @@ async function handleChatCoreInner({
// further down — see #8378 (context limit resolved by the combo was silently
// discarded because it only existed inside this `if` block).
let contextLimit = getTokenLimit(provider, effectiveModel);
const contextWindowChecksDisabled = areContextWindowChecksDisabled();
const effectiveInputCap = contextWindowChecksDisabled
? null
: toPositiveInteger(
resolveInputTokenCapForGate({ provider, model: effectiveModel }, { isCombo })
);
const getEffectiveCompactionCeiling = (limit: number) =>
effectiveInputCap != null ? Math.min(limit, effectiveInputCap) : limit;
// Strict model proofs cannot invoke auxiliary compression providers or prewarmers.
// This trusted in-process fence is never accepted from a client header/body.
if (!validationExecutorFence && body && Array.isArray(allMessages) && allMessages.length > 0) {
Expand Down Expand Up @@ -2009,6 +2017,7 @@ async function handleChatCoreInner({
}
}

const effectiveCompactionCeiling = getEffectiveCompactionCeiling(contextLimit);
const COMPRESSION_THRESHOLD = (
await import("@/lib/db/compression")
).getProactiveCompressionRatio();
Expand All @@ -2018,12 +2027,12 @@ async function handleChatCoreInner({
}
const threshold = Math.max(
1,
Math.floor((Math.max(1, contextLimit) - reservedTokens) * COMPRESSION_THRESHOLD)
Math.floor((Math.max(1, effectiveCompactionCeiling) - reservedTokens) * COMPRESSION_THRESHOLD)
);

log?.debug?.(
"CONTEXT",
`Checking compression: ${estimatedTokens} tokens vs ${threshold} threshold (${contextLimit} limit, ${reservedTokens} reserved)`
`Checking compression: ${estimatedTokens} tokens vs ${threshold} threshold (${effectiveCompactionCeiling} limit, ${reservedTokens} reserved)`
);

// Capture pre-compression body so translators can access original message
Expand All @@ -2045,7 +2054,7 @@ async function handleChatCoreInner({
// separately as the threshold's reserve), unlike the full-breakdown
// number the final guard rejects with — state the basis so the two
// lines can be read side by side without a decoder ring.
`Proactive compression triggered: ${estimatedTokens} message tokens > ${threshold} threshold (${contextLimit} limit, tools reserve ${reservedTokens})`
`Proactive compression triggered: ${estimatedTokens} message tokens > ${threshold} threshold (${effectiveCompactionCeiling} limit, tools reserve ${reservedTokens})`
);

// Adapt Responses `input[]` → messages so compressContext can run, then restore.
Expand Down Expand Up @@ -2111,15 +2120,17 @@ async function handleChatCoreInner({
const finalContextLimit = contextLimit;
const toolsReserve = Array.isArray(body?.tools) ? estimateTokens(body.tools) : 0;

const effectiveCompactionCeiling = getEffectiveCompactionCeiling(finalContextLimit);

// Last-resort compaction against the concrete input budget (not the 70% threshold).
// Covers cases where the proactive pass was skipped or still left the request oversized (#8560).
if (
reactiveContextCompactionEnabled &&
!nativeCodexPassthrough &&
finalEstimatedInputTokens >= finalContextLimit &&
finalEstimatedInputTokens >= effectiveCompactionCeiling &&
body
) {
const lastResortTarget = Math.max(1, finalContextLimit - toolsReserve - 1);
const lastResortTarget = Math.max(1, effectiveCompactionCeiling - toolsReserve - 1);
const lastResortAdapter = adaptBodyForCompression(body as Record<string, unknown>);
const lastResortResult = compressContext(lastResortAdapter.body, {
provider,
Expand Down Expand Up @@ -2151,18 +2162,13 @@ async function handleChatCoreInner({
const modelOutputCap = toPositiveInteger(
getExplicitModelOutputCap({ provider, model: effectiveModel })
);
const contextWindowChecksDisabled = areContextWindowChecksDisabled();
const outputBudget = enforceOutputTokenBudget(
body as Record<string, unknown>,
finalEstimatedInputTokens,
contextWindowChecksDisabled ? Number.MAX_SAFE_INTEGER : finalContextLimit,
targetFormat === FORMATS.CLAUDE && sourceFormat !== FORMATS.CLAUDE ? DEFAULT_MAX_TOKENS : 0,
modelOutputCap,
contextWindowChecksDisabled
? null
: toPositiveInteger(
resolveInputTokenCapForGate({ provider, model: effectiveModel }, { isCombo })
)
effectiveInputCap
);
if (outputBudget.ok === false) {
const exceededInputCap = outputBudget.maxInputTokens !== undefined;
Expand Down
137 changes: 137 additions & 0 deletions tests/unit/compaction-input-cap-alignment.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";

const TEST_DATA_DIR = fs.mkdtempSync(
path.join(os.tmpdir(), "omniroute-compaction-input-cap-alignment-")
);
process.env.DATA_DIR = TEST_DATA_DIR;

const core = await import("../../src/lib/db/core.ts");
const { setModelCapabilityOverride } = await import("../../src/lib/db/modelCapabilityOverrides.ts");
const { updateCompressionSettings } = await import("../../src/lib/db/compression.ts");
const { handleChatCore } = await import("../../open-sse/handlers/chatCore.ts");

const TEST_PROVIDER = "test-compaction-prov";
const TEST_MODEL = "test-compaction-model";
const ENV_CONTEXT_LIMIT = "CONTEXT_LENGTH_TEST_COMPACTION_PROV";

const originalFetch = globalThis.fetch;
const originalEnvContext = process.env[ENV_CONTEXT_LIMIT];

async function resetStorage() {
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
}

test.before(async () => {
await resetStorage();
// Window is 8000 tokens, but input-only cap is 4000 tokens
process.env[ENV_CONTEXT_LIMIT] = "8000";

await updateCompressionSettings({
enabled: true,
defaultMode: "off",
});

setModelCapabilityOverride(`${TEST_PROVIDER}/${TEST_MODEL}`, "max_input_tokens", 4000);

globalThis.fetch = async () =>
new Response(
JSON.stringify({
id: "chatcmpl-test",
object: "chat.completion",
created: Date.now(),
model: TEST_MODEL,
choices: [
{
index: 0,
message: { role: "assistant", content: "ok from upstream" },
finish_reason: "stop",
},
],
usage: { prompt_tokens: 3000, completion_tokens: 5, total_tokens: 3005 },
}),
{
status: 200,
headers: { "content-type": "application/json" },
}
);
});

test.after(async () => {
globalThis.fetch = originalFetch;
if (originalEnvContext === undefined) {
delete process.env[ENV_CONTEXT_LIMIT];
} else {
process.env[ENV_CONTEXT_LIMIT] = originalEnvContext;
}
await new Promise((r) => setTimeout(r, 200));
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
});

test("aligns reactive compaction ceiling to max_input_tokens so dead band [cap, window) auto-compacts instead of 400", async () => {
// Generate multi-turn history with ~5,500 estimated tokens (chars/4).
// Fits in window (8000) but exceeds input cap (4000).
// Without the fix, last-resort compaction skipped (5500 < 8000) and enforceOutputTokenBudget rejects with 400.
// With the fix, last-resort targets 4000, prunes older history, fits < 4000 and succeeds.
const turns = [];
for (let i = 0; i < 10; i++) {
turns.push({
role: "user",
content: `Turn ${i} user request: ` + "word ".repeat(250),
});
turns.push({
role: "assistant",
content: `Turn ${i} assistant reply: ` + "answer ".repeat(250),
});
}

const result = await handleChatCore({
body: {
model: TEST_MODEL,
messages: turns,
stream: false,
},
modelInfo: {
provider: TEST_PROVIDER,
model: TEST_MODEL,
extendedContext: false,
},
credentials: {
apiKey: "sk-test",
providerSpecificData: {
baseUrl: "https://test-compaction.example.test",
},
},
clientRawRequest: {
endpoint: "/v1/chat/completions",
body: {
model: TEST_MODEL,
messages: turns,
stream: false,
},
headers: new Headers({ accept: "application/json" }),
},
userAgent: "unit-test",
isCombo: false,
log: {
debug() {},
info() {},
warn() {},
error() {},
},
});

assert.equal(
result.success,
true,
`expected request to succeed via last-resort compaction to input cap, got error: ${
(result as { error?: string }).error
}`
);
});
Loading