* 前缀缓存实测探针(手动跑,不进 CI)。
*
* 用途:新网关进入 MEASURED 白名单前先实测;验证 MeowCode 的保活 ping 在真实
* 端点真的读回(read=0 说明保活在白写)。分类口径照 hermes 的
* cache_concurrency_probe.py(ideal / stuck / collapse),用来抓代理静默重排前缀。
*
* 用法(任一协议都走真实的 streamChatCompletion 序列化):
* MEOWCODE_PROBE_BASE_URL=https://api.example.com/v1 \
* MEOWCODE_PROBE_API_KEY=sk-... \
* MEOWCODE_PROBE_MODEL=claude-sonnet-4-5 \
* MEOWCODE_PROBE_PROTOCOL=anthropic-messages \
* pnpm exec tsx scripts/cache-probe.ts
*
* 流程:稳定前缀(≥2048 token)连发两次 → 追加一轮工具调用再发 → 原样再发
* (等价于保活 ping 的 max_tokens=1 重放)。没有 API key 直接跳过(exit 0),
* 任何一次该命中的请求 read=0 时 exit 1。
*/
import type { ChatMessage, ProviderConfig, TokenUsage, ToolDefinition } from "../src/agent/types.js";
import { streamChatCompletion } from "../src/agent/provider.js";
const baseUrl = process.env.MEOWCODE_PROBE_BASE_URL;
const apiKey = process.env.MEOWCODE_PROBE_API_KEY;
const model = process.env.MEOWCODE_PROBE_MODEL;
const protocol = (process.env.MEOWCODE_PROBE_PROTOCOL ?? "openai-chat-completions") as
| "openai-chat-completions"
| "openai-responses"
| "anthropic-messages";
if (!apiKey) {
console.log("[probe] MEOWCODE_PROBE_API_KEY is not set; skipping (exit 0).");
process.exit(0);
}
if (!baseUrl || !model) {
console.error("[probe] MEOWCODE_PROBE_BASE_URL and MEOWCODE_PROBE_MODEL are required.");
process.exit(1);
}
const STABLE_SYSTEM = Array.from(
{ length: 300 },
(_, index) =>
`Rule ${index + 1}: keep the response short, cite files by path, and never invent tool output.`,
).join("\n");
const tools: ToolDefinition[] = [
{
type: "function",
function: {
name: "read_file",
description: "Read a file.",
parameters: {
type: "object",
properties: { path: { type: "string" } },
required: ["path"],
},
},
},
];
const provider: ProviderConfig = {
id: "probe",
name: "Probe",
protocol,
baseUrl,
model,
apiKey,
promptCacheKey: "meowcode-probe:main",
};
type Sample = { label: string; usage?: TokenUsage };
async function send(label: string, messages: ChatMessage[], maxTokens?: number): Promise<Sample> {
let usage: TokenUsage | undefined;
try {
const result = await streamChatCompletion({
provider: maxTokens ? { ...provider, maxTokens } : provider,
messages,
tools,
onDelta: () => undefined,
onUsage: (next) => {
usage = next;
},
});
usage ??= result.usage;
} catch (error) {
console.error(`[probe] ${label} failed: ${error instanceof Error ? error.message : String(error)}`);
process.exitCode = 1;
}
return { label, usage };
}
function classify(sample: Sample, previous: Sample | undefined): string {
const read = sample.usage?.cacheReadTokens;
if (read === undefined) return "not_reported";
if (read === 0) return "no_hit";
const previousInput = previous?.usage?.inputTokens ?? 0;
if (previousInput > 0 && read >= previousInput * 0.8) return "ideal";
if (previousInput > 0 && read < previousInput * 0.5) return "collapse";
if (previous?.usage?.cacheReadTokens === read) return "stuck";
return "partial";
}
function report(sample: Sample, previous: Sample | undefined): void {
const usage = sample.usage;
const read = usage?.cacheReadTokens;
const write = usage?.cacheWriteTokens;
console.log(
`[probe] ${sample.label.padEnd(16)} input=${usage?.inputTokens ?? "?"} ` +
`read=${read ?? "n/a"} write=${write ?? "n/a"} class=${classify(sample, previous)}`,
);
}
const conversation: ChatMessage[] = [
{ role: "system", content: STABLE_SYSTEM },
{ role: "user", content: "Reply with OK." },
];
const samples: Sample[] = [];
const first = await send("baseline", conversation);
samples.push(first);
report(first, undefined);
const replay = await send("replay", conversation);
samples.push(replay);
report(replay, first);
conversation.push({
role: "assistant",
content: null,
tool_calls: [
{
id: "probe-call-1",
type: "function",
function: { name: "read_file", arguments: JSON.stringify({ path: "probe.txt" }) },
},
],
});
conversation.push({
role: "tool",
tool_call_id: "probe-call-1",
name: "read_file",
content: "probe tool output line\n".repeat(120),
});
const toolTurn = await send("tool_loop", conversation);
samples.push(toolTurn);
report(toolTurn, replay);
const toolReplay = await send("tool_replay", conversation);
samples.push(toolReplay);
report(toolReplay, toolTurn);
const keepalive = await send("keepalive_ping", conversation, 1);
samples.push(keepalive);
report(keepalive, toolReplay);
const misses = samples.slice(1).filter((sample) => !sample.usage?.cacheReadTokens);
if (misses.length) {
console.error(
`[probe] ${misses.map((sample) => sample.label).join(", ")} read no cached tokens: ` +
"端点可能不支持缓存、代理在重排前缀,或保活参数与主请求不一致。",
);
process.exitCode = 1;
} else {
console.log("[probe] cache reads confirmed on every post-write request.");
}