fix(web): keep consultation conclusions across turns and surface cache hits (BUG-555, BUG-556)
Session history was silently clipped to the first 4000 characters of the last 12 messages, so follow-ups could not see timing or audit tables. Keep an append-only tail plus a checkpoint summary, retry overflow in the same request, and expose cache hit rate in admin usage. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -415,6 +415,7 @@ type Usage = {
|
||||
outputTokens: number;
|
||||
costMicrousd: number;
|
||||
durationMs: number | null;
|
||||
cacheReadTokens: number | null;
|
||||
createdAt: string;
|
||||
};
|
||||
|
||||
@@ -452,6 +453,11 @@ export function UsageResource() {
|
||||
render: (_, item) =>
|
||||
`${item.inputTokens.toLocaleString()} / ${item.outputTokens.toLocaleString()}`,
|
||||
},
|
||||
{
|
||||
title: "缓存读 tokens",
|
||||
dataIndex: "cacheReadTokens",
|
||||
render: (value: number | null) => value == null ? "—" : value.toLocaleString(),
|
||||
},
|
||||
{
|
||||
title: "成本",
|
||||
dataIndex: "costMicrousd",
|
||||
|
||||
@@ -27,7 +27,19 @@ const BANDS: DistributionBand[] = [
|
||||
type ModelRow = ModelPrice & { modelId: string; status: string; enabled: boolean };
|
||||
type PricingRow = { featureKey: string; modelTier: string; creditCost: number; status: string; enabled: boolean };
|
||||
type Product = { code: string; priceCents: number; enabled: boolean; status: string; entitlements: Array<{ featureKey: string; metadata?: { minuteLimit?: number | null; dayLimit?: number | null; billingLimit?: number | null } }> };
|
||||
type UsagePayload = { data: Array<{ featureKey: string; hasData: boolean; metrics: { costMicrousd: Metric; inputTokens: Metric; outputTokens: Metric; durationMs: Metric } }> };
|
||||
type CacheModelRow = {
|
||||
actualModelId: string | null;
|
||||
runsWithCache: number;
|
||||
hitRate: number | null;
|
||||
readTokens: number;
|
||||
writeTokens: number;
|
||||
noCacheTokens: number;
|
||||
cacheShare: number | null;
|
||||
};
|
||||
type UsagePayload = {
|
||||
data: Array<{ featureKey: string; hasData: boolean; metrics: { costMicrousd: Metric; inputTokens: Metric; outputTokens: Metric; durationMs: Metric } }>;
|
||||
cache?: { days7: CacheModelRow[]; days30: CacheModelRow[] };
|
||||
};
|
||||
|
||||
type LoadState = { models?: ModelRow[]; pricing?: PricingRow[]; products?: Product[]; usage?: UsagePayload; errors: string[] };
|
||||
|
||||
@@ -48,6 +60,10 @@ function errorText(error: unknown, fallback: string) {
|
||||
return error instanceof Error ? error.message : fallback;
|
||||
}
|
||||
|
||||
function formatPercent(value: number | null) {
|
||||
return value === null || !Number.isFinite(value) ? "—" : `${(value * 100).toFixed(1)}%`;
|
||||
}
|
||||
|
||||
export function PricingSimulator() {
|
||||
const [state, setState] = useState<LoadState>({ errors: [] });
|
||||
const [usdToCny, setUsdToCny] = useState<number | null>(null);
|
||||
@@ -158,6 +174,36 @@ export function PricingSimulator() {
|
||||
<Col xs={24} md={8}><Statistic title="画像下可承载会员数" value={capacity ?? "—"} /></Col>
|
||||
</Row>
|
||||
</Card>
|
||||
|
||||
<Card title="缓存命中">
|
||||
<Typography.Paragraph type="secondary">
|
||||
只统计 `usage_ledger.metadata.cache` 有数据的运行。命中 = 缓存读 tokens > 0。两个窗口互不混算。
|
||||
</Typography.Paragraph>
|
||||
{([
|
||||
["近 7 天", state.usage?.cache?.days7 ?? []],
|
||||
["近 30 天", state.usage?.cache?.days30 ?? []],
|
||||
] as const).map(([title, rows]) => (
|
||||
<div key={title} style={{ marginBottom: 16 }}>
|
||||
<Typography.Text strong>{title}</Typography.Text>
|
||||
<Table<CacheModelRow>
|
||||
size="small"
|
||||
pagination={false}
|
||||
rowKey={(row, index) => `${title}-${row.actualModelId ?? "none"}-${index}`}
|
||||
dataSource={rows}
|
||||
locale={{ emptyText: "暂无缓存数据" }}
|
||||
columns={[
|
||||
{ title: "模型", dataIndex: "actualModelId", render: (value) => value ?? "—" },
|
||||
{ title: "运行数", dataIndex: "runsWithCache" },
|
||||
{ title: "命中率", dataIndex: "hitRate", render: formatPercent },
|
||||
{ title: "缓存占比", dataIndex: "cacheShare", render: formatPercent },
|
||||
{ title: "读", dataIndex: "readTokens", render: (value: number) => value.toLocaleString() },
|
||||
{ title: "写", dataIndex: "writeTokens", render: (value: number) => value.toLocaleString() },
|
||||
{ title: "未命中", dataIndex: "noCacheTokens", render: (value: number) => value.toLocaleString() },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</Card>
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user