Skip to content

feat(usage): sync aggregate Hermes usage - #6120

Closed
xrbs00 wants to merge 14 commits into
farion1231:mainfrom
xrbs00:feat/hermes-session-usage
Closed

xrbs00 wants to merge 14 commits into
farion1231:mainfrom
xrbs00:feat/hermes-session-usage

Conversation

@xrbs00

@xrbs00 xrbs00 commented Aug 4, 2026 •

Copy link
Copy Markdown

2026-10-03 更新:采集兼容性与耗时语义(HEAD 2bc6b199)

  • 最低版本仅针对可选请求采集插件:Hermes ≥0.21.5(正式发布标签 v2026.9.24),并且实际具备 post_api_request、api_request_error、post_auxiliary_call。已补全 provides_hooks 与 requires_hermes;注册前检查实际 VALID_HOOKS,缺少钩子时明确拒绝采集,不创建账本、不静默漏掉辅助调用。
  • 原有只读聚合用量同步不受上述最低版本限制,仍按源表/列能力探测并隔离不兼容配置档。
  • 主循环钩子的开始时间在重试之间复用:界面明确标注为“主调用累计耗时,含重试与等待”;辅助调用标注为“单次尝试耗时”。保留源时间与已有账本值,不相减估算单次重试延迟,不宣称提供商侧精确耗时。
  • 保留辅助模型调用钩子,不以人工审批展示/选择事件替代模型 API 调用。聚合六列来源标识、用量/费用计算、历史估算以及数据库结构均未改变;未引入原生每日统计、错误恢复或延迟分布面板。
  • 本次针对性验证:Python 8/8,采集面板与 Dashboard 前端 15/15,Rust 聚合/采集 20/20,类型检查、改动文件格式检查与 git diff --check 通过。兼容版本隔离 fixture 的真实 Doctor/严格 admission 检查通过(仅控制宿主版本为 0.21.5,非已启用的真实安装验证);低于最低版本的环境被拒绝。不把此前全套验证当作此提交的全套结果。
  • 上一 HEAD 25b8751d 的 7 项 GitHub 检查已通过,替代此前“action_required”的旧状态;本次新 HEAD 的 CI 工作流 为 action_required,测试 jobs 为 0,需要维护者批准运行;目前只有标签检查通过,不能称为新 HEAD CI 已通过。代码所有者审查仍需完成。没有启用或重启任何 Hermes 运行时。

2026-09-30 阶段记录:可选请求采集与历史估算(非当前 HEAD)

  • 在保留原有只读聚合导入及同步窗口趋势的同时,新增按 Hermes Profile 手动启用的 CC Switch Hook 插件。重启 Hermes 后,插件仅从该时刻起记录受支持请求的时间、路径、模型、状态、耗时和已上报 Token;不记录提示词、响应正文、密钥或 URL。
  • 插件写入独立的每 Profile SQLite 账本,不修改 Hermes state.db。CC Switch 以只读、持久游标、分批方式导入账本,并提供可翻页请求列表。当前 Hook 不覆盖所有调用路径,详情可能缺失;请求明细不与现有聚合总量重复相加。
  • 采集前的历史仅支持用户手动导入一份标注为估算的累计基线(含可识别的无模型归属主调用)。不拆分到过去各天,不伪造请求记录,不加入总览或每日趋势;同一 Profile 不重复叠加可能重叠的历史基线。
  • 含 YAML 锚点或注释的 plugins 配置会被一键启用安全拒绝,需手动启用插件;普通配置会先备份并验证写入后的 YAML。不会自动重启已运行的 Hermes。

当时 HEAD 25b8751 已解决与上游主线的合并冲突。本地验证:Python 插件 5/5、Rust 采集 4/4、v20→v21 迁移及配置锚点回归各 1/1、前端相关 13/13、TypeScript 类型检查、Rust fmt 和 Clippy 通过。前端全套单进程 151/151 个文件、1706/1706 项通过;默认并行模式曾有两项 App 集成用例失败,故不声称默认并行模式稳定全绿。Rust 全套排除当前 Windows 账户无权限运行的两个符号链接用例后,2954 项通过、10 项忽略、0 项失败;未排除时不声称全套通过。当时 GitHub CI 为 action_required、尚未启动测试 job;此后该 HEAD 的 7 项检查已通过。上述全套结果属于旧 HEAD,最新状态见顶部更新;代码所有者审查仍需完成。


