在 Xiaomi Agent(MiMoCode)里统计第三方 API 的 Token 用量

从「只有小米订阅才有用量通知」到「任意 OpenAI 兼容网关都能本地记账 + 按模型分色报表」

背景:为什么官方通知不够用

用 Xiaomi Agent / MiMoCode 时,有一个很实际的问题:

  • 走小米官方订阅时,平台侧通常能告诉你大概用了多少 token;
  • 一旦你把模型切到 第三方 API(例如 OpenCode Go、OpenRouter、自建兼容网关),官方账单/通知往往就对不上了——因为流量根本没走小米计费通道。

于是需求变得很具体:

  1. 无论对接什么平台的 API,都要能统计 token;
  2. 能记到 缓存命中 / 未命中(这直接决定花费);
  3. 能 配置模型单价,看到预估人民币费用;
  4. 能按 天 出图:Token 用量、费用,并且 按模型区分颜色。

最终落地形态是一个很轻的 MiMoCode 插件:

产物 路径 作用
原始明细 data/usage.csv 持久化、可 Excel 打开
可视化日报 report/report.html 默认「今天」,下拉切换 7/30 天
价目表 plugin/prices.json 元 / 百万 tokens

下面按「问题 → 方案 → 实现 → 使用」完整讲一遍。


一、方案选型:为什么是插件,而不是代理或平台账单

可选路径大致有三条:
aea6320a833572705c335ce48c30336f

Error: Lexical error on line 9. Unrecognized text.
...-> H[仅该平台]  C -. 推荐:轻量、跨平台 .-> G
----------------------^
方案 优点 缺点
插件(最终选择) 不改主程序、不改流量路径;换 provider 仍生效 依赖响应里回传 usage
本地反向代理 在 HTTP 层抄 usage,最「底层」 要改 baseURL、多维护一跳服务
只靠平台账单 零开发 换第三方 API 就断了

MiMoCode 兼容 OpenCode 的插件体系(@mimo-ai/plugin)。关键在于:客户端每次 LLM 调用结束后,assistant 消息轨迹里已经带了 token 明细,不需要破解上游协议,也不需要劫持流量。

因此结论很干净:

统计点放在 Agent 本地出口(插件钩子),而不是某个平台的订阅后台。


二、数据从哪来:插件钩子与 tokens 结构

2.1 用哪两个钩子

钩子 时机 本插件职责
session.userQuery.post 每一步 LLM 结束后 唯一写入 CSV 的地方(避免重复记账)
session.post 整段 session 结束 只刷新 HTML 报表

刻意不在 session.post 再写一遍 CSV:它的 trajectory 是全量轨迹,和逐步钩子叠在一起会 double count。

2.2 trajectory 里的 tokens 长什么样

每条 assistant 消息大致有:

{
  role: "assistant",
  providerID: "opencodego",   // 或 xiaomi / openrouter / ...
  modelID: "mimo-v2.6-flash",
  tokens: {
    total?: number,
    input: number,            // 通常视为「未命中缓存」的输入
    output: number,
    reasoning: number,
    cache: {
      read: number,           // 缓存命中读
      write: number           // 缓存写入
    }
  },
  cost?: number               // 系统若有定价可能已算过,币种/口径不一定符合你的价目表
}

要点:

  • 缓存命中 token 是现成的(cache.read),不必自己猜;
  • 命中率可在聚合时计算:
    hitRate = cache.read / (input + cache.read);
  • 自定义价目时,不要盲目信任系统 cost 字段的币种——我们自己按「元/百万 tokens」重算更可控。

2.3 CSV 落盘格式

每步追加一行(示意):

date,time,sessionID,agentID,step,providerID,modelID,input,output,reasoning,cache_read,cache_write,total,cost_cny
2026-09-24,11:45:04,ses_xxx,main,4,opencodego,mimo-v2.6-flash,8985,91,199,33152,0,42427,0.00511402

为什么选 CSV 而不是 TXT/SQLite:

  • 结构固定,Excel / WPS 直接打开;
  • 程序 append 简单;
  • 个人本地统计不需要再引入数据库。

读入时会过滤:日期非法、模型为空、total<=0 的脏行。


三、费用怎么算:价目表与公式

3.1 单位约定

统一为:

元(CNY) / 百万 tokens(per 1M tokens)

3.2 已配置的小米官方价(示例)

模型 输入(未命中) 输入(命中缓存) 输出
mimo-v2.6-pro ¥1.50 ¥0.0125 ¥3.00
mimo-v2.6-flash ¥0.50 ¥0.01 ¥1.00

deepseek-flash 这类 分时段计价 的模型:先不写死单价(fallback: null),token 照记,费用记 0,避免用错价。

3.3 公式

cost_cny = (
    input        × 未命中价
  + (output + reasoning) × 输出价     // reasoning_as_output: true
  + cache.read   × 命中价
  + cache.write  × 缓存写价
) / 1_000_000

plugin/prices.json 示例:

{
  "currency": "CNY",
  "unit": "per_1m_tokens",
  "models": {
    "mimo-v2.6-pro": {
      "input_miss": 1.5,
      "output": 3,
      "cache_read": 0.0125,
      "cache_write": 0,
      "reasoning_as_output": true
    },
    "mimo-v2.6-flash": {
      "input_miss": 0.5,
      "output": 1,
      "cache_read": 0.01,
      "cache_write": 0,
      "reasoning_as_output": true
    }
  },
  "fallback": null
}

3.4 系统侧 cost 是否也要写?

可以两边都写:

  1. 插件 prices.json:报表与 CSV 的权威人民币价目;
  2. 全局 mimocode.jsonc 里模型的 cost:让运行时也有定价参考。

注意:系统消息上的 cost 文档口径常见为 USD 约定,和你的人民币价目 不一定同一币种。因此:

  • 对账、博客里的「预计花费」以插件 cost_cny 为准;
  • 系统 cost 仅作补充,不混加。

配置片段示意:

// ~/.config/mimocode/mimocode.jsonc
"mimo-v2.6-flash": {
  "name": "MiMo-V2.6-Flash",
  "cost": {
    "input": 0.5,
    "output": 1,
    "cache_read": 0.01
  }
}

四、HTML 日报:交互与图表设计

报表是 单文件静态 HTML:生成时把「日期 × 模型」汇总嵌进页面,浏览器里过滤,切换范围不用重新写文件。

4.1 时间范围

  • 默认:今天
  • 下拉框可选:近 7 天 / 近 30 天
    (不是一排分段按钮,更接近常见后台的「时间范围」选择)

切换后联动:

  • 总卡片(Token / 费用 / 日均 / 调用数 / 模型数)
  • 每个模型一张卡(Token、占比、费用、调用)
  • Token 按模型堆叠柱状图
  • 费用 按模型分色折线
  • 缓存命中率折线
  • 明细表(含模型列与色点)

4.2 为什么必须按模型分色

真实场景里经常是:

今天 flash 用了上千万 token,pro 只有几百万,deepseek 只有一点点。

若只画一条总曲线,大模型会把小模型压成噪声。按模型分色后:

  • 堆叠柱一眼看出结构占比;
  • 费用多线对比谁在烧钱;
  • 模型卡片给出「今日 flash ¥x / pro ¥y」这种可读结论。

颜色按 modelID 稳定分配(排序后循环取调色板),刷新不会串色。

4.3 页面结构(逻辑示意)

[时间范围 ▾ 今天]     ← select#rangeSelect
[图例:色块 + 模型名]

[总卡片] [总卡片] ...
[模型卡 flash] [模型卡 pro] ...

[Token 堆叠柱状图]
[费用分色折线]
[命中率折线]
[明细表:日期 | 模型 | ... | 费用]

五、完整接入步骤

5.1 目录结构

<YOUR_PROJECT_ROOT>/
├── plugin/
│   ├── index.js        # 插件入口
│   └── prices.json     # 价目表
├── data/
│   └── usage.csv       # 调用明细(本地,勿随意公开)
├── report/
│   └── report.html     # 日报
└── README.md

5.2 注册插件

