补齐缓存写量与请求轮廓记账,便于核对提示缓存是否真的命中。
给用量行挂上轮次步骤和分段字数,统计页能看出缓存断在哪。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user