Skip to content

Token 仪表盘 / Token Meter Board: 本机 token 用量实时看板(逐次调用台账、turn 级对话明细、与客户端同口径的输出速率) - #60

Open
HaneWari wants to merge 1 commit into
MiniMax-AI:mainfrom
HaneWari:feat/token-meter-board
Open

HaneWari wants to merge 1 commit into
MiniMax-AI:mainfrom
HaneWari:feat/token-meter-board

Conversation

@HaneWari

@HaneWari HaneWari commented Oct 22, 2026 •

Copy link
Copy Markdown

中文摘要

提交 Token 仪表盘(插件 ID token-meter-board):本机 MiniMax Code 的实时 token 用量看板。直读运行时自己的逐次调用台账与观测日志,展示的是客户端真实记录的数字;输出速率采用与产品内界面一致的口径(Σ输出 ÷ Σdecode,时间加权)。纯 Node,无需 Python,全程只读、不出网、无遥测。

界面为中文,README 提供英文与中文两版。预览图为合成数据实拍,不含任何真实会话内容。


What it does

A local, read-only token-usage dashboard for MiniMax Code. It reads the runtime's own per-call usage table (local_runtime_token_usage) and the observability logs, so the numbers it shows are the ones the client actually recorded — not an estimate reconstructed from session transcript files.

  • Usage trend — stacked bars per model, with the cache-hit-rate and output-speed curves drawn on top. Buckets adapt from minutes to hours to days as the window grows.
  • Five detail tables — per-model breakdown, daily totals, a 12-column per-call ledger, a conversation (turn) table, and a session ranking.
  • Per-call ledger — every model call with its exact timestamp, model, all five token classes, output speed, whole-request speed, decode duration, request duration and HTTP status code.
  • Conversation (turn) table — one row per question you asked, showing your own message text, the outcome, wall-clock duration, how many model calls the turn took, the tokens and the rate. A running turn is marked unfinished and grows live; nothing has to be closed or archived first.
  • Time windows — last hour / today / last 7 days / last 30 days, or a custom range. The time bar is laid out in two rows so the native date picker can never cover the Apply button.
  • 5-second auto refresh, and switching a window never blanks the tables or waits.

Token Meter Board with synthetic data

How it differs from the two token boards already in the gallery

Both existing boards are good; they solve a slightly different problem.

Token Meter Board (this one) mcode-token-usage-board mcode-usage-monitor
Source of truth Runtime usage table + observability logs Session transcript files (messages.jsonl) Runtime database, read through Python 3.8+
Per-call timings Yes — decode, request, status code per call No — transcripts carry no timings Partly — per-request speed
Per-call ledger Yes, 12 columns No Recent requests only
Conversation (turn) table with your own words Yes No No
Needs Python No No Yes
Extra process per query No No Yes
Auto refresh 5 s 60 s 5 s / 10 s / 30 s
Layout Fixed, dense Fixed Draggable, user-editable cards

The practical difference: if you want to know where the tokens went in one specific conversation, or to audit one specific call (its decode duration, its request duration, its HTTP status), only this board can show it — without re-deriving anything from transcript files. If you prefer a freely rearrangeable card layout, mcode-usage-monitor is the one to look at.

Measured on a reference machine against a transcript-based board, that board reported about 48% fewer calls and 66% fewer cache-read tokens overall, with the gap swinging between 0% and 98% from day to day — because some sessions leave no transcript file at all, so a transcript-based board cannot see them.

Correctness notes

The headline rate uses the same basis as the client interface: Σ output tokens ÷ Σ decode duration, time-weighted, with P50/P90 shown next to it.

Each usage row is paired with the assistant message carrying that call's decode/request durations. The rule is "the first message of the turn written at or after the call started" — not index position and not nearest timestamp, because the runtime also writes assistant rows that are not model calls (context compaction). On this machine 490 of 17,214 assistant messages carry no usage block; counting them as calls shifted a whole turn by one, so 22.5% of the calls reported the previous call's duration and rate. The rule above was verified against a token-signature ground truth over 16,723 calls with zero mismatches, and it stays correct when the query window starts in the middle of a turn. Details in the package's docs/validation.md.