以下保留原有聚合导入的设计说明;其旧版验证记录不代表当前 HEAD。

概要

本 PR 将 Hermes Agent 的 session_model_usage 累计统计作为独立数据源接入 Usage Dashboard。

  • 以只读方式同步累计 API 调用数、令牌用量和 Hermes 报告的成本。
  • 首次发现数据库或统计序列时只建立基线,不回填既有累计量。
  • 后续变化记录为相邻两次同步之间的增量,因此趋势图表示同步窗口,而不是精确请求时间。
  • 累计统计不会被转换为 proxy_request_logs,也不会用于推断状态码、成功率、延迟或逐请求费用。
  • 不要求 Hermes 流量经过 CC Switch 代理,不修改 Hermes 数据库。

因此,升级后不会立即出现既有 Hermes 历史用量;只有建立基线后的新增用量和有界成本校正会进入统计。

背景

此前的 Hermes 集成以会话为粒度。同一进程内的多次 API 调用可能聚合到一条会话记录,模型字段也可能停留在初始模型,无法可靠表示实际的模型和提供商归因。维护者在 #5201 中说明了这一限制,并暂时移除了旧实现。

较新的 Hermes 版本提供 session_model_usage,按会话、模型、提供商路径、计费模式和任务累计记录调用数、令牌与成本。该表改善了模型和提供商归因,但其语义仍是累计统计,不是请求日志。

本 PR 因此采用“累计快照与同步窗口增量”的独立导入模型,而不是将数据写入现有的 proxy_request_logs。

Addresses #5088

Addresses the Hermes-specific portion of #4784

Context: #5201

