Skip to content
junhongchashuiPublic

About

检测token用量

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

7 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

tokenmon

本机 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,否则模型选择器里根本不出现它——配置"写进去了"和"能用"是两件事,这种坑靠替你写文件是躲不掉的,所以向导把它写在步骤里。

不花钱、不要 key 的自测

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 翻译这里没有,也不打算偷偷做——转一次格式就会让"数字来自供应商响应体"这个前提变脏

About

检测token用量

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages