kvguard 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +66 -3
- package/bin/kvguard.js +22 -0
- package/install.js +95 -0
- package/package.json +23 -6
package/README.md
CHANGED
|
@@ -1,3 +1,66 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
# kvguard
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/kvguard)
|
|
4
|
+
[](https://pypi.org/project/kvguard/)
|
|
5
|
+
[](https://github.com/tranhoangtu-it/kvguard/actions/workflows/ci.yml)
|
|
6
|
+
[](https://github.com/tranhoangtu-it/kvguard#license)
|
|
7
|
+
|
|
8
|
+
**Catch 1-token prompt-cache misses in real time — and heal them before the request leaves your machine.**
|
|
9
|
+
|
|
10
|
+
Prompt caches (Anthropic, OpenAI) and local KV caches (llama.cpp, Ollama, vLLM) only reuse work when a new prompt starts with *exactly* the same bytes as the last one. One rotating header, one timestamp, one reshuffled MCP tool list — and 30–80K tokens are billed or prefilled again. Nothing errors. You just pay more and wait longer.
|
|
11
|
+
|
|
12
|
+
kvguard is a local proxy + terminal UI that shows you **where** the prefix broke, **why**, and **what it cost** — and can rewrite requests so it doesn't break again.
|
|
13
|
+
|
|
14
|
+
## Quick start
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npx kvguard --heal -- claude # Claude Code
|
|
18
|
+
npx kvguard --heal --target http://127.0.0.1:8080 -- opencode # any agent → llama-server
|
|
19
|
+
npx kvguard attach # live TUI in a second terminal
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
The first run downloads the native binary for your platform from [GitHub Releases](https://github.com/tranhoangtu-it/kvguard/releases) and verifies its SHA-256 checksum. No Docker, no database, no account.
|
|
23
|
+
|
|
24
|
+
## What you see
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
Turn Branch / Slot Model Prefix Health Hit % Root Cause / Action
|
|
28
|
+
#04 main [slot 0] claude-sonnet-4-5 [█░░░░░░░░░] 12% ❌ BUST @ tok ~#418 (volatile header/id in system · system[0].text)
|
|
29
|
+
#05 main [slot 0] claude-sonnet-4-5 [██████████] 96% ⚡ HEALED (sorted 48 tools + stripped cch= billing header)
|
|
30
|
+
#06 sub-1 [slot 1] claude-haiku-4-5 [··········] – ◆ NEW BRANCH
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## What it catches
|
|
34
|
+
|
|
35
|
+
| Cause | Typical source | `--heal` |
|
|
36
|
+
|---|---|---|
|
|
37
|
+
| tools[] order shuffled | MCP servers starting asynchronously | ✅ sorted by name |
|
|
38
|
+
| schema key order changed | non-deterministic JSON serialization | ✅ RFC 8785 key order |
|
|
39
|
+
| volatile header in system | Claude Code `cch=` billing header on non-Anthropic backends | ✅ stripped |
|
|
40
|
+
| timestamp in prefix | `Current time: …` with seconds | ✅ pinned per session |
|
|
41
|
+
| chat-template drift | Qwen/DeepSeek dropping `<think>` from old turns | ✅ `--normalize-think-tags` |
|
|
42
|
+
| history message mutated | compaction, truncated tool output | 🔎 reported with JSON path |
|
|
43
|
+
| sub-agent evicting main KV slot | llama-server with parallel slots | ✅ main pinned to `id_slot 0` |
|
|
44
|
+
|
|
45
|
+
Sub-agents and parallel sessions get their own branch, so they are never reported as false busts.
|
|
46
|
+
|
|
47
|
+
## CI gate
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
npx kvguard --record trace.jsonl -- my-agent run-scenario
|
|
51
|
+
npx kvguard check trace.jsonl --fail-on-bust --min-hit-rate 0.95
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Prints a Markdown report and exits `1` when a request breaks its parent's prefix.
|
|
55
|
+
|
|
56
|
+
## Supported
|
|
57
|
+
|
|
58
|
+
Anthropic `/v1/messages` · OpenAI `/v1/chat/completions` and `/v1/responses` · llama.cpp `llama-server` · Ollama · any OpenAI-compatible server. Prebuilt for Linux (x86_64, arm64), macOS (Intel, Apple Silicon) and Windows (x86_64).
|
|
59
|
+
|
|
60
|
+
Also available as `uvx kvguard` / `pipx run kvguard` and as a [standalone binary](https://github.com/tranhoangtu-it/kvguard/releases).
|
|
61
|
+
|
|
62
|
+
**Full documentation, accuracy notes and design:** https://github.com/tranhoangtu-it/kvguard
|
|
63
|
+
|
|
64
|
+
## License
|
|
65
|
+
|
|
66
|
+
MIT OR Apache-2.0
|
package/bin/kvguard.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Runs the native kvguard binary, downloading it first when postinstall was skipped
|
|
3
|
+
// (e.g. `npm install --ignore-scripts`).
|
|
4
|
+
"use strict";
|
|
5
|
+
|
|
6
|
+
const fs = require("node:fs");
|
|
7
|
+
const { spawnSync } = require("node:child_process");
|
|
8
|
+
const { install, BINARY } = require("../install.js");
|
|
9
|
+
|
|
10
|
+
async function main() {
|
|
11
|
+
if (!fs.existsSync(BINARY)) {
|
|
12
|
+
await install();
|
|
13
|
+
}
|
|
14
|
+
const result = spawnSync(BINARY, process.argv.slice(2), { stdio: "inherit" });
|
|
15
|
+
if (result.error) throw result.error;
|
|
16
|
+
process.exit(result.status ?? 1);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
main().catch((err) => {
|
|
20
|
+
console.error(`kvguard: ${err.message}`);
|
|
21
|
+
process.exit(1);
|
|
22
|
+
});
|
package/install.js
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Downloads the kvguard binary for this platform from GitHub Releases, verifies its SHA-256
|
|
3
|
+
// checksum and unpacks it into ./native. Uses only Node built-ins and the system `tar`.
|
|
4
|
+
"use strict";
|
|
5
|
+
|
|
6
|
+
const crypto = require("node:crypto");
|
|
7
|
+
const fs = require("node:fs");
|
|
8
|
+
const https = require("node:https");
|
|
9
|
+
const os = require("node:os");
|
|
10
|
+
const path = require("node:path");
|
|
11
|
+
const { execFileSync } = require("node:child_process");
|
|
12
|
+
|
|
13
|
+
const REPO = "tranhoangtu-it/kvguard";
|
|
14
|
+
const TARGETS = {
|
|
15
|
+
"linux-x64": "x86_64-unknown-linux-musl",
|
|
16
|
+
"linux-arm64": "aarch64-unknown-linux-musl",
|
|
17
|
+
"darwin-x64": "x86_64-apple-darwin",
|
|
18
|
+
"darwin-arm64": "aarch64-apple-darwin",
|
|
19
|
+
"win32-x64": "x86_64-pc-windows-msvc",
|
|
20
|
+
};
|
|
21
|
+
const MAX_REDIRECTS = 5;
|
|
22
|
+
|
|
23
|
+
const NATIVE_DIR = path.join(__dirname, "native");
|
|
24
|
+
const BINARY = path.join(NATIVE_DIR, process.platform === "win32" ? "kvguard.exe" : "kvguard");
|
|
25
|
+
|
|
26
|
+
function assetName(version) {
|
|
27
|
+
const target = TARGETS[`${process.platform}-${process.arch}`];
|
|
28
|
+
if (!target) {
|
|
29
|
+
throw new Error(`kvguard has no prebuilt binary for ${process.platform}-${process.arch}`);
|
|
30
|
+
}
|
|
31
|
+
const ext = process.platform === "win32" ? "zip" : "tar.gz";
|
|
32
|
+
return `kvguard-v${version}-${target}.${ext}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function download(url, redirects = 0) {
|
|
36
|
+
return new Promise((resolve, reject) => {
|
|
37
|
+
https
|
|
38
|
+
.get(url, { headers: { "User-Agent": "kvguard-npm-installer" } }, (res) => {
|
|
39
|
+
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
40
|
+
res.resume();
|
|
41
|
+
if (redirects >= MAX_REDIRECTS) return reject(new Error(`too many redirects for ${url}`));
|
|
42
|
+
return resolve(download(new URL(res.headers.location, url).toString(), redirects + 1));
|
|
43
|
+
}
|
|
44
|
+
if (res.statusCode !== 200) {
|
|
45
|
+
res.resume();
|
|
46
|
+
return reject(new Error(`GET ${url} failed with HTTP ${res.statusCode}`));
|
|
47
|
+
}
|
|
48
|
+
const chunks = [];
|
|
49
|
+
res.on("data", (c) => chunks.push(c));
|
|
50
|
+
res.on("end", () => resolve(Buffer.concat(chunks)));
|
|
51
|
+
res.on("error", reject);
|
|
52
|
+
})
|
|
53
|
+
.on("error", reject);
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function install() {
|
|
58
|
+
const { version } = require("./package.json");
|
|
59
|
+
const asset = assetName(version);
|
|
60
|
+
const base = `https://github.com/${REPO}/releases/download/v${version}`;
|
|
61
|
+
const [archive, checksumFile] = await Promise.all([
|
|
62
|
+
download(`${base}/${asset}`),
|
|
63
|
+
download(`${base}/${asset}.sha256`),
|
|
64
|
+
]);
|
|
65
|
+
|
|
66
|
+
const expected = checksumFile.toString("utf8").trim().split(/\s+/)[0].toLowerCase();
|
|
67
|
+
const actual = crypto.createHash("sha256").update(archive).digest("hex");
|
|
68
|
+
if (expected !== actual) {
|
|
69
|
+
throw new Error(`checksum mismatch for ${asset}: expected ${expected}, got ${actual}`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "kvguard-"));
|
|
73
|
+
try {
|
|
74
|
+
const archivePath = path.join(tmp, asset);
|
|
75
|
+
fs.writeFileSync(archivePath, archive);
|
|
76
|
+
fs.mkdirSync(NATIVE_DIR, { recursive: true });
|
|
77
|
+
// bsdtar (Windows 10+, macOS) and GNU tar both read .tar.gz; bsdtar also reads .zip.
|
|
78
|
+
execFileSync("tar", ["-xf", archivePath, "-C", NATIVE_DIR], { stdio: "inherit" });
|
|
79
|
+
} finally {
|
|
80
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
81
|
+
}
|
|
82
|
+
if (process.platform !== "win32") fs.chmodSync(BINARY, 0o755);
|
|
83
|
+
return BINARY;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
module.exports = { install, BINARY };
|
|
87
|
+
|
|
88
|
+
if (require.main === module) {
|
|
89
|
+
install()
|
|
90
|
+
.then((bin) => console.log(`kvguard installed at ${bin}`))
|
|
91
|
+
.catch((err) => {
|
|
92
|
+
console.error(`kvguard: install failed: ${err.message}`);
|
|
93
|
+
process.exit(1);
|
|
94
|
+
});
|
|
95
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,23 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "kvguard",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "kvguard",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Local proxy & TUI that catches 1-token prompt/KV cache prefix misses and auto-heals requests for Anthropic, OpenAI, llama.cpp and Ollama.",
|
|
5
|
+
"license": "MIT OR Apache-2.0",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/tranhoangtu-it/kvguard.git"
|
|
9
|
+
},
|
|
10
|
+
"keywords": ["llm", "prompt-caching", "kv-cache", "proxy", "claude", "openai", "llama.cpp", "ollama"],
|
|
11
|
+
"bin": {
|
|
12
|
+
"kvguard": "bin/kvguard.js"
|
|
13
|
+
},
|
|
14
|
+
"scripts": {
|
|
15
|
+
"postinstall": "node install.js"
|
|
16
|
+
},
|
|
17
|
+
"files": ["bin/", "install.js"],
|
|
18
|
+
"engines": {
|
|
19
|
+
"node": ">=18"
|
|
20
|
+
},
|
|
21
|
+
"os": ["darwin", "linux", "win32"],
|
|
22
|
+
"cpu": ["x64", "arm64"]
|
|
23
|
+
}
|