编辑 全局 配置(对所有项目生效):

~/.config/mimocode/mimocode.jsonc

"plugin": [
  ["<YOUR_PROJECT_ROOT>/plugin/index.js", {
    "csvPath": "<YOUR_PROJECT_ROOT>/data/usage.csv",
    "reportPath": "<YOUR_PROJECT_ROOT>/report/report.html",
    "pricesPath": "<YOUR_PROJECT_ROOT>/plugin/prices.json",
    "autoReport": true
  }]
]

路径按你的机器改成绝对路径(示例中的 <YOUR_PROJECT_ROOT> 请替换)。
改完后 重启 MiMoCode 或新开会话,否则旧进程可能仍用内存里的旧代码,甚至把 report.html 刷回旧版。

5.3 正常使用

不需要额外命令:

  1. 正常聊天 / 跑 agent;
  2. 每步结束自动 append CSV;
  3. session 结束刷新 HTML;
  4. 浏览器打开 report/report.html。

5.4 校验是否生效

  • CSV 是否出现 sessionID 以 ses_ 开头的新行;
  • HTML 是否有「时间范围」下拉框、模型分色卡片;
  • 若一直为空:优先怀疑 配置改完没重启。

5.5 费用自检(可用样例)

以 pro、单位「元/百万」为例:

输入未命中 输出 缓存命中 计算 结果
1,000,000 100,000 2,000,000 1.5 + 0.3 + 0.025 ¥1.825
500,000(flash) 60,000 1,000,000 0.25 + 0.06 + 0.01 ¥0.32

未配置价目的模型:cost_cny = 0,但 token 仍入账。


六、跨平台:为什么第三方 API 也能统计

因为插件读的是 MiMoCode 运行时已解析好的 tokens,不是某个平台的账单 API:

任意 OpenAI 兼容网关
        │
        ▼
  返回 usage(input/output/cache)
        │
        ▼
  AI SDK / 运行时写入 assistant.tokens
        │
        ▼
  插件 session.userQuery.post
        │
        ├─► 本地价目 → cost_cny
        └─► CSV + HTML

只要满足:

  1. 上游(或运行时归一化后)能给出 input / output / cache;
  2. 你在 prices.json 里为 该 modelID 配了价(或接受费用为 0)。

就能统计。换 provider 时通常只需确认:

  • modelID 字符串与价目表 key 一致(或你改表);
  • 响应确实带 usage(极少数流式接口不回 usage 会记不到——这在代理方案里同样躲不掉)。

七、开发中踩过的坑(博客里值得写)

  1. 重复记账
    session.userQuery.post 与 session.post 都写 CSV → 翻倍。
    解法:逐步钩子写数据,结束钩子只出报表。

  2. 扁平行 vs 嵌套 tokens
    CSV 行是 cache_read,计费函数若只读 tokens.cache.read 会恒为 0。
    解法:cache?.read ?? cache_read 双兼容。

  3. 旧进程覆盖新报表
    改完 index.js 但未重启,旧模块继续 writeFile(report),表现为「下拉框总是消失」。
    解法:改插件后必须重启/新开会话;排查时看 report.html 是否还有 rangeSelect。

  4. 虚拟测试数据污染
    开发时写入的 sessionID=s 样例会让金额虚高。
    解法:只保留 sessionID 以 ses_ 开头的真实行;测完删掉测试数据与备份。

  5. 币种与「每百万」
    口径不写清楚会差 6 个数量级或币种错位。价目文件显式写 unit: per_1m_tokens、currency: CNY。

  6. 分时段模型不要硬编码单价
    deepseek 等按时段变价 → fallback: null,宁可费用 0,不要错账。


八、安全与隐私注意

  • CSV/HTML 含你的 使用习惯、模型偏好、会话 ID,默认当本地隐私文件,不要随手公网分享;
  • 配置里的 API Key 不要贴进博客/截图;
  • 插件只读会话轨迹并写本地文件,不对外上传(除非你自己加)。

九、效果截图应拍什么