实现内容

  • 自动发现 Hermes 默认 state.db 和 profiles/*/state.db。
  • 以 SQLite 只读和 query_only 模式读取,兼容 WAL。
  • 不对 Hermes 数据库执行表结构迁移、检查点或任何写入。
  • 在 CC Switch 自有数据库中保存数据库身份、同步快照、记账分段和已导入成本余额。
  • API 调用数和令牌仅导入可证明的非负增量。
  • 数据库被替换、累计计数回退或统计序列重置时建立新的记账分段,不生成负调用数、负令牌或跨数据库实例增量。
  • 保留 Hermes 可证明的配置档、提供商、模型、任务、计费路径和同步窗口维度。
  • 增加 Hermes 来源筛选以及配置档、任务筛选和相应的聚合查询。
  • 仅 Hermes 视图隐藏无法由源数据证明的请求详情、成功率和平均延迟。
  • 混合视图同时保留旧来源的缓存创建令牌和 Hermes 报告的缓存写入令牌;Hermes 未报告该字段时保留已知值并标记为部分数据。
  • 趋势图只在源数据实际提供对应非零维度时显示 Hermes 缓存写入和推理令牌序列。
  • Hermes 与逐请求来源混合展示时,显示数据精度差异提示。
  • 更新英文、简体中文、繁体中文和日文界面文案。
  • 通过独立导入器接入 Hermes;现有 Claude Code、Codex、Gemini、Grok Build 和 OpenCode 的采集及计费路径未修改。

调用数和时间语义

Hermes 的 api_call_count 是源表报告的累计 API 调用计数,不等同于 CC Switch 代理实际观察到的物理 HTTP 请求数。

对于每个首次发现的统计序列:

首次同步:
  保存当前累计值作为基线
  emitted_delta = 0

后续同步:
  emitted_delta = current_counters - previous_counters
  仅导入可证明的非负增量

如果计数回退、数据库身份变化或统计序列重置,则建立新的基线,不拼接新旧序列。

所有增量归属于相邻两次同步之间的窗口。因此,日、周和趋势视图反映的是同步窗口,不能用于恢复精确请求时间、逐请求顺序或重试与回退链。

成本语义

成本直接使用 Hermes 源表报告值:

  • 当 cost_status 为 actual、final、settled 或 complete,且存在 actual_cost_usd 时,采用实际成本。
  • 其他情况下采用 estimated_cost_usd。
  • 实际成本和估算成本不相加,避免状态切换造成重复计费。
  • 不使用 CC Switch 模型价格表重新计算 Hermes 令牌成本。
  • 如果 Hermes 将某条订阅路径报告为 included 或 $0,则保留该源值和来源语义,不转换为 API 市场等价成本。

每个统计序列持久化首次成本基线和此前已导入的成本余额:

target_balance =
  max(current_selected_cost - persisted_cost_baseline, 0)

emitted_delta =
  target_balance - previous_imported_balance

当较高的估算成本被较低的实际成本替换时,可以产生只包含成本的有符号校正:

  • API 调用数和令牌均为零。
  • 成本增量可以为负。
  • 最多回滚该统计序列在首次基线后已经导入的成本。
  • 不会扣减首次同步前未回填的历史金额。
  • 已导入净成本不会低于零。

即使源成本先跌破基线,之后再回升到基线,也不会重新导入历史金额;只有超过基线的部分才会形成新增成本。

因此,Dashboard 中的 Hermes Cost 表示首次建立基线后,Hermes 报告成本的净变化。它不是 Hermes 当前绝对累计成本、逐请求账单、提供商最终实付金额,也不是 CC Switch 重新估算的市场价格。

仅筛选包含成本校正的同步窗口时,窗口成本可能为负。界面显示 $0.0000 也不代表服务免费;它可能表示该同步窗口没有新增成本、Hermes 未报告成本,或源数据将费用标记为订阅已包含。

兼容性和错误隔离

原有只读聚合导入已针对 Hermes Agent v0.19.1 的 session_model_usage 表结构完成验证,不通过 Hermes 版本号硬编码最低兼容版本;新增可选请求采集插件则要求 Hermes ≥0.21.5(v2026.9.24)及完整的三个钩子,见顶部更新。

同步时直接探测源表和所需列:

  • 表不存在、必需列缺失或查询不兼容时,该配置档返回明确同步错误,不导入数据。
  • 单个配置档失败不阻塞其他兼容配置档。
  • 所有错误路径仍保持 Hermes 数据库只读。

CC Switch 自有表结构覆盖以下情况:

  • 全新数据库。
  • 既有 v16/v17 数据库按顺序迁移到 v19。
  • 上游 v18(已有会话日志字节游标列)升级时补齐 Hermes 聚合表。
  • 本 PR 早期 v18(已有 Hermes 聚合表)升级时补齐上游字节游标列。
  • 已存在候选表但缺少成本校正字段时的幂等修复。

旧快照缺少 emitted_cost_balance_usd,且历史增量没有记账分段时,系统不会尝试跨计数重置重建历史余额,而是建立安全的新基线:

previous_imported_balance = 0
persisted_cost_baseline = max(current_selected_cost, 0)

如果已有可证明的已导入余额,仅缺少成本基线,则按以下规则恢复连续状态:

persisted_cost_baseline =
  max(current_selected_cost - previous_imported_balance, 0)

数据边界

原有累计聚合导入不推断逐请求事实。新增可选 Hermes Hook 插件从启用并重启后开始记录受支持路径的请求时间、状态与延迟等元数据;未覆盖路径、Hook 写入失败和未上报的用量保持未知,不声称完整物理 HTTP 请求日志或逐请求费用。

本 PR 也不读取提示词、消息正文、请求头、Cookie、密钥或原始错误,不持久化 billing_base_url 原值,只保存稳定归因所需的派生标识。

旧版验证记录(非当前 HEAD)

本次与最新主线 3217f725 合并后,以下验证通过:

  • TypeScript tsc --noEmit。
  • 改动文件 Prettier 检查。
  • Hermes Dashboard/趋势图聚焦前端测试:16/16。
  • cargo fmt --check。
  • cargo clippy -- -D warnings。
  • 数据库 schema 测试:11/11,其中包含两种 v18 历史到 v19 的双向收敛回归测试。
  • Hermes 导入器定向测试:13/13。
  • 用量聚合定向测试:43/43。
  • 上游会话用量核心定向测试:15/15。
  • Rust 全套在仅排除 3 个当前 Windows 账户无 symlink 权限的测试后:2867 通过、0 失败、6 忽略、3 排除。
  • git diff --check,且无未解决合并路径。

完整前端 Vitest 在候选分支仍有 1 个主线既有的全套时序超时:

  • 候选分支:135 个测试文件中 134 个通过;1031 项测试中 1030 项通过、1 项超时。
  • 最新主线 3217f725 独立基线:133 个测试文件中 132 个通过;1012 项测试中 1010 项通过,包含同一超时及其后续 DOM 清理级联。
  • tests/integration/App.test.tsx 独立新进程复跑:7/7 通过。

因此不把完整前端套件标记为全绿;现有证据表明剩余超时可在最新主线复现,并非本 PR 新增的稳定回归。

完整 Rust 未在当前账户下原样全绿:3 个测试需要 Windows 创建 symlink 特权(错误码 1314)。排除这 3 个环境受限用例后,其余 Rust 测试和 Clippy 均通过。

建议审查入口

  1. src-tauri/src/services/session_usage_hermes.rs
    数据库发现、只读同步、快照与增量、计数重置、数据库身份和有界成本校正。
  2. src-tauri/src/database/schema.rs
    CC Switch 自有快照、增量及迁移与修复表结构。
  3. src-tauri/src/services/usage_stats.rs
    与现有聚合、筛选和查询接口的合并。
  4. src/components/usage/UsageDashboard.tsx 及相关概览、表格和趋势组件
    数据精度提示、混合来源汇总和不支持指标的隐藏逻辑。

@xrbs00
xrbs00 requested a review from farion1231 as a code owner August 4, 2026 10:00
@github-actions github-actions Bot added frontend Frontend (React/TypeScript) backend Backend (Rust/Tauri) labels Aug 4, 2026
@xrbs00
xrbs00 marked this pull request as draft August 4, 2026 10:22
@xrbs00 xrbs00 changed the title feat(usage): 增加 Hermes 累计用量同步 feat(usage): sync aggregate Hermes usage Aug 4, 2026
@xrbs00 xrbs00 closed this Aug 4, 2026
@xrbs00 xrbs00 reopened this Aug 4, 2026
@xrbs00
xrbs00 marked this pull request as ready for review August 4, 2026 11:48
@farion1231

Copy link
Copy Markdown
Owner

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Already looking forward to the next diff.

Reviewed commit: 4c4d65c15c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

xrbs00 added 2 commits August 5, 2026 10:52
…replacement

NTFS can reuse the same creation-time tick when a file is deleted and
immediately recreated at the same path, so relying on creation_time
alone could misidentify a replaced Hermes database as the same file and
skip starting a new baseline. Use volume serial + file index as the
primary identity signal with creation_time as a second factor, and
space out the replacement test to avoid the metadata reuse window.
@github-actions github-actions Bot added the i18n Internationalization (zh/en/ja) label Aug 16, 2026
@Finn763

Finn763 commented Aug 26, 2026

Copy link
Copy Markdown

Reviewed the full diff (25 files, +4811/−123) against the issue's proposed solution — this is a solid, first-class implementation. I went through the Rust sync path (session_usage_hermes.rs), the DB migration (v17→v18 with dedup/repair), and the frontend wiring. Highlights:

What's done well

  • discover_hermes_databases() covers both the default state.db and profiles/*/state.db — matches how Hermes actually layouts multi-profile homes, and the per-source error isolation ("keep valid Profiles usable when one source is corrupt") is the right failure semantics.
  • Aggregate-delta sync with sync_window_start/end + source-incarnation dedup is a clean design: idempotent across re-scans, and it deliberately scopes out per-request status/latency (documented in the data-source breakdown) instead of pretending to have data it doesn't.
  • Tests use a temp Hermes root via sync_hermes_usage_at_root — properly testable without touching the user's live home. Nice.
  • Wiring: it rides the existing sync_all_unlocked scheduler, same as OpenCode/Pi/Grok — consistent with the app's current architecture.