Install and open

Copy plugins/hanewari/token-meter-board/ into your MiniMax Code data directory (by default ~/.minimax/plugins/token-meter-board/), keep the hidden .minimax-plugin directory, restart MiniMax Code, confirm the plugin is enabled, and open Token 仪表盘 — or just ask the Agent to open it. Full instructions are in the package README.

Data & access

Files read (read-only, never modified):

Path Used for
<dataDir>/v2/sqlite/runtime-state.sqlite The per-call usage ledger and the assistant/user message rows (decode timings, finish reason, message text). Opened with node:sqlite in read-only mode.
<dataDir>/v2/observability/logs/runtime-*.log Provider, model and response status, correlated per turn. The usage table itself does not store the model name, so the logs are what make custom (BYOK) providers show up as themselves instead of "unknown".
<dataDir>/config.yaml Provider and model display names.

These paths and the runtime database layout are not a published API — they are the client's internal storage. This is stated in the README; if a future client changes them the board shows an explicit error instead of an empty dashboard.

What the page displays (so screenshots can carry more than you might expect): the first 200 characters of each turn's own user message (truncated inside the database query, so the full text never enters memory or the browser), session titles and ids, the absolute workspace path of each session (session-ranking tooltip), and model/provider names including your own custom providers.

Written: nothing. No files are created or modified, no data leaves the machine, no telemetry. The only cache is in the running process's memory. Browser storage holds exactly one thing: the theme choice, under the key tmb-theme.

Network: none. The Mini App serves one page on 127.0.0.1 and answers its own API routes. Query parameters are an explicit per-endpoint allow-list, so a crafted query string cannot reach the query layer with an unexpected key.

Subprocess: normally none. If the host's Node runtime does not expose node:sqlite, the identical read-only queries are re-run in a child process. It first tries node from PATH and only falls back to the runtime's own executable with ELECTRON_RUN_AS_NODE=1; the query mode and parameters are passed as an argv array, never through a shell, and the child prints one JSON document on stdout and exits. No user input is placed in a command line — the mode is from a fixed set and the parameters are the same allow-listed values the in-process path receives.

Node.js: the in-process path needs node:sqlite, unflagged from Node.js 23.4. The runtime used for testing is Node.js v24.18.0.

Tested environment

  • MiniMax Code Desktop 3.1.4 (build 186), Windows 11 (10.0.22631, x64)
  • Bundled Node.js v24.18.0, node:sqlite available
  • Verified in the desktop client: plugin install, window switching, custom start/end windows, per-call ledger, live turn growth, auto refresh, light and dark themes, and the error path when the runtime database is missing.
  • macOS and Linux are not verified. Nothing in the code is platform-specific — the data paths come from context.dataDir — but please treat the other platforms as untested.

Validation results

  • npm run validate → plugins/hanewari/token-meter-board: 0 errors, 1 warning, and the repository-level check reports repository: OK (the plugin ID is unique across the repository, and both root READMEs now link to it).
  • npm test → 67 of 69 pass. The two failures are not caused by this package: they are the repository's own test/files.test.mjs cases that create a symbolic link in a temporary fixture to prove the validator rejects one. On this Windows account fs.symlink returns EPERM (no SeCreateSymbolicLinkPrivilege, i.e. Developer Mode is off), so the fixture cannot be created — reproduced independently of this package with a standalone fs.symlink call in a temp directory. CI runs on ubuntu-latest, where symlinks are available.

About the ENTRY_STDOUT warning

The warning points at miniapp/node/lib/db-child.mjs, not at the Host entry miniapp/node/server.mjs. That file is the child-process executor we spawn ourselves when the host runtime lacks node:sqlite; it writes one JSON document to stdout, which the parent that spawned it reads back over that pipe — it is parent/child IPC, not the Host process's output channel. The Host entry never writes to stdout (the checker treats that as an error, and it did not fire).