写博客配图时建议按这个顺序截:

  1. 全局配置里的 plugin 注册片段(打码 key);
  2. prices.json 价目表;
  3. 报表顶部:时间范围下拉框 + 图例;
  4. 今日总卡片 + 分模型卡片(flash / pro 不同颜色左边条);
  5. Token 堆叠柱状图;
  6. 费用 分色折线;
  7. 明细表含 模型列;
  8. (可选)Excel 打开的 usage.csv。

十、总结

问题 答案
小米订阅外的第三方 API 怎么统计? 本地插件读运行时 tokens,与平台账单解耦
能不能记缓存? 能:cache.read / cache.write,并算命中率
能不能配价格看花费? 能:prices.json 按模型配「元/百万」
能不能按天出图? 能:默认今天,下拉 7/30 天
Token 和费用要不要分模型? 要:堆叠柱 + 分色折线 + 模型卡片
插件作用域? 写在全局 mimocode.jsonc → 所有项目生效

一句话:

在 Xiaomi Agent 上统计第三方 token,正确层级不是「求平台多发一条通知」,而是 在 Agent 出口挂一个可插拔的记账插件——CSV 落真实数据,价目表算人民币,HTML 按模型和时间范围给你看结构。

如果你也在用多 provider 混跑,这套思路可以原样复用:只改 prices.json 和路径,就能覆盖你自己的网关组合。


附录 A:插件职责一览

tokenUsagePlugin(options)
├─ options.csvPath / reportPath / pricesPath / autoReport
├─ loadPrices()
├─ priceFor(modelID)
├─ calcCostCny(tokens, price)
├─ aggregateByDayModel(rows)
├─ writeReport()          # 生成带下拉框的单文件 HTML
├─ session.userQuery.post # 提取 trajectory → append CSV → 可选刷报告
└─ session.post           # 仅刷新报告

附录 B:价目与配置文件清单

  • 插件价目:plugin/prices.json
  • 系统模型 cost:~/.config/mimocode/mimocode.jsonc
  • 插件注册:同文件 plugin 数组
  • 数据:data/usage.csv
  • 报表:report/report.html

附录 C:常见问题 FAQ

Q:换了 OpenRouter / 自建网关,要改插件吗?
A:一般不用改代码;确认 modelID 并在 prices.json 补价即可。

Q:为什么有的行费用是 0?
A:该 modelID 未配置价目,或 token 全 0 被过滤前的历史行。

Q:报表和 CSV 对不上?
A:先看是否旧进程刚覆盖 HTML;强制用当前 index.js 再生成一次。

Q:可以按 session 导出吗?
A:CSV 已有 sessionID 列,Excel 筛选即可;也可自行加导出按钮。


附录 D:让 Xiaomi Agent 从零生成这个插件(完整 Prompt)

如果你读完文章想自己落地,不必手写全部代码。把下面整段 Prompt 粘贴给 Xiaomi Agent(MiMoCode),按提示改路径和价目即可。

使用前请先改三处占位符:
1)项目根目录;2)全局配置里要写入的插件绝对路径;3)你的模型与官方单价(元/百万 tokens)。

复制以下 Prompt

请在指定项目目录从零实现一个「MiMoCode / Xiaomi Agent 本地 Token 用量统计插件」,并完成注册与自检。请严格按下列规格实现,不要擅自简化。

【目标】
无论对接小米订阅还是任意 OpenAI 兼容第三方 API,都在 Agent 本地统计 token、缓存命中与预估费用,并生成可按时间范围筛选、按模型分色的 HTML 日报。

【项目路径】
- 项目根目录:`<YOUR_PROJECT_ROOT>`(例如 F:/GitHub/xxx)
- 必须创建:
  - `<ROOT>/plugin/index.js`
  - `<ROOT>/plugin/prices.json`
  - `<ROOT>/data/usage.csv`(可先只有表头)
  - `<ROOT>/report/report.html`(由插件生成)
  - `<ROOT>/README.md`(简要使用说明)
- 插件将注册到全局配置 `~/.config/mimocode/mimocode.jsonc` 的 `plugin` 数组,路径使用绝对路径指向上面的 `plugin/index.js`。

