alxsuv/pino
GitHub: alxsuv/pino
一个专为 Claude Code 设计的本地反向代理,通过自动注入缓存断点、升级 TTL 和裁剪冗余工具,将 Anthropic API 的输入成本降低约 90%。
Stars: 3 | Forks: 1

# Pino proxy
[](./LICENSE)
[](https://nodejs.org)
[](./package.json)
[](https://github.com/alxsuv/pino/stargazers)
[](#savings-math)
一个运行在 `api.anthropic.com` 前端的微型本地 HTTP 反向代理。它会原封不动地转发所有请求到上游,**除了**针对 `/v1/messages` 的请求,它会选择性进行以下操作:
- **自动注入 prompt-cache 断点**,作用于 Claude Code 未缓存的数据块 —— 最重要的是 `tools`(约 24k tokens,开箱即有 0 个断点)以及 `messages[0]` 中的静态提醒块。
- **将 TTL 升级至 1 小时**,作用于不常变化的可缓存内容。Claude Code 确实在 system prompt 上设置了 `cache_control: {type: "ephemeral"}`,但**省略了 `ttl` 字段** —— 这会静默回退到新的 5 分钟默认值,因此一次需要几分钟阅读的深思熟虑的交互,就可能在下一个交互时超出缓存窗口并重新支付 1.25 倍的写入费用。该代理将每个 ephemeral 断点重写为 `ttl: "1h"`(除了滚动尾部,它被刻意保留为 5 分钟,以免您为每个交互都在移动的断点支付过高的 2.0 倍写入倍数)。
- **丢弃未使用的工具**,并从系统提醒中清除它们的名称,从而缩减请求大小。
- **剥离 ANSI 转义码**,使工具结果中的终端输出能被干净地缓存。
- **使 system prompt 可编辑** —— 请求体的任何部分都可以通过用户自定义的 `transform(body)` 钩子进行重写。没有内置的 system prompt 修改开箱即用;该钩子是扩展点。
主要为 **Claude Code** 设计。在 Claude Code 中,相同的约 24k token 工具目录在每个交互中未缓存发送,而约 8k token 的系统提示使用的是脆弱的 5 分钟计时器 —— Claude Code 的 `cache_control` 省略了 `ttl` 并静默回退到 5 分钟默认值,单次深思熟虑的交互(长生成、慢工具调用、用户阅读输出)就足以让缓存过期。
### 真实会话证明
通过 pino-proxy 代理的两个连续 API 请求。数字是 Anthropic API 响应中原始的 `usage` 字段:
```
# Turn N # Turn N+1
input_tokens: 6 input_tokens: 6
cache_read_input_tokens: 83_324 cache_read_input_tokens: 83_910 ← the previous tail, cached
cache_creation: cache_creation:
ephemeral_5m_input_tokens: 586 ephemeral_5m_input_tokens: 252 ← only the moving delta written
ephemeral_1h_input_tokens: 0 ephemeral_1h_input_tokens: 0
output_tokens: 195 output_tokens: 802
```
此配对的 Opus 定价:**使用代理约 $0.34 vs 不使用约 $2.60**(输入 + 输出,根据上面的 `usage` 数字计算) —— 5 分钟的滚动尾部加上对 tools / system / reminders 的 1 小时缓存发挥了主要作用。有关详细明细,请参阅[节省金额计算](#savings-math)。
## 快速开始
### 1. 克隆并安装
```
git clone https://github.com/alxsuv/pino
cd pino
```
不需要 `npm install` —— 零运行时依赖。需要 Node >= 20。
### 2. 启动代理
**Linux / macOS (bash/zsh):**
```
# 在 :8787 上纯透传
npm start
# 典型 dev 设置:auto-cache + transforms + logs
AUTO_CACHE=1 \
TRANSFORM_FILE=./src/transforms/default.js \
DROP_TOOLS=NotebookEdit,CronCreate,CronDelete,CronList,RemoteTrigger,PushNotification,Monitor \
LOG_BODIES=1 \
npm start
# 或直接调用 bin
node bin/pino-proxy.js
```
**Windows (PowerShell):**
```
# 在 :8787 上纯透传
npm start
# 典型 dev 设置:auto-cache + transforms + logs
$env:AUTO_CACHE=1
$env:TRANSFORM_FILE="./src/transforms/default.js"
$env:DROP_TOOLS="NotebookEdit,CronCreate,CronDelete,CronList"
$env:LOG_BODIES=1
npm start
# 或直接调用 bin
node bin/pino-proxy.js
```
**Windows (cmd.exe):**
```
:: pure pass-through on :8787
npm start
:: typical dev setup: auto-cache + transforms + logs
set AUTO_CACHE=1
set TRANSFORM_FILE=./src/transforms/default.js
set DROP_TOOLS=NotebookEdit,CronCreate,CronDelete,CronList
set LOG_BODIES=1
npm start
```
### 3. 将您的客户端指向它
**Linux / macOS:**
```
export ANTHROPIC_BASE_URL=http://127.0.0.1:8787
```
**Windows (PowerShell):**
```
$env:ANTHROPIC_BASE_URL="http://127.0.0.1:8787"
```
**Windows (cmd.exe):**
```
set ANTHROPIC_BASE_URL=http://127.0.0.1:8787
```
## 如何验证 (确凿证据)
您不必只听我的一面之词。您可以在 60 秒内看到 Claude Code 实际发送的内容:
1. **以直通模式启动代理**(捕获日志但暂不改变任何内容):
LOG_BODIES=1 npm start
2. **在 Claude Code 中运行任意命令**(例如,`claude "hi"`)。
3. **检查捕获的请求**:
# 1. Tools:零个 cache_control 条目。约 24k token 的工具目录未被缓存。
jq '[.body.tools[] | select(.cache_control)] | length' logs/*.req.json
# 2. System:cache_control 已设置,但缺少 ttl → 静默默认为 5 分钟。
jq '.body.system[]? | select(.cache_control) | {len: (.text|length), cache_control}' logs/*.req.json
预期输出:tools 计数为 `0`,并且每个 system `cache_control` 都是纯粹的 `{"type":"ephemeral"}`,没有 `ttl` 键。这种纯粹的形式意味着 **5 分钟** —— 这个时间长得足以让一次深思熟虑的交互使缓存过期,并在下一个交互时强制重写。
4. **现在,启用修复**:
使用 `AUTO_CACHE=1` 重启代理:
AUTO_CACHE=1 LOG_BODIES=1 npm start
对新的 `logs/*.req.json` 重新运行相同的 `jq` 查询,您将看到断点被添加到 `tools`,每个 ephemeral 被重写为 `ttl: "1h"`(除了滚动尾部),并且 `anthropic-beta` 带有 `extended-cache-ttl-2025-04-11`。
5. **运行另一个命令并观察命中情况**:
您的 Anthropic API 仪表板(或响应中的 `usage` 字段)现在将显示 `cache_read_input_tokens` 捕获了约 90% 的输入费用。
## 缓存工作原理
Anthropic API 允许每个请求最多 **4 个缓存断点**。每个断点告诉 API “缓存至此(包含此块)的所有内容”,这样具有相同前缀的后续请求将以 0.1 倍的基础输入价格命中缓存,而不是 1 倍。
此代理在(4 个槽位的限制内)如下放置它们:
1. **最后一个 `tools` 条目** → 1 小时 TTL。Claude Code 在 `tools` 上发送 **零个** 断点,因此整个约 24k token 的目录在每个交互中都会按完整输入价格重新计费。这是最大的一项收益。
2. **最后一个 `system` 块** → 1 小时 TTL。Claude Code 的 system prompt 大约是 8k tokens,并且在数小时内保持稳定。Claude Code 确实在 system 块上设置了 `cache_control`,但没有 `ttl`,因此会回退到 5 分钟的默认值 —— 这个时间长得足以让一次深思熟虑的交互使窗口过期,并在下一个交互时对所有约 8k tokens 强制收取 1.25 倍的重写费用。代理将其重写为 1 小时(并去除小于 500 个字符的 system 块上浪费的断点 —— Claude Code 在一个约 57 个字符的块上放置了一个,消耗了一整个槽位只为缓存约 15 个 tokens)。
3. **`messages[0]` 的最后一个可缓存块** → 1 小时 TTL。Claude Code 将静态提醒(CLAUDE.md、技能目录、延迟工具列表 —— 约 5k tokens)塞入第一个用户消息。每个会话缓存一次。
4. **滚动尾部** → 默认 5 分钟 TTL(可通过 `TAIL_TTL=1h` 配置)。所有消息中最后一个 `text` / `tool_result` / `image` 块。每个交互都会移动,因此每个新交互都会从缓存中读取上一个交互的前缀,并仅为增量部分支付基础价格。5 分钟是合适的默认值,因为尾部很少能在重复使用中存活一小时;为不断移动的断点支付 2.0 倍的写入倍数是浪费的。
在注入之前,代理还会 **剥离客户端在小于 500 个字符的 system 块上发送的断点** —— 缓存约 125 个 tokens 会消耗一整个槽位,而这个槽位更好地花在 `messages[0]` 提醒上。
## 节省金额计算
Anthropic 定价乘数(均相对于基础输入):
| 操作 | 乘数 |
|-----------------------------------|------------|
| 基础输入 (未缓存) | 1.0× |
| 缓存写入, 5 分钟 TTL | 1.25× |
| 缓存写入, 1 小时 TTL | 2.0× |
| 缓存读取 (命中) | 0.1× |
### 每次 API 调用的节省 (Claude Sonnet,每百万输入/输出 token $3 / $15)
下面的“交互”是指**一次到 `api.anthropic.com` 的 HTTP 往返**,而不是一条用户消息。(一条用户消息会扩展为许多次往返 —— 参见下面的[往返倍数](#round-trip-multiplier-the-bigger-win))。数字反映了典型的 Claude Code 请求,即会话中期的稳定状态:
| 数据块 | Tokens | 无代理 | 有代理 (缓存命中) | 节省 |
|------------------------------------------------------|---------|---------------|------------------------|--------------------|
| `tools` | 24,400 | $0.0732 | $0.00732 | **$0.0659** |
| `system` | 8,200 | $0.0246 | $0.00246 | **$0.0221** |
| `messages[0]` 提醒 | 5,000 | $0.0150 | $0.00150 | **$0.0135** |
| 上一轮历史记录 (滚动尾部) | ~15,000 | $0.0450 | $0.00450 | **$0.0405** |
| **每次调用总计 (输入)** | ~52,600 | **$0.158** | **$0.0158** | **约 $0.142 (−90%)** |
首次交互需支付**缓存写入附加费**:`(24.4k + 8.2k + 5k) × $3 × (2.0 − 1.0) / 1M = $0.113` 额外费用。在**第 2 次交互**时回本;之后的每次交互都是纯利润。
### 往返倍数 (更大的收益)
上面每次调用的节省看起来已经很不错了 —— 但在实践中,它们会乘以每条用户消息的往返次数,而这个数字才是让账单变得吓人的原因。
Anthropic API 是**无状态**的:每个 HTTP 请求都带有完整的对话上下文(system + tools + 先前消息 + 先前工具结果)。Claude Code 的循环也会**每次工具调用进行一次往返** —— Read、Grep、Edit、Bash、另一个 Read 等等。因此,像“修复这个 bug”这样的单个用户提示通常会扩展为:
- 小任务约 10-20 次往返
- 典型的调试或功能实现消息约 30-50 次
- 长时间自主任务或 `/plan` 风格的多阶段运行 100 次以上
这些往返中的每一次都会以完整的输入价格(无断点)重新发送约 24k token 的工具目录,并冒着 system prompt 无 TTL 的 5 分钟窗口过期的风险(1.25 倍重写)。
**在三种往返量级下的实际费用**(上面表格中的每次调用总计 × N,包括一次性的缓存写入附加费):
| 量级 | 定价层 | 无代理 | 有代理 | 节省 |
|-----------------------------------------------------|---------------------|---------------|------------|--------------|
| 10 次往返 (小任务) | Sonnet ($3/M input) | ~$1.58 | ~$0.27 | **约 $1.31** |
| | Opus ($15/M input) | ~$7.90 | ~$1.36 | **约 $6.54** |
| 30 次往返 (典型用户消息) | Sonnet | ~$4.74 | ~$0.59 | **约 $4.15** |
| | Opus | ~$23.70 | ~$2.94 | **约 $20.76** |
| 100 次往返 (重度会话) | Sonnet | ~$15.80 | ~$1.69 | **约 $14.11** |
| | Opus | ~$79.00 | ~$8.46 | **约 $70.54** |
数字仅为输入费用;输出费用不变(输出不被缓存)。缓存写入附加费(Sonnet 约 $0.11 / Opus 约 $0.57)在每个缓存窗口只支付一次,而不是每次往返支付一次 —— 因此在保持在 1 小时 TTL 内的会话中,您只需在开始时支付一次,随后的每次往返都是纯粹的 0.1 倍缓存读取。
**为什么缓存胜过裁剪。** 裁剪工具目录(例如,`DROP_TOOLS=…` 削减约 3k tokens)可以在没有缓存的情况下为您节省 `3k × N 往返` 的费用。有了缓存,它只能为您节省 `3k × 1 次缓存写入 + 3k × (N−1 次缓存读取` —— 收益大约小了一个数量级。缓存是承重的优化;裁剪是在缓存到位后有用的附加项。
### 请求大小节省 (`DROP_TOOLS` + ANSI 剥离)
独立于缓存,请求体修改减少了网络传输大小:
- `DROP_TOOLS=NotebookEdit,CronCreate,CronDelete,CronList,RemoteTrigger,PushNotification,Monitor` → 每次交互丢弃约 **3,300 tokens**。
- `STRIP_ANSI=1` → 从 `/context` 输出、终端颜色等中剥离 SGR 转义码。在受影响的交互中,大致将 `tool_result` 大小减半(约 500-2,000 tokens)。
- `TRIM_BASH_GIT=1` → 删除 Bash 工具描述的 git-commit 和 PR-creation 子部分。如果您不在 Claude Code 中使用 git,则可节省约 1,800 tokens。
### Claude Code 工具丢弃参考
Claude Code 附带了一个庞大的工具目录。并非每个会话都使用每个工具,您丢弃的每一个工具都会在每次交互中从工具 schema 中削减约 100-800 个 tokens。下面是一份实用的速查表,说明每个工具的作用以及通常是否可以安全丢弃。
图例:🟢 如果不使用可安全丢弃 · 🟡 有附加条件地丢弃(功能会消失) · 🔴 不要丢弃(没有它 Claude Code 会出错)
| 工具 | 作用 | 是否丢弃? |
|------------------------------------------------------|------------------------------------------------------------------|-------------------------------------------------------------------------------------|
| `Bash` | 运行 shell 命令。 | 🔴 保留。几乎所有操作的核心。 |
| `Read` | 读取本地文件(文本、图像、PDF、笔记本)。 | 🔴 保留。 |
| `Edit` | 在现有文件中进行精确字符串编辑。 | 🔴 保留。 |
| `Write` | 创建或覆盖文件。 | 🔴 保留。 |
| `Glob` | 按模式查找文件。 | 🔴 保留。比通过 Bash 使用 `find` 便宜得多。 |
| `Grep` | 搜索文件内容 (ripgrep 封装)。 | 🔴 保留。比通过 Bash 使用 `grep -r` 便宜得多。 |
| `TaskCreate` / `TaskUpdate` / `TaskList` / `TaskGet` | 在持久任务列表中跟踪多步骤工作。 | 🟡 如果您不想要任务跟踪则可丢弃 —— Claude 也会停止通过 todos 进行规划。 |
| `TaskOutput` / `TaskStop` | 检查/终止后台 `run_in_background` 任务。 | 🟡 仅当您也不运行长时间后台命令时才丢弃。 |
| `AskUserQuestion` | 带预览的结构化多项选择题。 | 🟡 丢弃以强制使用自由文本进行澄清。 |
| `EnterPlanMode` / `ExitPlanMode` | 规划模式工作流(先设计后实现)。 | 🟡 如果您从不使用 `/plan` 则丢弃。Claude 将以纯文本形式进行规划。 |
| `EnterWorktree` / `ExitWorktree` | 为隔离工作创建/退出 git worktrees。 | 🟢 除非您积极使用 worktrees 否则丢弃。 |
| `NotebookEdit` | 编辑 Jupyter `.ipynb` 单元格。 | 🟢 除非您使用笔记本否则丢弃。 |
| `WebFetch` | 获取 URL 并总结其内容。 | 🟡 如果您从不需要网络查找则丢弃 —— 会破坏文档获取。 |
| `WebSearch` | 搜索网络 (仅限美国)。 | 🟡 如果您不需要实时网络信息则丢弃。 |
| `CronCreate` / `CronDelete` / `CronList` | 在 cron 上调度提示;默认仅限会话内。 | 🟢 除非您使用会话内调度否则丢弃。 |
| `Monitor` | 后台事件流观察器(尾部日志、轮询 API)。 | 🟢 除非您需要实时监控否则丢弃。 |
| `PushNotification` | 通过 Remote Control 推送桌面/移动通知。 | 🟢 丢弃。很少需要。 |
| `RemoteTrigger` | 调用 claude.ai remote-trigger API (例程/计划)。 | 🟢 除非您管理计划的远程代理否则丢弃。 |
| `Skill` | 调用命名技能 (`/skills`)。 | 🟡 仅当您从不使用技能或斜杠命令时才丢弃。 |
| `mcp__ide__getDiagnostics` | 拉取 IDE 诊断(仅在附加了 IDE 扩展时出现)。 | 🟢 如果您不使用 IDE 扩展则丢弃。 |
保守的起步设置:`DROP_TOOLS=NotebookEdit,CronCreate,CronDelete,CronList,RemoteTrigger,PushNotification,Monitor,EnterWorktree,ExitWorktree`。
在一次交互后检查 `logs/*.req.json`,看看您的客户端实际发送了哪些工具 —— 目录因 Claude Code 版本和您加载的 MCP 服务器而异。
## 环境变量
| 变量 | 默认值 | 作用 |
|------------------|----------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `PORT` | `8787` | 要绑定的本地端口(始终为 `127.0.0.1`)。 |
| `AUTO_CACHE` | 关闭 | 启用缓存断点注入 + 1 小时 TTL 重写 + beta header。 |
| `TAIL_TTL` | `5m` | 滚动尾部断点的 TTL。`5m`(推荐)或 `1h`。其他槽位始终为 1 小时。 |
| `TRANSFORM_FILE` | — | 导出 `transform(body)` 用于自定义请求体修改的 JS 模块的路径。 |
| `DROP_TOOLS` | — | 从 `body.tools` 中移除的、逗号分隔的工具名称 *(需要 `TRANSFORM_FILE=./src/transforms/default.js`)*。 |
| `STRIP_ANSI` | `1` | 从消息文本和工具结果中剥离 ANSI 转义序列。设置为 `0` 以禁用。 |
| `TRIM_BASH_GIT` | `0` | 在“Committing changes”部分截断 Bash 工具描述。 |
| `MODEL_OVERRIDE` | — | 对每个 `/v1/messages` 请求强制使用不同的模型(例如,`claude-opus-4-6`)。同时重写 `system` 块中的模型名称引用,以便模型的自我描述保持一致。 |
| `LOG_BODIES` | 关闭 | 将修改后的请求 JSON + 原始响应字节转储到 `LOG_DIR`。 |
| `LOG_DIR` | `./logs` | 写入请求体转储的位置。 |
## 30 秒架构概述
```
bin/pino-proxy.js # CLI entry (shebang)
src/server.js # HTTP server + request handler, exports startServer()
src/config.js # env parsing, constants, transform loader
src/cache.js # breakpoint inject/rewrite, beta header
src/model.js # model-name rewrites for system-prompt self-description
src/logger.js # timestamps, sanitizers, request/response log writers
src/transforms/default.js # default body mutator (env-driven tools/ANSI stripping)
```
- `src/server.js` — 位于 `127.0.0.1:$PORT` 的 HTTP 服务器。缓冲请求体,解析匹配路径上的 JSON,运行 transform → inject → rewrite → beta-header 管道,流式传输响应。
- `src/transforms/default.js` — 默认的请求体修改器。处理工具丢弃、ANSI 剥离和上下文重构。
- 日志记录:`LOG_BODIES=1` 为每个请求写入 `
.req.json`(修改后,凭据已隐去)+ `.resp.log`(原始响应)。
有关完整的内部机制、操作顺序、注意事项以及扩展转换管道的指南,请参见 [CLAUDE.md](./CLAUDE.md)。
## 注意事项
- 代理仅绑定到 `127.0.0.1` —— 其他主机无法访问。
- Header 按原样透传:`x-api-key` / `authorization` 原封不动地发送到上游(仅在日志中隐去)。
- 节省金额计算是说明性的 —— 实际数字取决于您的使用模式、模型选择以及您的上下文在多次交互间的稳定性。标签:AI大模型, ANSI转义码清理, Anthropic API, API成本优化, API流量管理, API网关, Claude Code, GNU通用公共许可证, HTTP代理, LLM, MITM代理, Node.js, Prompt缓存, Unmanaged PE, 中间人代理, 工具过滤, 数据清洗, 缓存注入, 网络安全, 自定义脚本, 请求转换, 逆向代理, 防御绕过, 隐私保护, 零依赖