From 993c6eb469c45d545a1c4eb3fa79f2256138ca48 Mon Sep 17 00:00:00 2001
From: snower
Date: Fri, 28 Aug 2026 16:30:25 +0700
Subject: [PATCH] feat(headroom): make the compression request timeout
configurable
The 3000 ms timeout on /v1/compress was fixed, so busy or slow machines
timed out often and sent the LLM an inconsistently compressed body,
hurting prompt caching. Add a headroomTimeoutMs setting, thread it from
the chat handler down to compressWithHeadroom, expose it in the Token
Saver dashboard, and normalize invalid values back to the 3000 ms default.
---
open-sse/handlers/chatCore.js | 4 +-
open-sse/rtk/headroom.js | 7 ++
.../dashboard/token-saver/TokenSaverClient.js | 22 +++++
src/lib/db/repos/settingsRepo.js | 1 +
src/sse/handlers/chat.js | 1 +
tests/unit/headroom.test.js | 95 +++++++++++++++++++
6 files changed, 128 insertions(+), 2 deletions(-)
diff --git a/open-sse/handlers/chatCore.js b/open-sse/handlers/chatCore.js
index 7fedca56..67e377ea 100644
--- a/open-sse/handlers/chatCore.js
+++ b/open-sse/handlers/chatCore.js
@@ -58,7 +58,7 @@ export function stripContinuityFields(body) {
return body;
}
-export async function handleChatCore({ body, modelInfo, credentials, log, onCredentialsRefreshed, onRequestSuccess, onDisconnect, clientRawRequest, connectionId, userAgent, apiKey, ccFilterNaming, rtkEnabled, headroomEnabled, headroomUrl, headroomCompressUserMessages, cavemanEnabled, cavemanLevel, ponytailEnabled, ponytailLevel, pxpipeEnabled, pxpipeMinChars, pxpipeTimeoutMs, pxpipeTransform, onPxpipeEvent, sourceFormatOverride, providerThinking }) {
+export async function handleChatCore({ body, modelInfo, credentials, log, onCredentialsRefreshed, onRequestSuccess, onDisconnect, clientRawRequest, connectionId, userAgent, apiKey, ccFilterNaming, rtkEnabled, headroomEnabled, headroomUrl, headroomCompressUserMessages, headroomTimeoutMs, cavemanEnabled, cavemanLevel, ponytailEnabled, ponytailLevel, pxpipeEnabled, pxpipeMinChars, pxpipeTimeoutMs, pxpipeTransform, onPxpipeEvent, sourceFormatOverride, providerThinking }) {
const { provider, model } = modelInfo;
const requestStartTime = Date.now();
// Stable per-session color so all lines of one CLI conversation share a tag
@@ -257,7 +257,7 @@ export async function handleChatCore({ body, modelInfo, credentials, log, onCred
// Headroom: optional external proxy compression; fail open if proxy is absent.
const headroomDiagnostics = {};
- const headroomStats = await compressWithHeadroom(translatedBody, { enabled: tokenSaverEnabled && headroomEnabled, url: headroomUrl, model: upstreamModel, format: finalFormat, compressUserMessages: headroomCompressUserMessages, diagnostics: headroomDiagnostics });
+ const headroomStats = await compressWithHeadroom(translatedBody, { enabled: tokenSaverEnabled && headroomEnabled, url: headroomUrl, model: upstreamModel, format: finalFormat, compressUserMessages: headroomCompressUserMessages, timeoutMs: headroomTimeoutMs, diagnostics: headroomDiagnostics });
const headroomLine = formatHeadroomLog(headroomStats);
const headroomSizeLine = formatHeadroomSizeLog(headroomDiagnostics);
if (headroomLine) {
diff --git a/open-sse/rtk/headroom.js b/open-sse/rtk/headroom.js
index bd6e3394..05004ac4 100644
--- a/open-sse/rtk/headroom.js
+++ b/open-sse/rtk/headroom.js
@@ -7,6 +7,12 @@ import {
const DEFAULT_TIMEOUT_MS = 3000;
+function normalizeTimeout(value) {
+ return typeof value === "number" && Number.isFinite(value) && value > 0
+ ? value
+ : DEFAULT_TIMEOUT_MS;
+}
+
function jsonBytes(value) {
try {
return new TextEncoder().encode(JSON.stringify(value) || "").length;
@@ -240,6 +246,7 @@ async function callCompress(url, messages, model, timeoutMs, compressUserMessage
// /v1/compress only understands OpenAI shape, so Claude bodies are translated
// to OpenAI, compressed, then translated back using 9Router's own translators.
export async function compressWithHeadroom(body, { enabled, url, model, format, compressUserMessages, timeoutMs = DEFAULT_TIMEOUT_MS, diagnostics = null } = {}) {
+ timeoutMs = normalizeTimeout(timeoutMs);
if (!enabled) {
setDiagnostic(diagnostics, "disabled");
return null;
diff --git a/src/app/(dashboard)/dashboard/token-saver/TokenSaverClient.js b/src/app/(dashboard)/dashboard/token-saver/TokenSaverClient.js
index bd899342..902f84ea 100644
--- a/src/app/(dashboard)/dashboard/token-saver/TokenSaverClient.js
+++ b/src/app/(dashboard)/dashboard/token-saver/TokenSaverClient.js
@@ -14,6 +14,7 @@ export default function TokenSaverClient() {
const [rtkEnabled, setRtkEnabledState] = useState(true);
const [headroomEnabled, setHeadroomEnabled] = useState(false);
const [headroomUrl, setHeadroomUrl] = useState("http://localhost:8787");
+ const [headroomTimeoutMs, setHeadroomTimeoutMs] = useState(3000);
const [headroomStatus, setHeadroomStatus] = useState({
installed: false,
running: false,
@@ -406,6 +407,13 @@ export default function TokenSaverClient() {
patchSetting({ pxpipeMinChars: next });
};
+ const handleHeadroomTimeoutBlur = () => {
+ const raw = Math.round(Number(headroomTimeoutMs));
+ const next = Number.isFinite(raw) && raw > 0 ? raw : 3000;
+ setHeadroomTimeoutMs(next);
+ patchSetting({ headroomTimeoutMs: next });
+ };
+
useEffect(() => {
const loadSettings = async () => {
try {
@@ -415,6 +423,7 @@ export default function TokenSaverClient() {
setRtkEnabledState(data.rtkEnabled !== false);
setHeadroomEnabled(!!data.headroomEnabled);
setHeadroomUrl(data.headroomUrl || "http://localhost:8787");
+ if (typeof data.headroomTimeoutMs === "number") setHeadroomTimeoutMs(data.headroomTimeoutMs);
setCodeAware(data.headroomCodeAware === true);
setKompress(data.headroomKompress !== false);
setCavemanEnabled(!!data.cavemanEnabled);
@@ -818,6 +827,19 @@ export default function TokenSaverClient() {
like http://headroom:8787.
+
+
Timeout (ms)
+
setHeadroomTimeoutMs(e.target.value)}
+ onBlur={handleHeadroomTimeoutBlur}
+ placeholder="3000"
+ className="font-mono text-sm"
+ />
+
+ Request timeout in milliseconds. Defaults to 3000 ms.
+
+
{headroomManaged ? (