【价目表 prices.json】
- currency: "CNY"
- unit: "per_1m_tokens"   // 元 / 百万 tokens,必须写死这个单位
- 按下面表格写入 models(仅这些模型计费;其它模型 fallback: null,即 token 照记、费用记 0):
  - mimo-v2.6-pro: input_miss=1.5, output=3, cache_read=0.0125, cache_write=0, reasoning_as_output=true
  - mimo-v2.6-flash: input_miss=0.5, output=1, cache_read=0.01, cache_write=0, reasoning_as_output=true
- 分时段计价模型(如 deepseek-flash)不要写死单价,保持 fallback 为 null。
- 可选:在 mimocode.jsonc 对应模型上写入 cost 字段(同一套每百万数值),但报表与 CSV 的人民币费用必须以 prices.json 为准,不要把系统 cost(可能是 USD 口径)和 cost_cny 混加。

【插件运行时行为】
1. 导出 ESM default 插件函数:`(input, options) => Promise<Hooks>`。
2. options 支持:csvPath、reportPath、pricesPath、autoReport(默认 true);缺省时用项目内相对路径。
3. 必须实现钩子:
   - `session.userQuery.post`:从 `input.trajectory` 提取 assistant 消息的 tokens,**唯一**负责 append CSV;写入成功后若 autoReport 则刷新 HTML。
   - `session.post`:**禁止再写 CSV**(全量 trajectory 会与逐步钩子重复记账);只刷新 HTML 报表。
   - 可选 `config`:重载 prices.json。
4. 从 trajectory 提取字段:
   - sessionID、agentID、step、providerID、modelID
   - tokens.input / output / reasoning / tokens.cache.read / tokens.cache.write / total
   - 费用用本地价目计算,写入 cost_cny;不要依赖系统 cost 币种。
5. 费用公式(必须实现):
   cost_cny = ( input*input_miss + (output+reasoning)*output_price + cache.read*cache_read + cache.write*cache_write ) / 1e6
   - 若 reasoning_as_output===false,则输出侧只用 output。
   - 找不到价目(fallback null)则 cost_cny=0,但 token 仍写入。
6. 计费函数必须同时兼容两种缓存字段:
   - trajectory 嵌套:tokens.cache.read / tokens.cache.write
   - CSV 扁平行:cache_read / cache_write
   (历史坑:只读 tokens.cache.read 会导致缓存价永远算不进费用。)

【CSV 规格】
- 表头固定:
  date,time,sessionID,agentID,step,providerID,modelID,input,output,reasoning,cache_read,cache_write,total,cost_cny
- 每次 append 一行;文件不存在则先写表头。
- 读取生成报表时过滤脏行:date 必须匹配 YYYY-MM-DD;modelID 非空;total>0。
- 不要写入任何虚构/测试 session 数据;真实 sessionID 形如 ses_ 开头。若开发时需要造数,测试结束后必须删除测试行,只保留真实数据。

【HTML 日报表规格】
- 单文件静态 HTML,无外部 CDN/网络依赖;数据在生成时嵌入 JS(如 ALL_ROWS、COLORS、TODAY)。
- 顶部必须有 **下拉框**(不是分段按钮)选择时间范围:
  - 默认选中「今天」(value=1)
  - 可选「近 7 天」(7)、「近 30 天」(30)
  - 使用 <select id="rangeSelect">,change 时调用 render(),不要用一排 button。
- 图例:每个模型固定颜色(按 modelID 排序后循环调色板,保证同模型同色)。
- 卡片:
  - 总览:Token 合计、费用合计、日均 Token、日均费用、调用次数、模型数
  - 每个模型一张卡:Token、占比%、费用、调用数;左边框用该模型颜色
- 图表(均按当前时间范围过滤后绘制):
  1. Token:按天 **堆叠柱状图**,不同模型不同颜色(title 显示 模型: 数值)
  2. 费用:按天 **多折线**,每模型一条线、颜色与图例一致
  3. 缓存命中率:合计 hitRate = cache_read/(input+cache_read),单线即可
