Compare commits

...

4 Commits

Author SHA1 Message Date
diegosouzapw
3c90c929bf fix(logs): preserve standalone detail token labels 2026-08-10 08:09:41 -03:00
diegosouzapw
fdc59323bd fix(logs): keep detail rendering independent of next-intl 2026-08-10 08:08:16 -03:00
diegosouzapw
9612b0d909 test(logs): use project alias in cache token coverage 2026-08-10 07:24:26 -03:00
diegosouzapw
e6a4dfc72b feat(logs): show cache read and write token counts (#9620) 2026-08-10 07:19:39 -03:00
3 changed files with 185 additions and 0 deletions

View File

@@ -0,0 +1,2 @@
- Show cache-read and cache-write token counts in request log rows and details when providers
report them.

View File

@@ -95,6 +95,7 @@ const RequestLoggerV2 = forwardRef<RequestLoggerV2Handle, { initialSelectedId?:
(props, ref) => {
const { initialSelectedId } = props as any;
const t = useTranslations("requestLogger");
const tCache = useTranslations("cache");
const { emailsVisible } = useEmailPrivacyStore();
// Get translated status filters
@@ -1514,6 +1515,30 @@ const RequestLoggerV2 = forwardRef<RequestLoggerV2Handle, { initialSelectedId?:
<span className="text-emerald-700 dark:text-emerald-400">
{log.tokens?.out?.toLocaleString() || 0}
</span>
{log.tokens?.cacheRead != null && log.tokens.cacheRead > 0 && (
<>
<span className="mx-1 text-border">|</span>
<span className="text-text-muted">CR:</span>{" "}
<span
className="text-sky-700 dark:text-sky-400"
title={tCache("cachedTokensCol")}
>
{log.tokens.cacheRead.toLocaleString()}
</span>
</>
)}
{log.tokens?.cacheWrite != null && log.tokens.cacheWrite > 0 && (
<>
<span className="mx-1 text-border">|</span>
<span className="text-text-muted">CW:</span>{" "}
<span
className="text-amber-700 dark:text-amber-400"
title={tCache("cacheCreation")}
>
{log.tokens.cacheWrite.toLocaleString()}
</span>
</>
)}
{log.tokens?.compressed != null && log.tokens.compressed > 0 && (
<>
<span className="mx-1 text-border">|</span>

View File

@@ -0,0 +1,158 @@
// @vitest-environment jsdom
import React, { act } from "react";
import { createRoot, type Root } from "react-dom/client";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
vi.mock("next-intl", () => ({
useTranslations: (namespace?: string) => (key: string) =>
namespace === "cache"
? ({ cachedTokensCol: "Cache Read", cacheCreation: "Cache Write" }[key] ?? key)
: key,
}));
vi.mock("next/navigation", () => ({
useRouter: () => ({ push: vi.fn(), replace: vi.fn(), refresh: vi.fn() }),
}));
vi.mock("@/store/emailPrivacyStore", () => ({
default: () => ({ emailsVisible: true }),
}));
const RequestLoggerV2 = (await import("@/shared/components/RequestLoggerV2")).default;
const RequestLoggerDetail = (await import("@/shared/components/RequestLoggerDetail")).default;
let container: HTMLElement;
let root: Root;
const populatedLog = {
id: "log-cache",
status: 200,
method: "POST",
path: "/v1/chat/completions",
model: "gpt-cache",
provider: "openai",
timestamp: "2026-08-10T12:00:00.000Z",
duration: 1_000,
tokens: {
in: 1_000,
out: 250,
cacheRead: 800,
cacheWrite: 120,
reasoning: 50,
compressed: 20,
},
};
const noop = () => {};
async function render(component: React.ReactNode) {
await act(async () => {
root.render(component);
});
}
beforeEach(() => {
container = document.createElement("div");
document.body.appendChild(container);
root = createRoot(container);
});
afterEach(async () => {
await act(async () => {
root.unmount();
});
container.remove();
vi.unstubAllGlobals();
});
describe("request log cache token metrics (#9620)", () => {
it("renders cache read/write beside the existing row token metrics", async () => {
const emptyCacheLog = {
...populatedLog,
id: "log-no-cache",
model: "gpt-no-cache",
timestamp: "2026-08-10T11:59:00.000Z",
tokens: { ...populatedLog.tokens, cacheRead: null, cacheWrite: 0 },
};
vi.stubGlobal(
"fetch",
vi.fn(async (input: RequestInfo | URL) => {
const url = String(input);
if (url.startsWith("/api/usage/call-logs")) {
return Response.json([populatedLog, emptyCacheLog]);
}
if (url.startsWith("/api/provider-nodes")) return Response.json({ nodes: [] });
if (url.startsWith("/api/logs/detail")) return Response.json({ enabled: false });
return Response.json({});
})
);
await render(<RequestLoggerV2 />);
await act(async () => {
await Promise.resolve();
});
const row = Array.from(container.querySelectorAll("tbody tr")).find((candidate) =>
candidate.textContent?.includes("gpt-cache")
);
expect(row?.textContent).toContain("TI: 1,000");
expect(row?.textContent).toContain("TO: 250");
expect(row?.textContent).toContain("CR: 800");
expect(row?.textContent).toContain("CW: 120");
expect(row?.textContent).toContain("↓20");
const emptyRow = Array.from(container.querySelectorAll("tbody tr")).find((candidate) =>
candidate.textContent?.includes("gpt-no-cache")
);
expect(emptyRow?.textContent).toContain("TI: 1,000");
expect(emptyRow?.textContent).toContain("TO: 250");
expect(emptyRow?.textContent).not.toContain("CR:");
expect(emptyRow?.textContent).not.toContain("CW:");
});
it("distinguishes cache read from cache write in the detail view", async () => {
await render(
<RequestLoggerDetail
log={populatedLog}
detail={populatedLog}
loading={false}
debugEnabled={false}
onClose={noop}
onCopy={async () => true}
/>
);
const inputGroup = container.querySelector('[data-testid="token-group-input"]');
const outputGroup = container.querySelector('[data-testid="token-group-output"]');
expect(inputGroup?.textContent).toContain("Total In: 1,000");
expect(inputGroup?.textContent).toContain("Cache Read: 800");
expect(inputGroup?.textContent).toContain("Cache Write: 120");
expect(inputGroup?.textContent).toContain("Compressed:");
expect(outputGroup?.textContent).toContain("Total Out: 250");
expect(outputGroup?.textContent).toContain("Reasoning: 50");
});
it("handles historical null and zero cache values without inventing usage", async () => {
const emptyCacheLog = {
...populatedLog,
id: "log-no-cache",
tokens: { ...populatedLog.tokens, cacheRead: null, cacheWrite: 0 },
};
await render(
<RequestLoggerDetail
log={emptyCacheLog}
detail={emptyCacheLog}
loading={false}
debugEnabled={false}
onClose={noop}
onCopy={async () => true}
/>
);
const inputGroup = container.querySelector('[data-testid="token-group-input"]');
expect(inputGroup?.textContent).toContain("Cache Read: N/A");
expect(inputGroup?.textContent).toContain("Cache Write: 0");
expect(inputGroup?.textContent).toContain("Total In: 1,000");
});
});