feat: show per-step OpenRouter cost on the Trajectory tab

Extend the openRouterCost projection with a per-step priced-cost map
keyed by turn:step so surfaces can render spend without re-pricing, and
have the Trajectory view read it through the framework useProjection seat:
each priced assistant step shows its USD cost on the request boundary chip
and in the inspector summary, while unpriced steps stay blank. Also fix a
pre-existing exactOptionalPropertyTypes error in extractCacheRates that the
host typecheck surfaced.
This commit is contained in:
2026-08-20 22:14:09 +07:00
parent d2ad46b8aa
commit 99b63abde6
20 changed files with 234 additions and 29 deletions

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-08-20-openrouter-cost-balance-readouts.md
2026-08-20-openrouter-cost-balance-readouts.md: 339acc4140eb6c34d1c3910a93babbeda9a28cac
2026-08-20-openrouter-cost-balance-readouts.zh.md: 16606ae58b346a1eeb10f703eae748665f64ea83
2026-08-20-openrouter-cost-balance-readouts.md: 471ca8f3d6552a0fdba680580908b3d7b268b115
2026-08-20-openrouter-cost-balance-readouts.zh.md: 5a3a09453bb26ebcc82887bda4209c281f8476bd

View File

@@ -42,7 +42,14 @@ into `conversation.composer.dock` (the shipped stats line's dock), reading the
balance badge registers into `sidebar.footer.action`, polling the balance
Remote on a 60s client-side interval. Both hide themselves when their data is
absent: cost hides before any priced step, balance renders an empty dashes
marker when the snapshot has no value yet.
marker when the snapshot has no value yet. The projection's `steps` view field
also maps every priced step to its USD cost under `${turn}:${step}` keys, and
`dsh-client-ui-trajectory` reads it from the resident projection store to show
each priced assistant step's cost on the Trajectory request boundary and in
its inspector summary. The projection store follows the session instance's
residency ([retention note](../bug-fix/2026-08-20-web-session-projection-store-survives-revival.md)),
so the readouts survive switching between chats instead of reverting to
blank.
**The API key is a credential reference, seeded identically to routing.**
`apiKeyEnv` (default `OPENROUTER_API_KEY`) resolves through the credentials

View File

@@ -14,7 +14,7 @@ Status: implemented
**费用是面向客户端的只读模型投影,绝不是被记录或模型可见的事实。** 网关拉取 OpenRouter 的模型定价表(`GET /models``pricing.prompt`/`completion` USD 每 token、flat 的 `request` 费用、披露的缓存费率),并在 `openRouterCost``SessionProjectionMap` 条目中按各会话已记录的用量折叠它。余额来自 OpenRouter 权威余额端点 `GET /credits`(可用余额 = `total_credits` 减去已花费的 `total_usage`,即仪表盘展示的数字),并与 `GET /auth/key` 合并以获取 key 标签与月度 token 预算(`usage`/`limit`);标准 `sk-or-v1` key 在 `/auth/key` 上**不**返回 `credits` 字段,因此仅靠该端点总会读到空余额。两个请求都使用 `redirect: 'error'``global.fetch` —— 携带凭证的出站调用的主机标准。
**Web 表面是两个 slot 条目。**每会话费用读数注册进 `conversation.composer.dock`(随附统计行的 dock通过 `useProjection` 框架座位读取 `openRouterCost` 投影;实时余额徽章注册进 `sidebar.footer.action`,以客户端 60s 间隔轮询余额 Remote。两者在数据缺失时自我隐藏费用在任何已计价步骤前隐藏余额在快照尚无值时渲染空的破折号标记。
**Web 表面是两个 slot 条目。**每会话费用读数注册进 `conversation.composer.dock`(随附统计行的 dock通过 `useProjection` 框架座位读取 `openRouterCost` 投影;实时余额徽章注册进 `sidebar.footer.action`,以客户端 60s 间隔轮询余额 Remote。两者在数据缺失时自我隐藏费用在任何已计价步骤前隐藏余额在快照尚无值时渲染空的破折号标记。投影的 `steps` 视图字段还以 `${turn}:${step}` 为键映射每个已计价 step 的美元成本,`dsh-client-ui-trajectory` 从常驻投影存储读取它,在 Trajectory 请求边界与其检查器摘要中显示每个已计价助手 step 的费用。投影存储跟随会话实例的常驻性([保留笔记](../bug-fix/2026-08-20-web-session-projection-store-survives-revival.md)),因此读数在切换聊天时持续存在,不会回退为空白。
**API key 是凭证引用,与路由的初始化方式一致。**`apiKeyEnv`(默认 `OPENROUTER_API_KEY`)与 `dsh-web-search-deepseek` 完全一样,通过凭证接缝加环境回退解析,按刷新在 thunk 中捕获,使凭证或设置变更无需重建即可在下一周期生效。