Two non-blocking observations before merge

  1. mergeable is UNKNOWN and the branch is ~10 days behind main — the PR has sat untouched since Aug 16 after the Codex review passed and all CI is green. A rebase onto current main would confirm no drift (usage tables/indexes have been touched in v18 migrations since), and would likely also flip the mergeability check.

  2. Sync cadence for Hermes: since Hermes only writes usage counters when sessions actually run, the aggregate-delta model means a long-lived desktop session's numbers appear only after the next sync_all_unlocked pass. If the app's sync trigger is dashboard-open/timer, that's fine; worth one sentence in the PR description (or a follow-up) documenting when Hermes deltas are expected to appear, so users don't file "usage not updating" issues against this feature.

Net: implementation quality is merge-ready from my read — I'd love to see this land and iterate on cadence/UX in follow-ups rather than block on polish. @farion1231 if the size is the concern, the diff is cleanly separated (Rust service + migration + frontend) and could even be merged in two steps if that helps.

(For context: I use both cc-switch and Hermes Agent daily — this is exactly the missing integration; happy to help with follow-ups, e.g. sync-cadence docs or a usage-sync trigger test.)

# Conflicts:
#	src/components/usage/UsageTrendChart.tsx
@Finn763

Finn763 commented Aug 26, 2026

Copy link
Copy Markdown

