补齐缓存写量与请求轮廓记账,便于核对提示缓存是否真的命中。

给用量行挂上轮次步骤和分段字数,统计页能看出缓存断在哪。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-07 23:47:51 +08:00
co-authored by Cursor
parent d0274afb80
commit bb4f934b3d
13 changed files with 738 additions and 50 deletions
+114
View File
@@ -0,0 +1,114 @@
import { describe, expect, it } from "vitest";
import {
annotateCacheBreaks,
firstCacheBreak,
outlineRequest,
} from "../src/llm/context-outline.js";
const sample = [
"规则开头",
"",
"## 写入规则",
"固定规则",
"",
"## 更早的对话",
"",
"【用户】",
"第一句",
"",
"【助手】",
"回过",
"",
"## 当前产物(已写入黑板)",
"",
"### 设计.氛围 〔order=0〕",
"雨",
"",
"### 设计.角色 〔order=1〕",
"甲",
"",
"## 本轮注入",
"",
"### topology @ 3",
"#### task",
"做一件事",
"",
"## 本轮",
"",
"方案进度",
"",
"最后一轮用户输入",
"再改一下",
].join("\n");
describe("outlineRequest", () => {
it("按标题、对话轮次和最后一轮输入切开,不留下正文", () => {
const segments = outlineRequest([
{ role: "system", content: sample.slice(0, sample.indexOf("## 更早的对话")) },
{ role: "user", content: sample.slice(sample.indexOf("## 更早的对话")) },
]);
expect(segments.map((item) => item.tag)).toEqual([
"规则",
"写入规则",
"更早的对话",
"更早·用户",
"更早·助手",
"当前产物",
"设计.氛围",
"设计.角色",
"本轮注入",
"topology",
"task",
"本轮",
"最后一轮用户输入",
]);
expect(JSON.stringify(segments)).not.toContain("再改一下");
expect(JSON.stringify(segments)).not.toContain("雨");
const again = outlineRequest([
{ role: "system", content: sample.slice(0, sample.indexOf("## 更早的对话")) },
{ role: "user", content: sample.slice(sample.indexOf("## 更早的对话")) },
]);
expect(again.map((item) => item.hash)).toEqual(segments.map((item) => item.hash));
});
it("工具定义放在正文之前", () => {
const segments = outlineRequest(
[{ role: "user", content: "你好" }],
[
{
type: "function",
function: { name: "insert", description: "写", parameters: {} },
},
],
);
expect(segments.map((item) => item.tag)).toEqual(["工具", "用户"]);
});
});
describe("firstCacheBreak", () => {
it("标出第一处标签或哈希不同的位置", () => {
const prev = outlineRequest([{ role: "user", content: sample.slice(sample.indexOf("## 更早的对话")) }]);
const changed = sample.replace("雨", "雪");
const next = outlineRequest([{ role: "user", content: changed.slice(changed.indexOf("## 更早的对话")) }]);
const index = firstCacheBreak(prev, next);
expect(next[index]?.tag).toBe("设计.氛围");
expect(firstCacheBreak(prev, prev)).toBe(-1);
expect(firstCacheBreak(prev, prev.slice(0, 3))).toBe(-1);
expect(firstCacheBreak(prev.slice(0, 3), prev)).toBe(3);
});
});
describe("annotateCacheBreaks", () => {
it("按同会话时间比较,窗口里第一条算冷启动", () => {
const a = outlineRequest([{ role: "user", content: "第一" }]);
const b = outlineRequest([{ role: "user", content: "第二" }]);
const marks = annotateCacheBreaks([
{ id: "2", at: "2026-10-02T02:00:00.000Z", sessionId: "s", segments: b },
{ id: "1", at: "2026-10-02T01:00:00.000Z", sessionId: "s", segments: a },
{ id: "9", at: "2026-10-02T03:00:00.000Z", sessionId: "other", segments: a },
]);
expect(marks.get("1")).toMatchObject({ cold: true, matched: 0 });
expect(marks.get("2")).toMatchObject({ cold: false, breakAt: 0, tag: "用户", matched: 0 });
expect(marks.get("9")?.cold).toBe(true);
});
});
+14 -1
View File
@@ -44,6 +44,16 @@ describe("parseUsage", () => {
expect(usage?.cacheMissTokens).toBe(100);
});
it("parses cache write tokens when the provider returns them", () => {
const usage = parseUsage({
prompt_tokens: 100,
completion_tokens: 10,
total_tokens: 110,
cache_creation_input_tokens: 40,
});
expect(usage?.cacheWriteTokens).toBe(40);
});
it("returns undefined for empty usage", () => {
expect(parseUsage(null)).toBeUndefined();
expect(parseUsage({})).toBeUndefined();
@@ -52,8 +62,10 @@ describe("parseUsage", () => {
describe("TokenTrackingProvider", () => {
it("stores pending usage with cache fields on context", async () => {
let seenSession: string | undefined;
const inner: LlmProvider = {
async complete(): Promise<CompleteResult> {
async complete(_messages, options): Promise<CompleteResult> {
seenSession = options?.sessionId;
return {
content: "ok",
model: "test-model",
@@ -71,6 +83,7 @@ describe("TokenTrackingProvider", () => {
const ctx: LlmTrackingContext = { sessionId: "sess-1" };
const provider = new TokenTrackingProvider(inner, () => ctx);
await provider.complete([], { caller: "worker:write-rules" });
expect(seenSession).toBe("sess-1");
expect(ctx.pendingUsage).toMatchObject({
totalTokens: 15,