| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131 |
- import { describe, expect, it } from "vitest"
- import type { ChatMessage, RequestOverrides } from "./llm-providers"
- import {
- LlmRequestTraceCollector,
- MAX_LLM_REQUEST_CACHE_TRACES,
- buildLlmRequestPrefixDescriptor,
- isLlmRequestCacheTrace,
- type LlmRequestCacheTrace,
- } from "./llm-request-trace"
- import type { LlmConfig } from "@/stores/wiki-store"
- const config: LlmConfig = {
- provider: "openai",
- apiKey: "sk-must-not-be-persisted",
- model: "gpt-test",
- apiMode: "chat_completions",
- ollamaUrl: "",
- customEndpoint: "https://secret.example/v1",
- maxContextSize: 204_800,
- reasoning: { mode: "medium" },
- }
- function messages(dynamicRule: string, stableCore = "项目稳定核心"): ChatMessage[] {
- return [
- {
- role: "system",
- content: [
- { type: "text", text: "固定基础规则\n" },
- { type: "text", text: stableCore, cacheControl: true },
- { type: "text", text: `\n动态规则:${dynamicRule}` },
- ],
- },
- { role: "user", content: `任务:${dynamicRule}` },
- ]
- }
- const tools: NonNullable<RequestOverrides["tools"]> = [{
- type: "function",
- function: {
- name: "read_outline",
- description: "读取大纲",
- parameters: { type: "object", properties: {} },
- },
- }]
- describe("LLM request prefix fingerprint", () => {
- it("ignores task, chapter and Skill changes after the cache breakpoint", async () => {
- const first = await buildLlmRequestPrefixDescriptor(config, messages("写第 11 章并启用 Skill A"), {
- tools,
- toolChoice: "auto",
- reasoning: { mode: "medium" },
- })
- const second = await buildLlmRequestPrefixDescriptor(config, messages("分析第 229 章并启用 Skill B"), {
- tools,
- toolChoice: "auto",
- reasoning: { mode: "medium" },
- })
- expect(first.prefixFingerprint).toMatch(/^[a-f0-9]{64}$/)
- expect(second.prefixFingerprint).toBe(first.prefixFingerprint)
- expect(first.prefixEstimatedTokens).toBeGreaterThan(0)
- })
- it("changes for stable text, model, tool schema and reasoning changes", async () => {
- const base = await buildLlmRequestPrefixDescriptor(config, messages("动态"), {
- tools,
- toolChoice: "auto",
- reasoning: { mode: "medium" },
- })
- const variants = await Promise.all([
- buildLlmRequestPrefixDescriptor(config, messages("动态", "变化后的稳定核心"), { tools, toolChoice: "auto", reasoning: { mode: "medium" } }),
- buildLlmRequestPrefixDescriptor({ ...config, model: "gpt-other" }, messages("动态"), { tools, toolChoice: "auto", reasoning: { mode: "medium" } }),
- buildLlmRequestPrefixDescriptor(config, messages("动态"), { tools: [{ ...tools[0], function: { ...tools[0].function, description: "变化" } }], toolChoice: "auto", reasoning: { mode: "medium" } }),
- buildLlmRequestPrefixDescriptor(config, messages("动态"), { tools, toolChoice: "auto", reasoning: { mode: "high" } }),
- ])
- for (const variant of variants) {
- expect(variant.prefixFingerprint).not.toBe(base.prefixFingerprint)
- }
- })
- it("returns no fingerprint when no virtual or real breakpoint exists", async () => {
- await expect(buildLlmRequestPrefixDescriptor(config, [
- { role: "system", content: "普通系统提示" },
- { role: "user", content: "任务" },
- ])).resolves.toEqual({})
- })
- })
- function trace(index: number, fingerprint = "a".repeat(64)): LlmRequestCacheTrace {
- return {
- provider: "openai",
- model: "gpt-test",
- apiMode: "chat_completions",
- prefixFingerprint: fingerprint,
- startedAt: index * 1_000,
- finishedAt: index * 1_000 + 400,
- durationMs: 400,
- firstResponseMs: 120,
- inputTokens: 1_000,
- outputTokens: 100,
- cacheReadTokens: 800,
- cacheWriteTokens: 0,
- status: "success",
- }
- }
- describe("LLM request trace collector", () => {
- it("computes same-prefix start/idle gaps and caps snapshots at 32 requests", () => {
- const collector = new LlmRequestTraceCollector()
- for (let index = 0; index < MAX_LLM_REQUEST_CACHE_TRACES + 2; index += 1) {
- collector.record(trace(index))
- }
- const snapshot = collector.snapshot()
- expect(snapshot.requests).toHaveLength(MAX_LLM_REQUEST_CACHE_TRACES)
- expect(snapshot.omittedRequestCount).toBe(2)
- expect(snapshot.requests[0].startedAt).toBe(2_000)
- expect(snapshot.requests[1]).toMatchObject({ startGapMs: 1_000, idleGapMs: 600 })
- })
- it("stores only sanitized diagnostics and strictly rejects damaged traces", () => {
- const value = trace(1)
- expect(isLlmRequestCacheTrace(value)).toBe(true)
- expect(JSON.stringify(value)).not.toContain(config.apiKey)
- expect(JSON.stringify(value)).not.toContain(config.customEndpoint)
- expect(JSON.stringify(value)).not.toContain("项目稳定核心")
- expect(isLlmRequestCacheTrace({ ...value, status: "timeout" })).toBe(false)
- expect(isLlmRequestCacheTrace({ ...value, durationMs: -1 })).toBe(false)
- })
- })
|