零依赖的本地 Web 控制台,用来消耗、观测和按额度管理 LLM 的 token 用量。
English · 简体中文
单文件(最快):从 Releases 下载 tokenfurnace-0.1.0.pyz,然后
python tokenfurnace-0.1.0.pyz整个工具就这一个文件,61 KB。不用 clone,不用 pip,不用管目录结构。浏览器会自动打开 http://127.0.0.1:8760/。
.pyz 是 Python 的 zipapp(PEP 441),把一个完整程序打成一个文件,等同于 Java 的 .jar。它要在终端里用 python 运行,双击通常没反应,因为 Windows 默认没给 .pyz 关联打开方式。
想双击启动:下载 Releases 页面底部的 Source code (zip),解压后双击 start.bat(Windows)或 start.sh(macOS / Linux)。
装到固定位置 + 桌面图标(推荐,一次装好以后不用再找):
git clone https://github.com/richenxiao/tokenfurnace.git
cd tokenfurnace
python scripts/install.py装完之后桌面上会多一个 TokenFurnace 图标,以后双击它就行——不用记命令、不用找目录。
程序装到 %LOCALAPPDATA%\TokenFurnace(macOS / Linux 是 ~/.local/share/tokenfurnace),
配置和账本一起迁过去,历史数据不丢。
源码目录通常带日期(比如 2026-09-20-09-30-59),哪天被清理掉工具就一起没了——
这个脚本就是为了避免那种事。想卸载:python scripts/install.py --uninstall(配置和账本会保留)。
克隆源码直接跑:
git clone https://github.com/richenxiao/tokenfurnace.git
cd tokenfurnace
python run.py装成命令(在克隆出来的目录里):
pip install -e .
tokenfurnace四种方式都只需要 Python 3.9+,没有其他依赖。配置和账本会落在你执行命令时所在的目录。
起点是 SenseNova TokenPlan 的积分返赠规则:Flash-Lite 专属积分每消耗 1 分返 1 通用积分,而通用积分能跑账号里所有已开放模型。要做的事就是把专属积分尽快烧掉、换成通用积分。
写着写着发现第二个问题:换一家服务商就得改代码,因为脚本把 base_url、模型名和请求格式全写死了。于是做成了通用的。
现在的形态是:填地址、选协议、勾模型、设上限,然后开跑。
界面上一共四步:
- 选协议,填 Base URL 和密钥,点「测试连接」
- 点「拉取模型」,勾选要消耗的模型
- 调消费策略(模式、并发、输入规模)
- 设额度上限,点「用当前配置开始消费」
不想先装再试的话,docs/preview.html 是用真实前端文件渲染的静态预览,含演示数据,双击就能看到完整界面。
界面顶部会显示一块引导,逐条列出当前还缺什么:
还差 3 步就能开始
1. 填 Base URL,比如 https://api.example.com
2. 填密钥,或改用环境变量(不落盘)
3. 点「拉取模型」勾选模型(也可以手动填模型 ID)
配好后点「测试连接」验证,再点「用当前配置开始消费」。
配置存到当前目录的 config.json,账本存到 data/tokenfurnace.db,两个都在 .gitignore 里。config.example.json 是一份配置模板。
命令行参数:
| 参数 | 默认 | 说明 |
|---|---|---|
--host |
127.0.0.1 |
监听地址。改成 0.0.0.0 会暴露到局域网,且不带任何鉴权 |
--port |
8760 |
端口被占用会自动往后找 |
--no-browser |
— | 不自动打开浏览器 |
--config |
./config.json |
指定配置文件 |
四套协议互不兼容,选错了直接 400 / 404:
| Anthropic Messages | OpenAI Chat | OpenAI Responses | Gemini Native | |
|---|---|---|---|---|
| 路径 | /v1/messages |
/v1/chat/completions |
/v1/responses |
/v1beta/models/{m}:generateContent |
| 默认认证 | x-api-key |
Authorization: Bearer |
同左 | x-goog-api-key |
| 系统提示 | 顶层 system |
messages 里 role: system |
顶层 instructions |
顶层 systemInstruction |
| 输入字段 | messages |
messages |
input |
contents[].parts[].text |
| 输出上限 | max_tokens(必填) |
max_tokens |
max_output_tokens |
generationConfig.maxOutputTokens |
| 思考开关 | thinking.budget_tokens |
reasoning_effort |
reasoning.effort |
thinkingConfig.thinkingBudget |
| 用量字段 | input_tokens / output_tokens |
prompt_tokens / completion_tokens |
input_tokens / output_tokens |
usageMetadata.promptTokenCount / candidatesTokenCount |
差异全在适配层里处理,选一个协议,请求体、认证头、用量字段解析都自动完成。
选哪个看端点暴露的路径:/v1/messages 选 Anthropic,/v1/chat/completions 选 Chat,/v1/responses 选 Responses,:generateContent 选 Gemini。地址里带 anthropic、claude 或 gemini 时会自动切过去。
想自己验证这四套确实不一样,跑 python scripts/protocol_probe.py。它起一个本地 mock 端点,把每种协议真实发出的 HTTP 请求抓下来打印。
凭证放在哪个请求头和协议是两回事。同一个协议在不同网关下可能要求不同的头,所以分开配置:
| 认证字段 | 实际发送 | 常见于 |
|---|---|---|
Authorization: Bearer |
Authorization: Bearer <key> |
OpenAI 系;Anthropic 生态里对应 ANTHROPIC_AUTH_TOKEN |
x-api-key |
x-api-key: <key> |
Anthropic 系;对应 ANTHROPIC_API_KEY |
x-goog-api-key |
x-goog-api-key: <key> |
Google Gemini |
切换协议时会自动带上该协议的惯用字段,之后仍可手动改。密钥留空就不发凭证头,所以不需要单独的「不认证」选项。
https://api.example.com → https://api.example.com/v1/chat/completions
https://api.example.com/v1 → https://api.example.com/v1/chat/completions
https://api.example.com/v1/chat/completions → https://api.example.com/v1/chat/completions
https://api.example.com/api/paas/v4 → https://api.example.com/api/paas/v4/chat/completions
https://generativelanguage.googleapis.com → https://generativelanguage.googleapis.com/v1beta/models/{m}:generateContent
填根地址就行,版本段自动补,粘贴完整端点也能救回来。
可以存任意多套配置(不同服务商、不同端点、不同额度规则),下拉切换,导出导入 JSON。复制配置走服务端接口,密钥和模型一起带过去。
| 来源 | 说明 |
|---|---|
| 直接填写 | 明文存进本地 config.json |
| 环境变量 | 只存变量名,运行时读取,密钥不落盘 |
| 文件路径 | 只存路径,运行时读文件内容 |
密钥在所有 API 响应里都脱敏成 sk-abc********wxyz,前端拿不到明文。三种来源各自独立存值,切换不会把上一份内容带过去。
不改配置文件也能起一个临时配置,适合容器和快速试用:
export TOKENFURNACE_BASE_URL=https://api.example.com/v1
export TOKENFURNACE_API_KEY=sk-xxxx
export TOKENFURNACE_MODEL=model-a,model-b
export TOKENFURNACE_PROTOCOL=anthropic # 可选
python run.py配置文件路径可以用 TOKENFURNACE_CONFIG 覆盖。
点「拉取模型」拿到端点的全量列表,逐个勾选。每个模型可以单独设权重(分流概率)和积分/1K(换算系数,留空用全局值)。面板底部实时显示「本次将消耗 N 个模型」。
两个细节值得说:
- 批量按钮只作用于当前筛选出来的模型。列表可能有几百个,无差别全选会把不想消耗的也带上。先用搜索框筛,再点「勾选可见」。
- 列表里没有的模型可以手动填 ID。有些网关的
/models只返回一部分,手动填不受列表限制。
没有「只留某个模型」这类一键预设。
在「⑤ 批量启动」里勾选多个配置,一次全部启动,不用来回切换。
- 每个会话用各自的密钥、模型、额度和换算系数
- 策略参数(模式、并发、输入规模、预算)取当前表单那一份,所有会话共用
- 起不来的配置会被直接禁用并写明原因(没填 Base URL / 没密钥 / 没勾模型),勾不上就不会误选
- 启动结果留在页面上,不只弹一个会消失的提示
看板顶部是所有会话的合计(速率、tokens、请求数逐点相加),下方「运行中的会话」逐个列出,可以单独停止。
滚动窗口是全局共享的。任一会话把额度跑满,所有会话一起等,因为平台额度本来就是共享的。全局 worker 上限默认 32,超出会拒绝并提示当前占用。
积分是逐请求落库的,滚动窗口本来就是全表求和,所以多个会话不会互相污染记账。
停止会话时在途请求无法中断(urllib 阻塞在 recv 上,Python 也没法强杀线程),状态会显示「正在停止(等在途请求结束)」。这些请求完成后仍会被记账。
配置 id 用严格查找。传一个不存在的 id 会明确报错,不会静默回退到别的配置,否则你以为在跑配置 X,实际烧的是配置 A 的额度。
| 模式 | 做法 | 适用 |
|---|---|---|
| 预填充(默认) | 超大输入 + 极短输出 | 最快,消耗效率最高 |
| 生成 | 小输入 + 长输出 | 模拟真实生成场景 |
| 混合 | 预填充为主,穿插生成 | token 构成更自然 |
默认预填充是因为实测差得多。同一模型下:
| 路径 | 实测吞吐 |
|---|---|
| 预填充(大输入 + 极短输出) | 9,000 ~ 18,600 tok/s |
| 纯生成(小输入 + 长输出) | ~111 tok/s |
差约 80 倍。预填充每次塞 38 万字符随机英文(约 18 万 tokens),模型只回两个字。
| 参数 | 默认 | 说明 |
|---|---|---|
| 并发数 | 4 | 实测比较平衡;再高容易触发限流 |
| 单请求输入 | 380000 字符 | 约 18 万 tokens,接近但不越过上下文上限 |
| 最大输出 | 8 | 预填充模式下保持很小 |
| 思考强度 | none |
关掉最快 |
| 击穿缓存 | 开 | 见下 |
复用同一段前缀,很多平台会走 prefix cache 并按折扣计费,你以为在消耗额度,实际只花了一点点。TokenFurnace 每次请求都重新生成随机文本,实测 cached_tokens 恒为 0。控制台里有这个指标,正常应该一直是 0。
有些平台触发每秒请求数限流时返回的是:
{"error": {"message": "rps exhausted", "type": "quota_exceeded_error"}}type 写着 quota_exceeded_error,但 message 是 rps,这是限流,可以重试。按 type 判断会在还有额度的时候误判成额度耗尽直接停机,所以 TokenFurnace 按 message 分类:
| 类型 | 触发 | 处理 |
|---|---|---|
rps / server |
限流、服务端 5xx | 本会话所有线程一起退避,避免并发把限流窗口踩满 |
timeout / network |
单条连接卡住 | 只让当前线程退避,其余线程继续跑 |
quota |
真额度耗尽 | 停机,并反推换算系数 |
auth |
401 / 403 | 停机并提示检查密钥 |
bad_request |
400 / 404 / 422 | 记录后跳过;404 会额外提示检查协议 |
超时按实测延迟自适应(最近延迟中位数 ×3,下限 30 秒,不超过你设的上限)。固定 180 秒意味着一条挂住的连接要占住一个 worker 三分钟,而正常请求只要 25 秒。
主数字是即时速率,统计窗口可以在 5 / 10 / 20 / 60 秒之间切换。
窗口会被自动放宽。预填充模式下单个请求要 20~40 秒才完成,5 秒窗口里经常一次完成都没有,主数字就会长期显示 0.0。实际使用的窗口会自动放宽到至少覆盖最近两次完成,界面下方会标注出来。你选的窗口是灵敏度下限,只放宽不收窄。
会话卡片上还显示单请求实测耗时,这是判断「谁的问题」最快的方法:
| 现象 | 结论 |
|---|---|
| 平均速率掉了,单请求耗时没变 | 本地问题:并发太低、撞了滚动窗口、或有线程在退避 |
| 单请求耗时明显变长(25 秒 → 115 秒) | 服务端拥塞,本地怎么调都救不回来 |
- 多开配置,收益最大。勾选多个配置同时跑,总吞吐相加。
- 提高并发,默认 4,可以试 6~8。请求大部分时间在等服务端,并发越高吞吐越高,直到撞上限流。
- 超时上限别设太大。已自适应,但你的设定就是天花板。
- 别关缓存击穿。关掉后请求会命中 prefix cache,平台按折扣计费。
- 输入规模不要盲目调小。每请求 token 更多意味着更少的分摊开销,除非你怀疑平台对长请求有限时时间。
任一触顶即自动停止,0 表示不限:积分上限、token 上限、请求数上限、运行时长。
本地记账推算 5 小时和 7 天两个滑动窗口。窗口内用量逼近上限时,工具算出最早一条记录还要多久滑出窗口,然后精确等待,不盲目轮询。
打开挂机模式后,撞额度会一直等下去并自动续跑。
注意计额口径。 平台的通用积分池和 Flash-Lite 专属积分池各自独立计额,而本地账本只记一个合并口径。 一直用 Flash-Lite 刷积分时两者一致;一旦混入按通用积分计费的模型,窗口占用就和平台的真实口径对不上了, 需要自己留余量。
有些平台把积分拆成多个独立计额的池,例如「专属积分池」和「通用积分池」, 各自有独立的 5 小时和周额度。规则通常是先用专属池,专属不够再扣通用池。
危险之处在于:专属池用尽时平台不报错,而是静默改从下一个池扣费, 请求照样返回 200。工具察觉不到,继续发,就会把不该动的池吃掉—— 而被动扣掉的那部分通常不参与返赠活动。
在「④ 额度与时长 → 积分池」里逐池声明,工具就会按池核算,某个池用满时停手:
| 池名称 | 匹配模型 | 5 小时上限 | 周上限 |
|---|---|---|---|
| Flash-Lite 专属积分 | *flash-lite* |
60000 | 600000 |
| 通用积分 | * |
60000 | 600000 |
按顺序匹配,首个命中生效。所以专属池写前面、兜底的 * 写最后,
语义就和平台「专属不足再扣通用」的规则对齐。模型填 glob 模式。
留空即单池,行为等同上面的 5 小时 / 周额度。
换算系数填错之后要重算历史。 窗口积分是逐请求落库的,改系数不会自动改历史 (这是有意为之,正常调参不该篡改已发生的事实)。但系数填错之后修正是另一回事: 不重算的话窗口会一直显示错的占用,周窗口最长要卡 7 天,期间工具一直以为额度满了。 确认新系数填对了,再点「重算历史」。
如果平台按积分而不是 token 计费,填一个换算系数(积分/1K tokens),工具就能显示实时积分消耗、按积分上限刹车、按 5h/周积分额度限流。
拿到这个系数有两条路:
- 手动:看平台账单差额,
系数 = 消耗积分 × 1000 ÷ 消耗 tokens - 自动:点界面上的「反推」,用当前窗口已消耗的 token 数反推。或者等平台真的返回额度耗尽,此时工具会在日志里打印系数
反推的前提是该窗口额度确实被打满过。如果只是跑了很久还没报错,得到的只是上界,不是真值。
分段控件用于协议、认证字段、消耗模式、密钥来源、图表时间范围。数值输入带步进按钮,步长按字段设定(输入规模一次跳 2 万字符)。布尔项用开关,副标题说明后果。
- 速率主卡:即时速率、平均、峰值、吞吐,右侧是最近一段时间的迷你图。数字按速率分档配色
- 趋势图:绿色面积是瞬时速率,蓝色虚线是累计 tokens,双 Y 轴,悬停显示精确值
- 滚动窗口占用:5 小时和周两个进度条,超过 75% 变黄、92% 变红
- 运行日志:彩色分级,自动滚到底
- 历史会话:可点「明细」看按模型和按错误的拆分
服务起来后可以直接调,方便脚本化:
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | /api/state |
全量状态(配置、实时指标、日志、历史) |
| GET | /api/run/detail?run=<id> |
单会话明细(按模型/错误拆分、时间桶) |
| POST | /api/profile/save |
新建或更新配置 |
| POST | /api/profile/activate |
切换当前配置 |
| POST | /api/profile/duplicate |
复制配置(连密钥一起) |
| POST | /api/profile/delete |
删除配置 |
| POST | /api/profile/import |
导入配置 |
| POST | /api/config/engine |
更新引擎默认参数 |
| POST | /api/config/window |
调整即时速率统计窗口(秒),立即生效 |
| POST | /api/test |
连通性自检(拉模型 + 打一个最小请求) |
| POST | /api/models |
拉取模型列表 |
| POST | /api/guess_protocol |
按 base_url 猜协议 |
| POST | /api/run/start |
启动一个会话 |
| POST | /api/run/start_batch |
批量启动多个配置,逐个返回成功/失败 |
| POST | /api/run/stop |
停止会话;带 sid 停单个,不带则全部停 |
| POST | /api/estimate |
按参数预估消耗 |
| POST | /api/calibrate |
手动算换算系数 |
| POST | /api/calibrate/auto |
按窗口反推系数 |
| GET | /api/export/requests.csv |
请求明细 CSV |
| GET | /api/export/runs.csv |
会话汇总 CSV |
| GET | /api/export/config.json |
配置导出 |
tokenfurnace/
├── run.py 启动器
├── start.bat 双击启动(Windows)
├── start.sh 双击启动(macOS / Linux)
├── tokenfurnace/
│ ├── cli.py 命令行入口
│ ├── config.py 配置源、Profile、密钥解析
│ ├── providers.py 四协议适配、错误分类、URL 归一化
│ ├── engine.py 消费引擎(多会话 / 限流 / 预算 / 滚动窗口)
│ ├── store.py SQLite 记账层
│ ├── server.py REST API + 静态服务
│ └── web/ 前端(原生 JS,无框架无构建)
├── docs/preview.html 界面静态预览(自包含,可直接打开)
├── scripts/
│ ├── protocol_probe.py 实证四种协议的真实 HTTP 差异(离线)
│ ├── smoke_ci.py 进程内起服务并断言接口形状(CI 用)
│ ├── smoke_live.py 对已运行的服务做端到端联调
│ ├── build_preview.py 重新生成 docs/preview.html
│ ├── build_release.py 打成单文件 .pyz(Release 附件)
│ ├── install.py 装到固定位置并建桌面图标
│ └── check_batch_ui.js 用 jsdom 跑真实前端代码(需 npm i jsdom)
├── tests/test_core.py 90 个离线单元测试
├── config.example.json 配置模板
└── data/tokenfurnace.db SQLite 记账库(自动生成,已 gitignore)
用 SQLite 而不是日志文件,是因为滚动窗口统计要走索引(几十万条记录也不慢)、历史查询一句 SQL 就够、断电不会写坏(WAL + 逐条提交)。
明细默认保留 30 天,启动时清理并 ANALYZE,避免库无限膨胀拖慢窗口查询。
python -m unittest discover -s tests -v # 90 个测试,全部离线
python -m compileall -q tokenfurnace run.py # 语法检查测试覆盖 URL 归一化、错误分类(含 rps 伪装成 quota 的回归用例)、四种协议的请求组装与响应解析、密钥三种来源、配置迁移与导入、SQLite 窗口记账、即时速率与自适应窗口、多会话聚合、预算刹车。
CI 只跑三个任务:Ubuntu × Python 3.9(声明的最低版本)、Ubuntu × 3.13、Windows × 3.13(主要用户平台)。要防的真实风险就这两个:3.9 的兼容性作者本地测不到,Windows 是路径和编码问题最容易暴露的地方。macOS 对纯标准库项目不提供额外信息,所以没开。
每个任务跑语法检查、90 个单元测试、协议差异实证和一次冒烟测试(在进程内起服务、打 API、断言、关掉)。
四个脚本可以复现关键结论:
| 脚本 | 用途 |
|---|---|
scripts/protocol_probe.py |
起本地 mock 端点,抓四种协议的真实 HTTP 请求 |
scripts/smoke_ci.py |
进程内起服务并断言接口形状,CI 用的就是这个 |
scripts/build_preview.py |
用真实前端文件生成 docs/preview.html |
scripts/check_batch_ui.js |
用 jsdom 跑真实前端代码,验证交互行为(需 npm i jsdom) |
报 404 / 400,提示路径不存在?
协议选错了。看端点实际暴露的是 /chat/completions、/responses、/messages 还是 :generateContent。
一直 429? 并发开太高,降到 4 或更低。工具会自动退避,但降并发才是根治。
cached_tokens 不是 0?
有请求命中了 prefix cache。确认「击穿缓存」是开着的。
实时速率显示 0.0? 看下方有没有「已自动放宽到 N 秒」。如果正在跑,说明窗口被放宽了;如果空闲,那确实是 0。
能同时跑多个配置吗? 能。在「批量启动」里勾选多个,或者切配置后各点一次「用当前配置开始消费」。滚动窗口是全局的,任一会话跑满,所有会话一起等。
能暴露到公网吗?
默认只绑 127.0.0.1。--host 0.0.0.0 本身不带鉴权,要远程访问请自己套反向代理加认证。
本工具是通用 API 客户端,不针对任何特定平台。请自行确认使用方式符合所用服务商的条款。作者不对因使用本工具导致的账号、额度或费用问题负责。