1. 为什么你的 CodingPlan 总在偷偷烧 TOKEN如果你同时订了两三个 CodingPlan 套餐大概率遇到过这种场景月初信心满满觉得额度够用结果不到两周就提示余额不足。你打开各家控制台看到的只有一个冷冰冰的剩余百分比至于这些 TOKEN 到底被谁吃掉了、是哪个模型、哪次请求、输入多还是输出多一概不知。这就是 CodingPlan 场景下最典型的痛点——TOKEN 消耗不可见。你花钱买的是「额度」但额度被消耗的过程是个黑盒。批量测试跑一轮、Agent 自动补全跑一天、群聊接力聊嗨了TOKEN 就像开了闸的水龙头你只能事后看着账单心疼。我试过最笨的办法每调一次接口就手动记一笔。结果当然是坚持不了三天。后来想明白了这事必须交给代码——在请求出口做一层拦截把每次调用的 usage 字段落盘再用一个本地看板把数据可视化出来。这就是今天要手搓的「CodingPlan 照妖镜」一个基于 Next.js 的本地 TOKEN 用量看板。它能做什么简单说三件事。第一统一走一个 API 通道不管你后面接的是哪家模型请求都从同一个出口出去第二每次请求结束后把 prompt_tokens、completion_tokens、total_tokens 连同模型名、时间戳一起写进本地 JSON第三提供一个仪表盘页面按天、按模型、按平台聚合展示让你一眼看出谁在烧钱。适合谁适合手上握着多个 CodingPlan、想搞清楚额度去向的开发者适合在跑批量评测、Agent 任务、需要核算成本的团队也适合刚接触 Next.js 全栈、想拿一个真实小项目练手的朋友。整篇教程从零开始命令可复制配置可照抄跑完你就能看到自己第一笔真实的 TOKEN 账单。需要说明的是本文的看板只做「记录与展示」不碰任何账号密码也不做额度代充之类的操作。所有请求都通过你自己配置的 API 通道发出数据全部落在你本地的 JSON 文件里安全边界清晰。2. 用 TaoToken 做统一出口先把 Key 和通道理清楚要让看板能统计到 TOKEN前提是所有模型调用都走同一个出口。如果每个平台各调各的你就得在每个 SDK 里都埋一遍统计逻辑维护成本极高。所以第一步我们用一个统一的 API 通道把请求收口。这里用 TaoToken 作为统一出口。它的价值在于你只需要维护一套 Base URL 和一把 Key就能在同一个通道里切换不同模型而每次响应里都会带回标准的 usage 字段正好喂给我们的统计逻辑。官网入口是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 地址是 https://taotoken.net/api 注意 API 地址后面不加任何查询参数。先注册并登录然后进控制台创建 API Key。控制台地址是 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content Key 管理页在 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。创建好之后把 Key 复制出来形如sk-xxxxxxxx后面写进.env.local。如果你只是想先验证模型通不通可以打开模型对话页 https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 手动发一条消息看看返回是否正常。这一步能帮你排除掉「Key 本身有问题」这类低级错误省得后面在代码里瞎找。对于长期跑编码任务、Agent 自动化的场景建议直接上 Coding Plan地址是 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。它的定位就是给高频调用准备的配合我们的看板你能清楚看到每个任务的 TOKEN 消耗曲线判断套餐是否划算。接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 里面写明了 OpenAI 兼容协议和 Anthropic 协议的调用方式。我们的看板默认走 OpenAI 兼容协议因为它的响应结构里 usage 字段最规整解析起来最省事。这里要强调一个关键点统计的准确性取决于响应里有没有 usage。有些流式响应默认不返回 usage需要显式开启。TaoToken 的 OpenAI 兼容接口在请求体里带上stream_options: { include_usage: true }就能在流式结束时拿到 usage。这个参数后面在代码里会体现先记住。配置阶段还有个小坑环境变量名不要用NEXT_PUBLIC_前缀。因为我们的 Key 只在服务端路由里使用加了NEXT_PUBLIC_反而会把它暴露到浏览器端既不安全也没必要。用普通的TAOTOKEN_API_KEY就行。3. 可复制的 Next.js 路由配置与环境变量写法这一节是全文的核心所有代码都可以直接复制。我们创建一个 Next.js 项目用 App Router 结构写一个 API 路由作为统一出口请求发出后把 usage 落盘。先初始化项目。打开终端执行npx create-next-applatest codingplan-burner --typescript --app --no-tailwind --no-eslint --src-dir --import-alias /* cd codingplan-burner npm install创建完成后在项目根目录新建.env.local写入TAOTOKEN_API_KEYsk-你的Key TAOTOKEN_BASE_URLhttps://taotoken.net/api TAOTOKEN_MODELclaude-sonnet-4-5注意TAOTOKEN_BASE_URL结尾不要带斜杠代码里会自己拼/v1/chat/completions。模型 ID 按你实际要用的填这里只是示例。接着建数据目录和统计文件。在项目根目录执行mkdir -p data echo [] data/usage.json然后写核心路由。新建文件src/app/api/chat/route.tsimport { NextRequest, NextResponse } from next/server; import fs from fs; import path from path; const USAGE_FILE path.join(process.cwd(), data, usage.json); type UsageRecord { ts: string; model: string; prompt_tokens: number; completion_tokens: number; total_tokens: number; latency_ms: number; }; function appendUsage(record: UsageRecord) { const raw fs.readFileSync(USAGE_FILE, utf-8); const list: UsageRecord[] JSON.parse(raw || []); list.push(record); fs.writeFileSync(USAGE_FILE, JSON.stringify(list, null, 2), utf-8); } export async function POST(req: NextRequest) { const { messages, model } await req.json(); const useModel model || process.env.TAOTOKEN_MODEL!; const started Date.now(); const resp await fetch( ${process.env.TAOTOKEN_BASE_URL}/v1/chat/completions, { method: POST, headers: { Content-Type: application/json, Authorization: Bearer ${process.env.TAOTOKEN_API_KEY}, }, body: JSON.stringify({ model: useModel, messages, stream: false, }), } ); if (!resp.ok) { const errText await resp.text(); return NextResponse.json( { error: errText, status: resp.status }, { status: resp.status } ); } const data await resp.json(); const usage data.usage || {}; appendUsage({ ts: new Date().toISOString(), model: useModel, prompt_tokens: usage.prompt_tokens ?? 0, completion_tokens: usage.completion_tokens ?? 0, total_tokens: usage.total_tokens ?? 0, latency_ms: Date.now() - started, }); return NextResponse.json({ content: data.choices?.[0]?.message?.content ?? , usage, }); }这段代码做了四件事接收前端传来的 messages 和 model用环境变量里的 Key 和 Base URL 向 TaoToken 发请求从响应里取出 usage把 usage 追加写入data/usage.json。注意这里用的是同步文件读写本地单机看板够用如果你要并发压测建议换成追加写或 SQLite。再写一个查询路由给仪表盘用。新建src/app/api/usage/route.tsimport { NextResponse } from next/server; import fs from fs; import path from path; const USAGE_FILE path.join(process.cwd(), data, usage.json); export async function GET() { const raw fs.readFileSync(USAGE_FILE, utf-8); const list JSON.parse(raw || []); const byModel: Recordstring, number {}; let total 0; for (const item of list) { byModel[item.model] (byModel[item.model] || 0) item.total_tokens; total item.total_tokens; } return NextResponse.json({ total, byModel, records: list }); }如果你更习惯用 TOML 或 JSON 配置文件管理模型清单可以在根目录建config/models.json{ models: [ { id: claude-sonnet-4-5, label: Sonnet 4.5 }, { id: claude-opus-4-6, label: Opus 4.6 }, { id: glm-4-plus, label: GLM-4-Plus } ] }然后在路由里读取这个文件做模型下拉。这样切换模型不用改代码改配置就行。最后写一个最简仪表盘页面src/app/page.tsx用 fetch 拉/api/usage展示总量和分模型统计。页面代码不复杂核心就是useEffect里请求一次把total和byModel渲染成表格。到这一步配置部分就齐了。4. 启动项目并验证一次真实请求的 TOKEN 账单配置写完启动项目npm run dev终端会输出http://localhost:3000。先别急着开页面我们用 curl 直接打一次 API 路由验证统计链路是否通。新开一个终端执行curl -X POST http://localhost:3000/api/chat \ -H Content-Type: application/json \ -d { messages: [ {role: user, content: 用一句话解释什么是 TOKEN} ] }如果一切正常你会看到类似这样的返回{ content: TOKEN 是模型处理文本的最小单位可以粗略理解为字或词的片段。, usage: { prompt_tokens: 18, completion_tokens: 27, total_tokens: 45 } }记下这个total_tokens: 45。然后打开data/usage.json应该能看到刚写入的一条记录[ { ts: 2025-01-15T08:30:12.345Z, model: claude-sonnet-4-5, prompt_tokens: 18, completion_tokens: 27, total_tokens: 45, latency_ms: 1832 } ]现在打开浏览器访问http://localhost:3000仪表盘应该显示总消耗 45 TOKEN分模型统计里 Sonnet 4.5 占 45。到这里一次完整的「请求—统计—展示」闭环就跑通了。接下来做核对。回到 TaoToken 控制台的用量页面找到刚才那次调用的记录对比它的 total_tokens 是不是 45。如果一致说明你的燃烧器统计准确如果有偏差先检查是不是有别的请求混进来了或者模型 ID 对不上导致计费口径不同。为了验证多模型场景再打两次请求分别指定不同模型curl -X POST http://localhost:3000/api/chat \ -H Content-Type: application/json \ -d {model:glm-4-plus,messages:[{role:user,content:写一个二分查找}]} curl -X POST http://localhost:3000/api/chat \ -H Content-Type: application/json \ -d {model:claude-opus-4-6,messages:[{role:user,content:解释一下快速排序}]}刷新仪表盘你会看到三个模型各自的 TOKEN 消耗。这时候「照妖镜」的效果就出来了同样一个问题不同模型的输入输出 TOKEN 差异可能很大谁更费钱一目了然。实测下来输出型任务里 Opus 的 completion_tokens 通常明显高于轻量模型这就是账单差异的主要来源。如果你要跑流式请求记得在请求体里加stream_options: { include_usage: true }否则流式响应可能不带 usage统计就会漏记。这是最容易踩的坑之一。5. 常见报错排查401、local proxy failed 与 reading choices跑起来之后报错基本集中在几个地方。这一节按真实报错逐个拆。401 Unauthorized。返回体里通常是{error:{message:Invalid API key}}。原因有三个Key 没填、Key 填错、或者.env.local改了之后没重启 dev server。Next.js 的环境变量是在启动时加载的改完必须CtrlC停掉再npm run dev。另外检查 Key 有没有多余空格复制的时候很容易带上换行。local proxy failed / fetch failed。这个报错说明请求根本没发出去通常是 Base URL 写错了。检查TAOTOKEN_BASE_URL是不是https://taotoken.net/api结尾不要带斜杠也不要在代码里重复拼/api。如果你在代码里写的是${BASE_URL}/v1/chat/completions那 BASE_URL 就应该是https://taotoken.net/api拼出来正好是https://taotoken.net/api/v1/chat/completions。多一层少一层都会 404 或连接失败。Cannot read properties of undefined (reading choices)。这个报错说明data.choices是 undefined也就是响应结构和你预期的不一样。最常见的原因是请求失败但没检查resp.ok直接把错误响应当成功解析了。解决办法是在resp.json()之前先判断resp.ok不 ok 就把resp.text()打出来看。另一个原因是模型 ID 写错接口返回了错误对象而不是正常的 chat completion 结构。usage 全是 0。请求成功但统计为 0八成是流式响应没开include_usage或者你用的模型/协议本身不返回 usage。先确认请求体里stream: false非流式一般都会带 usage。如果确实要用流式加上stream_options。OAuth / 认证相关报错。如果你在 Claude Code 或 Codex 这类工具里配置报 OAuth 错误通常是因为认证方式选错了。这类工具要的是 Base URL API Key Model ID 三件套不是网页登录的 OAuth 流程。以 Claude Code 为例配置时把 Base URL 指向https://taotoken.net/apiKey 填sk-开头的 API KeyModel ID 填具体模型名三者缺一不可。Codex 的auth.json里同理OPENAI_BASE_URL和OPENAI_API_KEY要对应上模型 ID 单独在配置里指定。CC Switch / Cline MCP 配置。如果你用 CC Switch 管理多个通道或者用 Cline 的 MCP 接模型同样记住三件套Base URL、Key、Model ID。MCP 配置里不要直连生产数据库只做模型调用。CC Switch 里新增通道时协议选 OpenAI 兼容地址填https://taotoken.net/apiKey 填你的模型 ID 按需填。排查顺序建议固定下来先看 HTTP 状态码再看响应体原文最后看本地data/usage.json有没有写入。三步走完90% 的问题都能定位。6. 把看板用起来从记账到优化你的 CodingPlan看板跑通只是开始真正有价值的是用它做决策。这里分享几个我实际用下来的思路。第一按任务类型分组统计。你可以在请求体里加一个自定义字段比如task: batch-test或task: agent-run然后在路由里把它一起写进 usage 记录。这样你就能看出「批量测试」和「日常补全」各占多少额度。很多人额度不够用其实是被某个自动化任务悄悄吃掉了分组之后一目了然。第二关注 completion_tokens 而不是 total。输入 TOKEN 你基本控制不了但输出 TOKEN 可以通过提示词约束。比如在系统提示里加一句「回答控制在 200 字以内」输出量能降一大截。看板里把 completion_tokens 单独列一栏你就能评估提示词优化的效果。第三给看板加一个按天聚合的视图。现在的byModel只统计了总量你可以再写一个按ts的日期部分分组的逻辑画出每天的消耗曲线。如果某天突然飙升回去翻那天的记录就能找到是哪次调用异常。第四定期导出 JSON 做归档。data/usage.json会一直增长建议每周导出一次清空文件重新记。导出后可以用 Excel 或脚本做更复杂的分析比如算每个模型的「每千 TOKEN 平均延迟」找出又慢又费钱的组合。如果你想让看板更实用可以加一个「预算告警」在查询路由里判断当日 total 是否超过阈值超过就在页面上标红。这个逻辑很简单几行代码的事但能帮你避免月底才发现额度见底。最后说下扩展方向。当前看板是单机 JSON 存储适合个人用。如果你要团队共享把存储换成 SQLite 或 Postgres路由逻辑基本不用改。如果你要接更多平台只要它们兼容 OpenAI 协议改一下 Base URL 和 Key 就能纳入统计。核心思路始终是统一出口、拦截 usage、本地落盘、可视化展示。这套东西不复杂但解决的是真问题。当你第一次看到自己所有 CodingPlan 的 TOKEN 消耗被摊在一张表上那种「终于看清了」的感觉比任何账单提醒都管用。
阅读完成 · 觉得有帮助?