The rule is a static scan — /\b(?:console\.(?:log|info|debug|dir|table)|process\.stdout\.write)\s*\(/u — and cannot tell "the Host entry's stdout" from "the stdout of a child process we own". Following the same approach the repository already accepts for plugins/webguhui/minimax-photo, the warning is left in place and explained here rather than worked around: the checker is not modified, no third-party or first-party code is hidden, and the implementation is not contorted into a file channel — which would contradict the "writes nothing" disclosure above. This code path does not run at all on a host with node:sqlite.

Third-party components

Chart.js v4.4.3 — vendored at miniapp/client/vendor/chart.umd.min.js for offline use. MIT License, © Chart.js Contributors. The full licence text ships with the package at miniapp/client/vendor/LICENSE-chartjs.txt.

License

Apache-2.0. The vendored Chart.js copy keeps its own MIT licence, reproduced in miniapp/client/vendor/LICENSE-chartjs.txt.


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.

A local, read-only token-usage dashboard for MiniMax Code. It reads the
runtime's own per-call usage table and observability logs, so it shows the
numbers the client actually recorded rather than an estimate reconstructed
from session transcript files.

- Usage trend with per-model stacked bars and cache-hit-rate / output-speed
  curves; five detail tables (per-model, daily, a 12-column per-call ledger,
  a turn-level conversation table, session ranking).
- Output speed uses the same basis as the client interface:
  Sigma output tokens / Sigma decode duration, time-weighted, with P50/P90.
- Every call is paired with its own decode and request durations. The pairing
  rule was verified against a token-signature ground truth over 16,723 calls
  with zero mismatches.
- No Python, no extra process in the normal path, no third-party runtime code
  except a vendored copy of Chart.js (MIT, full licence text included).
- Strictly read-only: no files written, no network, no telemetry. Query
  parameters are an explicit per-endpoint allow-list.

Includes the English README with the required "## Tested environment" and
"## Data & access" sections, a Chinese README, an Apache-2.0 LICENSE, a
synthetic-data preview image, and a validation record.

@MyPrototypeWhat MyPrototypeWhat left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @HaneWari,欢迎第一次向本仓库投稿!逐次调用台账、turn 级对话明细、以及和客户端同口径的输出速率,方向很清楚;文档也写得很完整(Data & access、与另外两块看板的对照、ENTRY_STDOUT 的说明、合成数据预览)。在 0414d527b9d9d53a2701ed7ab7153643e9588286 上本地看过:范围只有 plugins/hanewari/token-meter-board/ 和两个根 README 各一行;Apache-2.0 + 内置 Chart.js 的 MIT 文本齐全;代码路径上确认是 DatabaseSync(..., { readOnly: true }),没有对宿主文件的写入;npm run check 对本包是 0 errors, 1 warning(就是你说明过的 db-child.mjs stdout)。

不过有一项仓库红线,这一版没法合入。

必须修改

  1. 请去掉对 MiniMax Code 内部数据库的直接访问(只读也不行)

社区插件不能直接打开 MiniMax Code 的内部数据库文件,读、写都不允许。PR / README 里写的数据来源,和代码行为是一致的,并不是「文档写了、代码其实没开」:

  • miniapp/node/server.mjs:24-34:从 context.dataDir 向上最多 8 层,专门查找宿主数据目录里的内部数据库文件,再落到 resolveDataDir()。
  • miniapp/node/lib/common.mjs:39-55:dbPath / openDb 用 node:sqlite 的 DatabaseSync 打开该库(readOnly: true)。
  • 查询侧实际读到的表包括用量台账、消息行、会话行:common.mjs:74-78(local_runtime_token_usage)、facts.mjs:46-53(local_runtime_message_rows,并在 SQL 里 substr 出用户消息前 200 字)、queries.mjs:67(local_runtime_sessions 的 title / workspace_dir)。
  • 同一条数据路径还顺带读了宿主观测日志与配置:common.mjs:304(v2/observability/logs)、common.mjs:137-158(config.yaml,整文件读入再解析显示名)。

因此「只读」「不出网」「参数白名单挡住 dataDir」这些加固都成立,也改变不了:插件在打开并查询宿主内部库。请整条数据路径拿掉(数据库、为关联模型/状态而扫的观测日志、以及为显示名去读的宿主 config.yaml)。没有这条路径之后,看板需要改成不依赖宿主内部库的数据来源(例如画廊里已有、基于会话记录文件的做法),或先收缩到不碰宿主内部库的功能;README 的 Data & access 也请一并改到与真实行为一致。

页面实际会带出的内容(在去掉上述访问之前,也请心里有数——这些都来自那座库):对话表明细的 userPreview(用户消息前 200 字,facts.mjs:36 / queries.mjs:308)、会话标题与 id、会话排行 tooltip 里的绝对工作区路径(queries.mjs:392)。用户若把密钥或路径贴进对话,前 200 字会出现在本机页面的 JSON 响应里。

建议(不阻塞;当前红线未解决前可先放着)

  • /api/health 与查询 meta 带绝对 dataDir(server.mjs:173,queries.mjs:98):成功路径也会把宿主数据目录绝对路径送给页面。若以后有不依赖内部库的版本,建议改成 basename,或干脆不返回。
  • 报错文案里的绝对路径(例如 common.mjs:52 的 not found at ${file}):同样建议脱敏。
  • Host / Origin:入口目前只校验 GET。可参考仓库里已有插件对 loopback Host(以及写操作时的 Origin)的检查,降低 DNS rebinding / 跨站误打到本机端口的风险。
  • ENTRY_STDOUT:你对 db-child.mjs 的说明和 minimax-photo 的处理方式一致,这条 warning 可以保留;若整条子进程读库路径随红线一起删掉,warning 也会自然消失。

谢谢投稿。去掉对宿主内部数据库(以及同路径上的观测日志 / 宿主配置)的访问之后,很乐意再看一版。

if (envDir && fs.existsSync(envDir)) return path.resolve(envDir);
let dir = contextDataDir ? path.resolve(contextDataDir) : null;
for (let i = 0; dir && i < 8; i += 1) {
if (fs.existsSync(path.join(dir, 'v2', 'sqlite', 'runtime-state.sqlite'))) return dir;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

必改: 这里从插件 dataDir 向上查找并定位 MiniMax Code 的内部数据库文件。社区插件不能直接打开宿主内部数据库(只读也不行),请去掉整条数据路径。

return { db: null, error: `runtime state database not found at ${file} — is this a MiniMax Code data directory with recorded usage?` };
}
try {
return { db: new sqlite.DatabaseSync(file, { readOnly: true }), error: null };

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

必改: DatabaseSync 直接打开了 MiniMax Code 的内部数据库。请删除 openDb / dbPath 以及对宿主内部库的查询;只读模式不能作为例外。

${jsonField('$.usage.request_duration_ms', "IN ('integer','real')")} AS request_ms,
${jsonField('$.finish_reason', "= 'text'")} AS finish_reason,
substr(${jsonField('$.msg_content', "= 'text'")}, 1, ${USER_PREVIEW_CHARS}) AS msg_preview
FROM local_runtime_message_rows

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

必改: 这里全表扫描宿主内部库的消息行,并取出用户消息前 200 字。请不要读宿主内部库;对话预览也不能再依赖这条路径。

function loadSessionTitles(db) {
const titles = new Map();
try {
for (const r of db.prepare('SELECT session_id, title, agent_name, workspace_dir FROM local_runtime_sessions').all()) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

必改: 从宿主内部库的会话表读取 title / workspace_dir。请去掉对宿主内部库的访问;绝对工作区路径也不应再经插件 API 带出。

const maxBytesPerFile = options.maxBytesPerFile ?? 12 * 1024 * 1024;
const now = options.now ?? Date.now();
const map = new Map();
const dir = path.join(dataDir, 'v2', 'observability', 'logs');

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

必改: 观测日志属于宿主内部存储,且是为补齐内部库里没有的模型名而扫的。请与内部库访问一并移除。

let catalogCache = null;

export function loadCatalog(dataDir) {
const file = path.join(dataDir, 'config.yaml');

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

必改: 这里整文件读取宿主 config.yaml(可能含自定义 provider 配置)。请不要读宿主配置文件;显示名需改用不依赖宿主内部文件的方式,或先去掉。

sqliteInProcess,
queryMode: childMode === true ? 'child' : 'in-process',
},
dataDir,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

建议:/api/health 返回了绝对 dataDir。若以后还有健康检查,请只返回 basename,或省略该字段。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants