本机 LLM Token 流量监控。数字来自供应商响应体里的 usage 字段,不是对提示词做事后估算。
两个采集口:一个是你把工具指过来的流量,另一个是客户端自己写在本机的会话记录(opencode 已支持,零配置、只读)。需要覆盖那些把端点写死的 App 时,再手动开 MITM 模式。
tokenmon app # 桌面面板窗口(Edge app 模式:无地址栏、独立任务栏图标)
tokenmon start --tray # 后台常驻 + 系统托盘(tooltip 显示今日用量)
tokenmon routes # 单入口地址 + 当前转发到哪家 + 18 个可切换的上游
tokenmon use zen # 换供应商:只改 tokenmon 自己,客户端的地址不用再动
tokenmon catalog # 34 个应用的接入方式,带"官方确认 / 未验证 / 接不上"分级
tokenmon verify claude-code # 某个应用到底有没有在走 Gateway
tokenmon top # 终端实时视图
tokenmon report --days 7 # 汇总
客户端只要填 http://127.0.0.1:8787/v1(Anthropic 那种会自己拼 /v1/messages 的客户端填 http://127.0.0.1:8787 就行)。之后 DeepSeek / Kimi / GLM / Zen / OpenAI 之间怎么换,都在 tokenmon 这侧:
tokenmon use <名字|完整 URL|off> # 或者面板顶部的下拉,点一下立即生效
它改的是 ~/.tokenmon/config.json 里的 defaultUpstream——tokenmon 自己的文件。正在跑的常驻服务当场用新路由,客户端不需要重启、不需要再改任何配置。这个设计是照 cc-switch 那类工具的做法来的:客户端指一次,切换留在代理自己这侧。
tokenmon 不写你的客户端配置。早先版本有过 tokenmon apply / undo(给 opencode 加一条服务商,带备份和还原),已经删掉了——去改别人的配置文件这条路,风险和工作量都换不回什么。向导现在只做一件事:告诉你哪个键填什么。
想按供应商分开看流量(verify 能分清是谁、按渠道聚合更准),也可以不用单入口,直接填分协议地址 http://127.0.0.1:8787/openai、/deepseek…… 两条路可以混用。
没有 Electron、没有 .NET SDK 依赖:窗口是 msedge --app 拉起的独立应用窗口(自己的 user-data-dir → 独立的任务栏标识和窗口分组),托盘是 PowerShell + WinForms NotifyIcon。
实测边界(截图验证过):托盘图标是自定义的(src/lib/desktop.mjs 的 buildIco 自己生成,Windows GDI 能解析),但标题栏图标仍是 Edge 自己的地球图标——Chromium 的 --app-icon 在这个场景不生效,参数我们照样传,但不保证。想要真·自定义窗口图标只能走 Electron 或原生壳。
托盘由常驻服务自己派生,不是由敲命令的那个终端派生——否则终端一关托盘就没了,而且服务停止时托盘也会跟着退出。tokenmon tray stop 只退托盘,tokenmon stop 停服务。
tokenmon catalog 列出 34 个应用(编程 agent CLI / IDE 插件 / 桌面与本地推理客户端 / 订阅制网页),每条给:改哪个环境变量或配置文件、走什么协议、以及这条信息的确信程度:
| 标记 | 含义 |
|---|---|
| 官方文档确认 | 打开过官方文档核对,附来源链接 |
| 有文档但未逐字核对 | 有出处但没逐字验证,粘贴前自己看一眼 |
| 未验证 | 没找到确证,不要当事实用 |
| Gateway 接不上 | 端点写死,只能走 MITM |
内置条目的实际分布(tokenmon catalog 的标记数,由 test/cli.test.mjs 锁住):
- 官方确认 12 条:claude-code、openai-sdk、opencode、copilot-cli、qwen-code、continue、cline、copilot-chat、cherry-studio、open-webui、litellm、ollama
- 有文档 3 条:aider、codex-cli、lm-studio
- 未验证 9 条:cursor-cli、amazon-q、zed、kilo-code、tabby、jan、lobechat、chatwise、immersive-translate —— 没找到确证,不要当事实用
- 接不上 10 条:gemini-cli(没有通用 base_url 覆盖项,只有
CODE_ASSIST_ENDPOINT)、roo-code(官方站显示扩展已停更)、以及全部 8 个订阅制网页/App
另有 4 条目录项写的是还不存在的上游名(tabby / litellm / jan / lm-studio 的 lmstudio):Gateway 会回 404 并列出可用名字,需要你先在 config.json 的 upstreams 里加一行——它们是自托管端点,端口只有你自己知道,我们替你猜不如不猜。
tokenmon verify <appid> 的判定分三档,不会把"别人家的流量"算到你头上:
- 已接入 — 目录里有 UA 特征且命中
- 前缀有流量 — 该 provider 前缀在用,但共用它的客户端长得一样,无法归属
- 暂无流量
想让自己的工具能被确认归属,在 ~/.tokenmon/catalog.json 里给它加一条 detects.ua,或者直接在向导里点「这是我的,记住它」。
面板默认是简单模式,术语全部换成大白话,右上角可切专业模式:
| 简单模式 | 专业模式 | 在说什么 |
|---|---|---|
| 用了多少内容 | 总 Token | 你和 AI 交换的文字量 |
| 你发出去 / 它回给你 | 输入 / 输出 | 一半对一半 |
| 省下的重复内容 | 缓存命中率 | 同样内容不用重复付费 |
| 它打字多快 | 出词速度 tokens/s | 回答生成的速度 |
| 多久开口 | 首 Token 延迟 | 从发出到看见第一个字 |
接入向导(打开面板的第一屏):顶部就是单入口地址和「现在这些请求发给哪家」的下拉;下面点你用的软件 → 列出该改哪个键、填什么 → 自动检测有没有数据进来,没进来就给具体排查项。
它不会去改你的客户端文件。识别不出来的软件(比如某些把地址藏在界面里、或者必须声明模型列表的)宁可让你手动填一行,也不替你猜——上一版这里有过「帮我改」,代价是要为 34 个应用各自维护一份"我以为它的配置长这样"的方案,删掉之后整个工具更小说得清了。
一个真实的教训写进了目录里:opencode 这类客户端,自定义服务商必须同时声明 models,否则模型选择器里根本不出现它——配置"写进去了"和"能用"是两件事,这种坑靠替你写文件是躲不掉的,所以向导把它写在步骤里。
node scripts/mock-provider.mjs --port 8799 # 本地假模型,长期挂着
node src/cli.mjs serve --tray # 配置里把上游指向 8799
node scripts/seed-demo.mjs # 造一批样本流量
假的是模型回答;客户端、协议、时间戳、用量解析全都是真的——用来验证链路,或者演示给别人看。
opencode 每完成一次回答,就把供应商返回的 usage 原样写进它自己的会话库 ~/.local/share/opencode/opencode.db(message.data 里的 tokens.input / output / reasoning / cache.read / cache.write、modelID、providerID、cost、time.completed)。tokenmon 只读这个库,所以在你把任何客户端指向 Gateway 之前,它就已经能报出这台机器上用掉多少:
tokenmon ingest # 手动补一次;常驻服务启动时自己跑,之后每 60 秒一次
三条规矩让它不至于变成第二本糊涂账:
- 不碰客户端的文件:只以只读方式打开它自己的 SQLite 库;WAL 边文件的修改时间一起看,否则刚写完的会话会漏在检查点之前
- 只记写完的:
time.completed没有的消息说明 tokens 还是半截,整个会话留到下一轮再看,不推进游标 - 和流量不重复计:事件 id 是
opencode:<会话>:<消息>,重扫必然跳过;如果同一条调用 Gateway 也在 30 秒内看到了同样的模型和同样的计数,那就以流量那条为准(它还带 TTFB),会话这条不入库
两类来源在面板「分开看 → 按怎么采到的」里是分开的两行,CLI 用 tokenmon report --by source,谁也没被偷偷加起来。
(这个思路不是我们想出来的:cc-switch 早就这么读客户端自己的会话记录了。我们只借了路子,格式是照着本机这个库实测出来的,实现全部自己写。)
| Gateway(默认) | MITM | |
|---|---|---|
| 触发方式 | $env:OPENAI_BASE_URL = "http://127.0.0.1:8787/v1" |
tokenmon mitm start + HTTPS_PROXY |
| 装根证书 | 不需要 | 需要(仅当前用户信任库) |
| 覆盖范围 | 允许改 base_url 的工具 | 额外覆盖写死端点的 App |
| 风险 | 无,进程只听 127.0.0.1 | 白名单域名可被本代理解密 |
未列入白名单的域名一律隧道直穿、不解密:一条隧道在关闭时落一行到 tunnels-*.jsonl,带真实的双向字节数和时长(不计入 token 统计,避免污染命中率)。
- 量:输入 / 输出 / 缓存读 / 缓存写(区分 5m 与 1h TTL)/ 推理 token,按渠道、供应商、模型、会话、客户端拆分
- 缓存命中率:
cache_read / (uncached_input + cache_read + cache_write)。Anthropic 的input_tokens不含缓存部分,OpenAI 的prompt_tokens含——适配器已分别归一,混用会静默算错。DeepSeek / Kimi 这一系把命中等写成prompt_cache_hit_tokens(同样包含在prompt_tokens里),三种拼法归到同一个缓存桶;只认 OpenAI 拼法时这几家会一直显示 0% - 速度:TTFB、首 token 延迟、解码 tokens/s。后两者来自 SSE 分片的实际到达时间,日志文件里没有
- 中断:客户端中途掐表的请求也会落一条,标
aborted(已收到的 token、时长、首字延迟都在),失败率里算一次失败——以前这种请求整条消失 - 成本:
~/.tokenmon/pricing.json可按 1M token 编辑。匹配分三档并回显在事件里(price_match):exact名字自己有行、snapshot去掉日期戳后同名、prefix去掉版本噪声后落进某一行;带档位后缀的名字不许套基座价(gpt-4o-mini不会按gpt-4o计),对不上就返回null显示「没定价」
内置上游 18 个:OpenAI、Anthropic、Google Gemini/Vertex、DeepSeek、Kimi、智谱 GLM、通义 DashScope、MiniMax、StepFun、SiliconFlow、xAI、Mistral、Groq、Together、OpenRouter、Cohere、Ollama、OpenCode Zen。Azure OpenAI、AWS Bedrock 按域名识别。
Zen 值得一提:https://opencode.ai/zen/v1 一个地址同时接 /chat/completions、/responses 和 /messages(三种都是实测 401,即路径存在、缺 key),所以单入口配 Zen 能同时喂 OpenAI 系和 Anthropic 系的客户端。模型 id 由它的 GET /v1/models 公开列出(实测 74 个)。
其他 OpenAI 兼容端点走通用适配器;未识别到协议的请求标成 generic,记字节但不记 token——宁可少报,不编数。
- 默认监听
127.0.0.1(--bind可以自己放宽;放宽后 Host 白名单只放行你绑的那个地址) Authorization/x-api-key/api-key等头不落盘,事件日志只留用量(自测里塞了假密钥再去事件文件里 grep,抓不到)- 面板只服务 Host 是回环地址(
localhost/127.0.0.1/::1,或你显式绑定的那个 IP)的请求,其余一律 403 —— 这是 DNS 重绑定的唯一破口,/api/status会把启动令牌发给通过这道检查的请求 - 写类操作(
/api/apply、/api/claim、/api/shutdown)还要:非浏览器来源不带 Origin、或 Origin 同为回环地址,并且带启动令牌;令牌在~/.tokenmon/run.json - 面板静态服务做了路径穿越防护
- 根 CA 私钥在
~/.tokenmon/ca/root.key,tokenmon ca untrust立刻从信任库移除;要彻底销毁删ca/与leafs/
MITM 的固有代价,用之前想清楚:装了根 CA 之后,凡是信任它的进程都能被这个代理解密。做了证书固定(pinning)的应用可能直接连不上——这时 tokenmon mitm stop,或把该域名从 config.json 的 mitmHosts 里去掉。
~/.tokenmon/events/events-YYYYMMDD.jsonl,一天一个文件,追加写。查询、去重、聚合在内存里做。
~/.tokenmon/ingest.json 是会话采集的游标(每个会话看到哪一刻了),删掉它等于下次全量重扫——事件 id 固定,所以重扫也不会多出账。
tokenmon export --days 30 --format csv --out usage.csv。删数据就是删这些文件。
工具能看见的只有经过本机的流量。三种接法,按侵入程度排:
1. 单入口(推荐)——一个地址填一次,换供应商不再动客户端:
$env:OPENAI_BASE_URL = "http://127.0.0.1:8787/v1"
$env:ANTHROPIC_BASE_URL = "http://127.0.0.1:8787" # 这类客户端自己拼 /v1/messages转发到哪家由 tokenmon use zen(或面板下拉)决定。前提是 defaultUpstream 已经设过,否则裸 /v1 请求返回 404 并告诉你为什么。
2. 分协议直连——想按渠道分清是谁,就把某个客户端固定指到某一家:
$env:OPENAI_BASE_URL = "http://127.0.0.1:8787/deepseek"tokenmon routes 打印全部 18 个内置上游的名字。自建的 OpenAI 兼容端点往 config.json 的 upstreams 里加一行即可。
3. MITM——给那些把端点写死、又不读环境变量的 App:
tokenmon ca create && tokenmon ca trust # 只进当前用户信任库
tokenmon mitm start
$env:HTTPS_PROXY = "http://127.0.0.1:8789"
$env:NODE_EXTRA_CA_CERTS = "$env:USERPROFILE\.tokenmon\ca\root.crt" # Node/Electron 自带 CA 表,不读系统库Electron 类应用还可以用 --proxy-server=http://127.0.0.1:8789 启动参数强制走代理。
订阅制客户端(网页版 ChatGPT/Claude)走的是另一套私有接口,usage 字段不一定存在,取不到时会记字节数但不编 token。
关于 opencode 有一点要提醒:它的自定义服务商必须同时声明 models,否则服务商在模型选择器里根本不出现——配置写进去了和能用是两件事。这也是这个工具不替你改客户端配置的原因之一:这类"看起来成功了其实没生效"的坑,靠替你写文件是躲不掉的,写在步骤里反而清楚。
npm pack --pack-destination dist # → dist/tokenmon-<版本>.tgz
npm i -g ./dist/tokenmon-0.1.0.tgz # 装完全局就有 tokenmon 命令
tokenmon doctor # 自检:运行时 / 端口 / 证书 / 数据 / 桌面壳
要求 Node ≥ 20;会话采集那一口要 Node ≥ 22.5(用内建的 node:sqlite,低版本会跳过这一路,其余照常)。零 npm 依赖,装完没有 node_modules。不装也能跑:node src/cli.mjs <命令>。
tokenmon / serve |
前台启动 Gateway + 面板 |
tokenmon start / stop / status |
后台常驻与查看 |
tokenmon routes |
单入口地址、当前转发到哪家、可切换的上游清单 |
tokenmon use <名字|URL|off> |
切换单入口的转发目标(有常驻服务就当场生效) |
tokenmon top -i 2 |
终端实时视图 |
tokenmon report --days 7 --by model |
汇总(vendor/model/channel/conversation/client/source) |
tokenmon export --days 30 --format csv --out u.csv |
导出 |
tokenmon ingest [--app opencode] |
读客户端自己记的会话用量(只读,零配置) |
tokenmon catalog [--cat cli] |
接入目录 |
tokenmon verify <appid> |
某个应用到底有没有在走 Gateway |
tokenmon ca create|show|trust|untrust |
本地根 CA 生命周期 |
tokenmon mitm start|stop |
透明拦截开关 |
tokenmon doctor |
运行时/端口/证书/价格表自检 |
node --test "test/*.test.mjs" # 73 项:usage 解析 / 成本 / 存储 / 路由 / CLI 文本 / 面板鉴权 / 会话采集 / 假上游端到端 / MITM 安全性质
node src/cli.mjs doctor # 运行时、端口、证书、价格表自检
TOKENMON_HOME=./.tmp-demo/home node src/cli.mjs serve --no-open
node scripts/seed-demo.mjs # 造样本流量看面板,不花真实额度
零 npm 依赖。核心只需要 Node ≥ 20;会话采集要 Node ≥ 22.5(用内建的 node:sqlite,读不到就跳过这一路);MITM 额外用系统里的 OpenSSL 签证书。测试会真的 spawn 一遍 CLI,所以路径里有空格(这个仓库就在 D:\ai project)也是被覆盖的场景。
改了代码要重启常驻服务才生效:正在跑的进程还是旧逻辑,面板看起来"一切正常"但修的东西没上线。tokenmon status 里的 uptime_ms 能看出它是几点起的。
- 应用不经过本机代理就不可能看见它(比如它在另一台机器上跑)
- 订阅制客户端(网页版 ChatGPT/Claude)的响应格式与公开 API 不同,字段可能取不到
pricing.json需要你自己核对现价,价格表过期只会让成本列失真,不影响 token 计数- MITM 的解密与"白名单外不解密"是在本地假上游 + 回环端口上验证的,没有对真实公网站点做过一次端到端验证;要不要装根证书去试,是你自己的决定
- 面板没有"你是谁"的鉴权,安全模型是"只有本机能连 + Host 白名单":同一台机器上的其他进程/用户可以读你的用量、拿到
run.json里的令牌 - 会话采集目前只有 opencode 一个(Claude Code / Codex / Gemini CLI 的会话格式各不相同,没实测过就不写解析器)。它用的是 Node 自带的
node:sqlite,仍然零 npm 依赖,但要求 Node ≥ 22.5;读不到就报一行,不影响流量采集 - 会话记录给不了的东西不硬凑:没有 TTFB / 首 token 时间(库里没记),
decode_tps是用created→completed整段算的上界,成本用客户端自己报的cost(免费渠道就是 0,不是我们估的) - 单入口不做协议互转。它只是按路径把请求转给
defaultUpstream,所以客户端说什么方言,就要选一个说同一种方言的上游(Zen 这类同址多方言的除外)。cc-switch 有的Anthropic ↔ OpenAI翻译这里没有,也不打算偷偷做——转一次格式就会让"数字来自供应商响应体"这个前提变脏