Thanks @xrbs00 - saw the origin/main merge onto pr-6120. Verified locally against the new head (fbd9c09ca0): no conflicts, mergeability flipped UNKNOWN -> true, so no drift from the v18 usage-table changes.

Checks had not started reporting yet when I looked (~10 min after your push), so just worth a glance once they land.

Observation 2 (one-line note in the description about when Hermes aggregate deltas are expected to appear between sync_all_unlocked passes) remains optional - not blocking anything.

xrbs00 added 6 commits August 28, 2026 08:45
Resolve the v18 schema lineage split by converging both byte-cursor and Hermes aggregate structures in v19. Preserve both dashboard features and isolate Hermes provider tests from the live HERMES_HOME.
@github-actions github-actions Bot added the documentation Improvements or additions to documentation label Sep 3, 2026
@teawhite-hk

Copy link
Copy Markdown

@farion1231 快合并这个功能

@Chunai-Bboy

Copy link
Copy Markdown
Contributor

Sharing two findings from implementing the same goal user-side, in case they're useful for this PR.

I built a user-side sync for Hermes → CC Switch usage dashboard and ran it against a real Hermes install. Two things came out of it that seem relevant to the discussion here.

1. The "per-call billing is not possible" concern may be outdated — but not in session_model_usage

The maintainer previously removed Hermes usage tracking, citing that all API calls in a process collapse into a single sessions row with the model field locked to the initial model.

Reading session_model_usage instead of sessions does resolve the model-attribution half of that problem: it is keyed per (session_id, model, task), so attribution is per model. From one real session I get three independently-attributed rows:

upstage/solar-pro4:free   task=(main)               13144 in / 107 out
upstage/solar-pro4:free   task=title_generation        57 in /  24 out
step-3.7-flash            task=(main)               42343 in / 2483 out
nemotron-3.5-lightning-free task=(main)             29519 in /  703 out

But session_model_usage is still cumulative, so it cannot give per-request rows either. I did not find a way to get true request-level detail from it.

There is, however, a table I have not seen discussed: messages, which has timestamp, token_count and finish_reason per row — i.e. request-level. In the install I tested, token_count was NULL for the rows I could inspect (I only had 2 messages to look at, so this is weak evidence, not a conclusion). If the current Hermes version does populate it, that would be the natural source for true per-request usage and would resolve the original objection outright. It may be worth checking whether that field is populated in practice before settling on an aggregate-only design.

2. The reason a proxy_request_logs-based importer cannot report a real request count

This one is structural, and I think it is the strongest argument for the dedicated-table design in this PR.

proxy_request_logs has 27 columns and none of them can hold an API call count. A dashboard built on it necessarily reports COUNT(*) as "total requests". So an importer that writes one row per sync window per (session, model, task) cannot make that number mean "API calls" — it will mean "sync windows in which something changed". In my measurements the same dataset that contained 3 distinct models produced 7 "requests", while the actual SUM(api_call_count) in the source was smaller.

Producing a truthful call count therefore seems to require either writing N rows per window (i.e. synthesising N requests that never existed, with the token split between them) or storing a counter in a table that has a counter column — which is what this PR does. I went with the second option being unavailable to me and documented the resulting limitation rather than fabricating rows.

Offer

If useful, I can test this PR against a real Hermes install and report the results — happy to test on Windows, and I can also check Linux if that is useful. I have no stake in the implementation choice; I mention this only because the messages.token_count question is the part I cannot answer from one small sample.

@xrbs00

xrbs00 commented Sep 30, 2026

Copy link
Copy Markdown
Author

