Skip to content
Open
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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ All notable user-facing changes to MTPLX. The format is based on
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versions follow
[Semantic Versioning](https://semver.org/).

## [Unreleased]

### Fixed

- **A cancelled request keeps its cache outcome in the request log.** A streamed turn the client closed during decode was written with `session_cache_hit: false` and no `cached_tokens`, `cache_source` or `ttft_s`, so the dashboard showed it as a "MISS" and counted it against the hit rate, even when its prompt had been restored from the cache. The receipt now carries the cache outcome of the completed prefill (`session_cache_hit`, `cached_tokens`, `new_prefill_tokens`, `cache_source`, `cache_miss_reason`, the SSD fields) and `ttft_s`; when the request was cancelled before its prefill finished, these are `null` (unknown). The dashboard gives a cancelled row its own badge, such as "cancelled · client · 3m 0s", with the cache outcome beside it, and leaves cancelled requests out of the hit rate.

## [2.12.2] - 2026-10-03

### Fixed
Expand Down
4 changes: 3 additions & 1 deletion dashboard/src/components/CacheTiles.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { BigNumber, Card } from "./Card";
import { fmtNumber } from "../lib/utils";
import { useDashboardStore } from "../state/store";
import { countsTowardHitRate } from "../lib/requestCancel";

export function CumulativeCachedTokensTile() {
const lifetime = useDashboardStore((s) => s.lifetime);
Expand All @@ -21,7 +22,8 @@ export function CumulativeCachedTokensTile() {

export function HitRateGauge() {
const recent = useDashboardStore((s) => s.recent);
const window = recent.slice(-32);
// A cancelled request says nothing about the cache's hit rate.
const window = recent.filter(countsTowardHitRate).slice(-32);
const hits = window.filter((r) => r.session_cache_hit).length;
const ratio = window.length > 0 ? (hits / window.length) * 100 : 0;
const tone = ratio >= 70 ? "accent" : ratio >= 40 ? "warm" : "hot";
Expand Down
25 changes: 22 additions & 3 deletions dashboard/src/components/RequestLogTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Card } from "./Card";
import { useRecentRequests } from "../hooks/usePolling";
import { fmtNumber, fmtSeconds, fmtTokS, relativeTime, truncateMiddle } from "../lib/utils";
import { useDashboardStore, useFilteredRecent } from "../state/store";
import { cancellationOf } from "../lib/requestCancel";
import type { MetricsLatest } from "../lib/types";

export function RequestLogTable() {
Expand Down Expand Up @@ -122,7 +123,7 @@ function Td({
);
}

function Row({
export function Row({
row,
isOpen,
onToggle,
Expand All @@ -132,11 +133,23 @@ function Row({
onToggle: () => void;
}) {
const sessionId = row.session_id ?? "—";
const cacheBadge = row.session_cache_hit
? { label: "HIT", color: "text-[var(--accent)] bg-[var(--accent)]/10" }
const cancellation = cancellationOf(row);
const cacheBadge = cancellation
? {
label: cancellation.label,
color: "text-[var(--text-muted)] bg-[var(--text-muted)]/10",
tooltip: cancellation.tooltip as string | undefined,
}
: row.session_cache_hit
? {
label: "HIT",
color: "text-[var(--accent)] bg-[var(--accent)]/10",
tooltip: undefined,
}
: {
label: (row.cache_miss_reason ?? "MISS").toUpperCase(),
color: "text-[var(--accent-warm)] bg-[var(--accent-warm)]/10",
tooltip: undefined,
};
return (
<>
Expand Down Expand Up @@ -164,10 +177,16 @@ function Row({
<Td align="right">{fmtNumber(row.verify_calls)}</Td>
<Td>
<span
title={cacheBadge.tooltip}
className={`px-2 py-0.5 rounded-full text-[10px] uppercase tracking-wider ${cacheBadge.color}`}
>
{cacheBadge.label}
</span>
{cancellation?.cacheHint ? (
<span className="ml-2 text-[10px] text-[var(--text-muted)]">
{cancellation.cacheHint}
</span>
) : null}
</Td>
<Td align="right" highlight={false}>
<span className="text-[var(--text-muted)] text-xs">
Expand Down
89 changes: 89 additions & 0 deletions dashboard/src/lib/requestCancel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import type { MetricsLatest } from "./types";
import { fmtNumber, fmtSeconds } from "./utils";

export type Cancellation = {
/** Badge text, e.g. "cancelled · client · 3m 0s". */
label: string;
/** Why and after how long, in a sentence, plus the cache outcome. */
tooltip: string;
/** The cache outcome the request still had, e.g. "hit 92,636"; null when unknown. */
cacheHint: string | null;
};

// Short names for the badge; the tooltip carries the raw reason. The keys are
// the server's cancellation reasons (the cancel origins in openai.py).
const REASON_LABELS: Record<string, string> = {
client_disconnected: "client",
client_disconnect: "client",
stream_cancelled: "stream",
stream_teardown: "stream",
stream_stall: "stall",
stop_sequence: "stop",
early_tool_finish: "tool call",
hidden_tool_guard: "tool guard",
post_endpoint: "cancel call",
};

const REASON_SENTENCES: Record<string, string> = {
client_disconnected: "the client closed the connection",
client_disconnect: "the client closed the connection",
stream_stall: "the stream stalled",
stop_sequence: "a stop sequence ended the answer",
early_tool_finish: "the server finished the tool call early",
hidden_tool_guard: "the hidden-tool stream guard stopped it",
post_endpoint: "POST /v1/mtplx/cancel stopped it",
};

function humanize(value: string): string {
return value.replace(/_/g, " ");
}

/**
* The cache outcome a cancelled row still reports; null when the server did
* not know it yet. Older servers wrote `session_cache_hit: false` and no
* `cached_tokens` on every cancelled row, so without a token count the
* outcome is unknown as well.
*/
export function cancelledCacheHint(row: MetricsLatest): string | null {
if (row.cached_tokens === null || row.cached_tokens === undefined) return null;
if (row.session_cache_hit === true) {
const source = row.cache_source && row.cache_source !== "ram" ? ` ${row.cache_source}` : "";
return `hit${source} ${fmtNumber(row.cached_tokens)}`;
}
if (row.session_cache_hit === false) {
return row.cache_miss_reason ? `miss: ${humanize(row.cache_miss_reason)}` : "miss";
}
return null;
}

/**
* Describe a request that was cancelled before it finished, or null for one
* that ran to the end. Its row would otherwise fall back to the MISS badge,
* whatever the cache did.
*/
export function cancellationOf(row: MetricsLatest): Cancellation | null {
if (!row.request_cancelled) return null;
const reason = row.cancellation_reason ?? "";
const elapsed = row.cancellation_elapsed_s ?? row.request_elapsed_s;
const parts = ["cancelled"];
if (reason) parts.push(REASON_LABELS[reason] ?? humanize(reason));
if (typeof elapsed === "number" && Number.isFinite(elapsed)) parts.push(fmtSeconds(elapsed));
const why = reason ? (REASON_SENTENCES[reason] ?? humanize(reason)) : "unknown reason";
const after =
typeof elapsed === "number" && Number.isFinite(elapsed) ? ` after ${fmtSeconds(elapsed)}` : "";
const cacheHint = cancelledCacheHint(row);
const cacheLine =
cacheHint === null
? "Cache outcome unknown: the request ended before its prompt was read."
: `Cache: ${cacheHint}.`;
return {
label: parts.join(" · "),
tooltip: `Cancelled${after}: ${why}${reason ? ` (${reason})` : ""}.\n${cacheLine}\nNot counted in the hit rate.`,
cacheHint,
};
}

/** Rows that count toward a cache hit rate: requests that ran to the end. */
export function countsTowardHitRate(row: MetricsLatest): boolean {
return !row.request_cancelled;
}
7 changes: 7 additions & 0 deletions dashboard/src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,16 @@ export type MetricsLatest = {
repair_time_by_reject_depth_s?: Record<string, number> | null;
// ---- when the request finished (wall clock, seconds) ----
completed_at_s?: number | null;
// ---- cancellation (a request that did not run to the end) ----
request_cancelled?: boolean | null;
cancellation_reason?: string | null;
cancellation_elapsed_s?: number | null;
stream_cancelled_by_client?: boolean | null;
// ---- cache ----
// null on a cancelled request whose prefill had not finished: unknown.
session_cache_hit?: boolean | null;
cache_miss_reason?: string | null;
cache_source?: string | null;
session_restore_mode?: string | null;
session_id?: string | null;
context_len?: number | null;
Expand Down
105 changes: 105 additions & 0 deletions dashboard/tests/requestCancel.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { describe, expect, test } from "bun:test";
import { renderToStaticMarkup } from "react-dom/server";
import { Row } from "../src/components/RequestLogTable";
import { cancellationOf, countsTowardHitRate } from "../src/lib/requestCancel";
import type { MetricsLatest } from "../src/lib/types";

// The receipt of a streamed turn the client closed after 180 s, after its
// prompt was restored from RAM.
const cancelledHit: MetricsLatest = {
session_id: "c3ad535e-27bb-42b1-98d5-9e95ceaa8b6b",
prompt_tokens: 92819,
cached_tokens: 92636,
completion_tokens: 11968,
decode_tok_s: 67.9,
ttft_s: 2.1,
request_cancelled: true,
cancellation_reason: "client_disconnected",
cancellation_elapsed_s: 180.0005,
stream_cancelled_by_client: true,
session_cache_hit: true,
cache_source: "ram",
cache_miss_reason: null,
};

const html = (row: MetricsLatest) =>
renderToStaticMarkup(
<table>
<tbody>
<Row row={row} isOpen={false} onToggle={() => {}} />
</tbody>
</table>,
);

describe("cancellationOf", () => {
test("names the reason and the duration, and keeps the cache outcome", () => {
const c = cancellationOf(cancelledHit);
expect(c?.label).toBe("cancelled · client · 3m 0s");
expect(c?.tooltip).toContain("client_disconnected");
expect(c?.tooltip).toContain("the client closed the connection");
expect(c?.cacheHint).toBe(`hit ${(92636).toLocaleString()}`);
});

test("a miss keeps its reason as the hint", () => {
const c = cancellationOf({
...cancelledHit,
session_cache_hit: false,
cache_source: "none",
cache_miss_reason: "evicted",
});
expect(c?.cacheHint).toBe("miss: evicted");
});

test("a cancel before the prompt was read has no cache outcome", () => {
const c = cancellationOf({
...cancelledHit,
session_cache_hit: null,
cached_tokens: null,
cache_source: null,
});
expect(c?.cacheHint).toBeNull();
expect(c?.tooltip).toContain("Cache outcome unknown");
});

test("an unknown reason is shown as sent", () => {
expect(
cancellationOf({ request_cancelled: true, cancellation_reason: "some_new_origin" })?.label,
).toBe("cancelled · some new origin");
});

test("a request that ran to the end is not a cancellation", () => {
expect(cancellationOf({ session_cache_hit: false })).toBeNull();
expect(cancellationOf({ request_cancelled: false })).toBeNull();
});
});

describe("request log row", () => {
test("a cancelled row shows CANCELLED with the cache hint, not HIT or MISS", () => {
const out = html(cancelledHit);
expect(out).toContain("cancelled · client · 3m 0s");
expect(out).toContain("title=");
expect(out).toContain(`hit ${(92636).toLocaleString()}`);
expect(out).not.toContain(">HIT<");
expect(out).not.toContain("MISS");
});

test("an older receipt (session_cache_hit false, no cached_tokens) is CANCELLED, not a miss", () => {
const old = { ...cancelledHit, session_cache_hit: false, cached_tokens: undefined };
expect(cancellationOf(old)?.cacheHint).toBeNull();
const out = html(old);
expect(out).toContain("cancelled · client");
expect(out).not.toContain("MISS");
expect(out).not.toContain("miss");
});

test("an ordinary miss keeps its MISS badge", () => {
expect(html({ session_cache_hit: false })).toContain("MISS");
});
});

describe("hit rate", () => {
test("cancelled requests do not count", () => {
expect(countsTowardHitRate(cancelledHit)).toBe(false);
expect(countsTowardHitRate({ session_cache_hit: true })).toBe(true);
});
});
1 change: 0 additions & 1 deletion mtplx/dashboard/_static/assets/index-BTDFdIWu.css

This file was deleted.

1 change: 1 addition & 0 deletions mtplx/dashboard/_static/assets/index-C94IkU2d.css

Large diffs are not rendered by default.

Loading
Loading