- 明细表列:日期、模型(带色点)、调用、输入、输出、推理、缓存读、缓存写、合计、命中率、费用
- 聚合粒度:date × modelID(不要只按天合并掉模型)
- 空范围显示「该时间范围内暂无数据」

【注册】
- 编辑 `~/.config/mimocode/mimocode.jsonc`:
  - 追加/合并 `plugin` 数组:`["<ABS_PATH>/plugin/index.js", { csvPath, reportPath, pricesPath, autoReport:true }]`
  - 不要覆盖已有 provider/mcp/apiKey 等无关字段;JSONC 注释尽量保留。
  - 为 mimo-v2.6-pro / mimo-v2.6-flash 写入 cost(若不存在则只加 cost,不要乱编 limit/modalities)。
- 明确告知用户:改配置后必须 **重启 MiMoCode 或新开会话** 才会加载新插件。

【自检(必须做)】
1. 用 node 动态 import 插件,确认 default 是 function,能拿到 session.userQuery.post / session.post。
2. 用临时 trajectory 模拟一次调用写入 CSV:
   - pro: input=1e6, output=1e5, cache.read=2e6 → cost_cny 必须为 1.825
   - flash: input=5e5, output=5e4, reasoning=1e4, cache.read=1e6 → cost_cny 必须为 0.32
   - 未配置价目模型 → 0
3. 生成 report.html 后断言包含:
   - id="rangeSelect"、value="1" selected、value="7"、value="30"
   - 不存在旧版分段按钮 data-range=
   - 含 ALL_ROWS、modelCards、renderStackedBar
4. 自检用的虚拟 CSV 行 **全部删除**,只保留真实 ses_ 数据(若尚无真实数据则留表头)。
5. 删除临时测试文件/备份,避免残留假数据。

【明确禁止的坑(历史踩坑,务必避免)】
1. 禁止在 session.post 再次 append CSV(重复记账)。
2. 禁止计费只读 tokens.cache.read 而忽略 CSV 扁平 cache_read(缓存价丢失)。
3. 禁止改完插件后假设已生效:必须提示重启;生成后检查 HTML 是否被旧进程覆盖(旧版特征:出现「共 N 天」静态 meta、没有 rangeSelect)。
4. 禁止用分段按钮代替时间下拉框;禁止默认展示全部历史而非「今天」。
5. 禁止价目单位含糊:必须是 CNY + per_1m_tokens;禁止把未配置模型随便填一个假单价。
6. 禁止把 API Key 写进 HTML/README/日志;不要回显 mimocode.jsonc 里的 apiKey。
7. 禁止留下 sessionID=s / test-session 等测试数据;测完只留真实数据。
8. 禁止未确认就 npm/pip 全局安装依赖;本插件应仅用 Node 内置 fs/path/url。
9. 禁止整文件盲目覆盖用户已有 mimocode.jsonc;做最小编辑。
10. 禁止图表只按天汇总而不区分模型(必须 date×model 堆叠/分线)。

【交付物】
- 上述目录中的完整可运行文件
- README:注册片段、价目表、费用公式、重启说明、报表打开路径
- 自检结果摘要(费用数值、HTML 关键标记、CSV 是否仅真实数据)

请先读当前目录与全局配置,再实现;实现完成后按【自检】逐项汇报结果。若路径/价目与占位符不一致,先向我确认再写入全局配置。

使用这段 Prompt 时的建议

  1. 先建空目录再粘贴 Prompt,避免 Agent 扫到无关大仓库。
  2. 价目若与示例不同,只改 Prompt 里的表格,不要让 Agent「自行上网猜价」。
  3. 实现完成后务必 重启会话,再打开 report/report.html 看下拉框与分色图。
  4. 若报表突然变回无下拉框的旧样式,按正文「旧进程覆盖」一节排查,而不是先改业务逻辑。
  5. 开发阶段允许 Agent 造临时数据,但 Prompt 已要求 测完删除;合并前再人工确认 CSV 只有 ses_ 真实行。

本文对应实现为本地 MiMoCode 统计插件实践,路径与价格表请按你的环境替换。