Update on this PR (head 25b8751):

  • Merged current upstream main; GitHub now reports no merge conflict.
  • Kept the existing read-only, cumulative Hermes importer for dashboard totals and sync-window trends. For request details, added an opt-in Hermes hook plugin: after enabling it for a profile and restarting Hermes, it records supported calls in a separate metadata-only ledger (time, route/model, status, latency, and usage when Hermes supplies it). It does not store prompts, responses, keys, or URLs, and it does not claim coverage of every request path. The exact rows are not added to aggregate totals.
  • Before activation, users may manually import only a clearly labeled estimated historical total. We do not invent past request rows or daily timestamps. This also addresses the messages.token_count uncertainty without treating message rows as proven API-call records.
  • The PR description now documents these boundaries and the new tests. Local checks: frontend 1706/1706 with one worker; Rust 2954 passed with two Windows symlink-privilege tests excluded; Python plugin 5/5; typecheck, formatting, Clippy, and renderer build passed. The default parallel frontend run previously hit two App integration timing failures, so I am not calling that mode green.

The current upstream CI run is action_required with no test jobs, so it has NOT validated this head yet. If a maintainer is comfortable approving the fork workflow, the remote checks can run; code-owner review remains outstanding. No Hermes runtime was enabled or restarted as part of this PR update.

@Chunai-Bboy

Copy link
Copy Markdown
Contributor

@xrbs00 我把你这个插件在真机上跑了一天(Hermes v0.21.2 / Windows / CC Switch 3.20.4),
有一组数据想跟你对一下。其中一条是夸你的,另外几条是可以直接改的。

先说好消息:你的聚合逻辑是干净的

拿一个真实长会话(会话 id 20261003_162817_8e6dd5,累计 310 次调用),我把你的账本
和 Hermes 自己的聚合计数器逐项对账:

口径 你的插件逐条求和 state.db 聚合计数器
input(非缓存) 420,581 420,581
output 109,412 109,412
cache_read 26,862,951 26,862,951

三项全等(插件 169 条事件 = 主循环 149 + background_review 19 + 1 条零散)。
我一开始怀疑你把 cache 重复计进去了——实测不是,input_tokens 用「非缓存输入」口径是对的。
这套聚合可以放心向维护者主张正确性。

另外纠正我自己的一个判断错误:background_review 你其实抓到了。
我原以为你只覆盖主循环,实际上子 agent 复用同一条 conversation_loop 代码路径,
post_api_request 会触发。19 次后台审查调用全部进了你的账本。

三条可以直接改的

1)post_auxiliary_call 这个钩子不存在——这是漏掉辅助任务的唯一根因

__init__.py 里这行:

for name in ("post_api_request", "api_request_error", "post_auxiliary_call"):
    ctx.register_hook(name, callback)

前两个在 VALID_HOOKS 里(37 个之一),第三个不在。全仓库 grep:

$ git grep -c post_auxiliary_call HEAD
(0 个文件)

所以 ctx.register_hook("post_auxiliary_call", ...) 是静默失败的。这同时解释了三件事:
hermes plugins validate 报错、plugins doctor 报 unknown hook、辅助任务一条都收不到。

同一会话按 task 分组:

task 调用数 你的账本
''(主循环) 149 ✅
background_review 19 ✅
approval 141 ❌
title_generation 1 ❌

漏了 141/310 = 45.5%。 approval 占大头,而且这是长任务里最高频的操作——
我这个会话 141 次调用全是点批准。

好消息是 approval 不需要上游改,VALID_HOOKS 里已有专用钩子
(docs/observability/README.md:197-198 有文档):

  • pre_approval_request — 审批请求展示/发出前
  • post_approval_response — 用户响应或超时后

把 post_auxiliary_call 换成这两个,覆盖率就从 54.5% 提到约 100%。
(title_generation 确实没有对应钩子,那是上游的缺口,我另外报。)

2)plugin.yaml 缺 provides_hooks,validate 过不了

现在 manifest 只有三行(name / version / description)。
校验器读 provides_hooks(hermes_cli/plugin_dev.py:300-313),声明了但不在
VALID_HOOKS 里会直接报 unknown hook in provides_hooks。

name: ccswitch-usage
version: "1.0"
description: ...
provides_hooks:
  - post_api_request
  - api_request_error
  - pre_approval_request
  - post_approval_response

3)重试的 duration_ms 是轮级累计,不是单次

一个 404 重试 3 次的调用,三条事件 started_at_ms 相同,duration 分别是
1063 / 4186 / 9488 ms。反推真实单次耗时约 1063 / 3123 / 5302 ms——第 2、3 次记的是累计值。
如果导入时拿 duration 求平均,单次失败会被高估。建议加一个 attempt 序号,或改成记录增量。

