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 ? (