diff --git a/src/shared/components/RequestLoggerDetail.tsx b/src/shared/components/RequestLoggerDetail.tsx index 840a8c3b65..ecb28345a9 100644 --- a/src/shared/components/RequestLoggerDetail.tsx +++ b/src/shared/components/RequestLoggerDetail.tsx @@ -451,12 +451,16 @@ export default function RequestLoggerDetail({ {tokenStats.compressed != null && tokenStats.compressed > 0 && (() => { - const fromTokens = tokenStats.totalIn + tokenStats.compressed; - const pct = Math.round((tokenStats.compressed / fromTokens) * 100); + const fromTokens = tokenStats.compressed + Math.max(0, tokenStats.totalIn); + const saved = Math.min(tokenStats.compressed, fromTokens); + const pct = + fromTokens > 0 + ? Math.max(0, Math.min(100, Math.round((saved / fromTokens) * 100))) + : 100; return ( - Compressed: {fromTokens.toLocaleString()} \u2192{" "} - {tokenStats.totalIn.toLocaleString()} (-{pct}%) + Compressed: {fromTokens.toLocaleString()} →{" "} + {Math.max(0, tokenStats.totalIn).toLocaleString()} ({pct}% saved) ); })()} diff --git a/tests/unit/request-log-detail-layout.test.ts b/tests/unit/request-log-detail-layout.test.ts index 8304923524..91f1fdbc74 100644 --- a/tests/unit/request-log-detail-layout.test.ts +++ b/tests/unit/request-log-detail-layout.test.ts @@ -127,6 +127,73 @@ test("request log detail labels OpenAI protocol variants explicitly", () => { assert.notEqual(responsesHtml.indexOf(">OpenAI-Responses<"), -1); }); +test("request log detail compression-summary badge shows positive saved%, never negative", () => { + // Regression: prior code used `(-{pct}%)` which produced literal "-100%" when the entire + // prompt was compressed (compressed=5286, totalIn=0). The fix clamps pct to [0, 100] and + // uses "(N% saved)" so the user-facing label is always positive. + const make = (tokensIn: number, tokensCompressed: number) => + renderToStaticMarkup( + React.createElement(RequestLoggerDetail, { + log: { + status: 200, + method: "POST", + path: "/v1/chat/completions", + timestamp: "2026-04-09T21:27:08.000Z", + duration: 1500, + provider: "openai-compatible-sp-openai", + sourceFormat: "openai-chat", + model: "gpt-5.4", + requestedModel: "openai-compatible-sp-openai/gpt-5.4", + cacheSource: "semantic", + tokens: { + in: tokensIn, + out: 42, + cacheRead: null, + cacheWrite: null, + reasoning: null, + compressed: tokensCompressed, + }, + }, + detail: { + tokens: { + in: tokensIn, + out: 42, + cacheRead: null, + cacheWrite: null, + reasoning: null, + compressed: tokensCompressed, + }, + }, + loading: false, + onClose: () => {}, + onCopy: async () => true, + }) + ); + + // Original bug repro: totalIn=0, compressed=5286 → previously rendered "(−100%)". + const fullyCompressed = make(0, 5286); + assert.match(fullyCompressed, /Compressed: 5,286 → 0 \(100% saved\)/); + assert.equal( + fullyCompressed.includes("(-100%)"), + false, + "literal '(-100%)' must never appear" + ); + assert.equal( + fullyCompressed.includes("\u2212100%"), + false, + "unicode minus + 100% must never appear" + ); + + // Half-compressed case must clamp cleanly inside the [0, 100] window. + const halfCompressed = make(2500, 2500); + assert.match(halfCompressed, /Compressed: 5,000 → 2,500 \(50% saved\)/); + assert.equal(halfCompressed.includes("-50%"), false); + + // Sanity: tiny input, tiny savings still rendered as a positive percentage. + const small = make(1000, 100); + assert.match(small, /Compressed: 1,100 → 1,000 \(9% saved\)/); +}); + test("request log detail follows the email visibility setting for accounts", () => { const props = { log: {