一个技术提问

我核对 session_model_usage 的主键时发现它是 6 列:

PRIMARY KEY (session_id, model, billing_provider, billing_base_url, billing_mode, task)

上面那个会话里出现了两条 task='approval'(rowid 一条 140 次、一条 1 次),
靠 billing_base_url 区分(一条为空,一条是具体 base_url)。
如果 Rust 侧分组键只用 (session, model, task),这两行会被并成一条——
我自己踩过这个坑,三轮同步后总量多算了 2 次调用 / 1994 input。

想确认你导入器那边分组用的是几列?如果也是 4 列,可能得跟着补。


自查

  • 每个数字都能追到原始会话 id 和 SQL 查询
  • 我自己判断错的地方(background_review)主动写进去了
  • 没指责口气,每条都给了具体改法
  • 区分了「插件没用专用钩子」(approval)和「上游真缺钩子」(title_generation)
  • 不含我方仓库地址
  • 覆盖率用最新一次读数(54.5%),不是早期快照

@xrbs00

xrbs00 commented Oct 3, 2026 •

Copy link
Copy Markdown
Author

@Chunai-Bboy 感谢实测反馈,已逐条核查,并在 2bc6b199 做了最小修复。

  1. 版本与插件声明:post_auxiliary_call 由 0e580956 加入,正式发布 v2026.9.24(0.21.5)已包含。插件现声明 Hermes ≥0.21.5,补全三个钩子的 provides_hooks,并在注册前检查钩子是否齐全;不兼容时明确拒绝采集。最低版本要求只针对可选采集插件,原有聚合同步不受影响。也请附上你那份 0.21.2 安装的源码 commit SHA,以确认版本差异。
  2. 审批调用:没有改用审批 UI 钩子,因为它们记录审批展示与选择,人工操作也会触发,并不等于模型请求。智能审批的 call_llm(task="approval") 和标题生成等辅助模型调用由 post_auxiliary_call 采集。
  3. 重试耗时:已复现相同开始时间下的累计耗时。现在明确标注主调用为“累计耗时,含重试与等待”,辅助调用显示单次尝试耗时;没有直接相减,以免把退避等待算成单次请求耗时。
  4. 分组键:导入器已保留完整六列:session_id, model, billing_provider, billing_base_url_digest, billing_mode, task。base URL 使用稳定摘要区分,未缩减分组键或更改计数算法。

本地针对性验证:Python 8/8、前端 15/15、Rust 聚合/采集 20/20,类型与格式检查通过。

请在 Hermes 0.21.5(v2026.9.24)或更新、具备全部三个钩子的安装上重新安装更新后的插件并重启 Hermes,复测 approval/title_generation 与重试耗时显示。

@Chunai-Bboy

Copy link
Copy Markdown
Contributor

@xrbs00 逐条复核完了。先说结论:你的回复全对,我评论里两条核心论断是错的,给你道歉。谢谢你点名要 SHA——正是它帮我把错误定位到了根上。

错误 1:post_auxiliary_call 存在,是我拿旧 checkout 查的

我 git grep 的对象是本机安装的 Hermes 源码,事后确认那是 0.21.2 build(commit 1021a0325696e9070e6659f95fcd84c3e7e114df,2026-09-11),落后上游 14,771 个提交。git merge-base 验证:钩子提交 0e580956 不在它的祖先里,所以我 grep 到 0 处就断言"这钩子不存在"。

上游早就有实现(agent/auxiliary_hooks.py)、有测试(tests/agent/test_auxiliary_hooks.py)、有文档(observer-hooks.md 明确写了 titling / compression / MoA / vision 全走 pre/post_auxiliary_call)。拿自己机器的陈旧快照当上游事实,是方法错误,不是运气问题。

顺带撤回我那句「换成 pre_approval_request/post_approval_response 覆盖率就能到 100%」——你判断得对,那两个钩子跟踪的是审批 UI 的展示与选择,人工点批准也会触发。真按我说的改,你的账本反而会被污染。

我评论里 45.5% / 100% 那些覆盖率数字全部作废:它们只在 0.21.2 上成立,在 0.21.5 上不存在这个缺口。

错误 2:六列分组键,你本来就是对的

提"如果也是 4 列要跟着补"之前,我应该先去读你的 session_usage_hermes.rs,而不是抛一个自己没验证过的假设。你的键本来就是六列(billing_base_url 还做了摘要)。这条是我多虑了,抱歉。

我在 v0.21.5 上复测了你的修复

升级后(v0.21.5,build af90026aa09949579bd423d24def3d38f743cde0)装插件 1.0.1:

项 结果
hermes plugins validate 通过(旧版是 fail)
plugins doctor 3 hook(s) 注册(旧版报 unknown hook)
插件自带测试 8/8
版本门 REQUIRED_HOOKS 缺任何一项就明确拒绝采集并提示升级,不静默丢
post_auxiliary_call 实抓 真实长写作会话 aux 从 0 → 17 条(approval 15 + compression 2)

同一会话、升级后同一窗口的对账

维度 插件逐条 state.db 聚合 结果
approval 15 条 / 10,490 in 15 / 10,490 逐 token 一致
main + background_review 251 / 429,487 223+28 / 332,237+97,250 恒等(bg 走 post_api_request 记成 main,聚合拆行)
compression 2 / 131,602 1 / 67,721 聚合漏了第一次

最后一条是给你的弹药,不是抱怨:第一次压缩(15:32:17,109s,in 63,881 / out 12,096)真实成功了——其后 main 的 prompt 从 3–10K 骤降到 1–2K,上下文确实被压了——但 session_model_usage 里 compression 行的 first_seen 恰好等于第二次压缩结束的时刻(15:59:18),第一次压缩的用量没进任何聚合行。

也就是说在 0.21.5 上,你的账本反而比 Hermes 官方聚合更完整。这值得写进 PR 描述当卖点,也值得给 Hermes 报一个 aux 丢账的 issue——需要的话我把时间线和数值整理成最小复现贴上来。

最后

谢谢你连我没说对的两条也认真核查,能改的当天就推了 2bc6b19,该坚持的(审批 UI 钩子那条,你说得完全对)也顶住了。这份 PR 的工程质量配得上早点 merge。CI 如果跑过这个 head,我可以把 validate / doctor / 8 个测试的输出连同上面这张对账表整理贴给维护者看。

我这边的同步(聚合走 session_model_usage、你的账本当可选增强)会继续跟你的结论对齐:明细与聚合分表、永不相加。

@funkpopo

Copy link
Copy Markdown
Collaborator

@xrbs00 这个 PR 的文档与诚实度在同类 PR 里非常突出(分阶段记录、明确标注哪些是估算、CI 状态如实说明"action_required 不能称为通过")。

但 +7421/−133、45 个文件是超大包,且混合了至少三个主题(用量同步、Hook 采集插件新子系统、Profile 会话导入)——按 grab-bag 处理,将以此为依据建议关闭本 PR,欢迎拆分后单独重提,然后我们再逐一查看哪些可以被合并。

PR 混合的主题(建议拆分)

  1. 只读聚合同步(Hermes 用量聚合导入 + 持久游标 + 分批 + 源表/列能力探测);
  2. Hook 采集插件(按 Profile 手动启用的 CC Switch Hook,独立 SQLite 账本,重启后从该时刻起记录)——这是一个新的采集面,需要先表态;
  3. UI(可翻页请求列表、采集面板)与历史估算基线。

需要维护者确认的方向问题

1. [P1] Hook 采集插件是新子系统。

在 Hermes 侧安装 CC Switch Hook 插件、维护每 Profile 独立账本、注册时检查 VALID_HOOKS——这是新的数据采集路径。虽然作者声明"不记录提示词、响应正文、密钥或 URL"(好),但这个采集面是否纳入产品方向、与 main 现有 Hermes 会话用量导入的关系(补充还是替代),最好开 issue 进行讨论。

值得肯定的设计

  • "主调用累计耗时,含重试与等待" vs "单次尝试耗时"的语义区分明确,不伪造提供商侧精确耗时;
  • 历史基线标注为估算、不拆分到过去各天、不伪造请求记录——诚实标注的范例;
  • 含 YAML 锚点/注释的 plugins 配置一键启用安全拒绝,普通配置先备份验证;
  • 最低版本检查实际钩子,缺少时明确拒绝不静默漏。

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

Labels

backend Backend (Rust/Tauri) documentation Improvements or additions to documentation frontend Frontend (React/TypeScript) i18n Internationalization (zh/en/ja)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants