feat: add searxng web-search provider, openrouter cost balance UI, offline scripts; update source-launch and docs
Some checks failed
CI / node 22.19 (push) Has been skipped
CI / node 26 (push) Has been skipped
CI / python 3.10 / keyless SDK (push) Has been skipped
CI / python runtime / release-shaped Linux x64 (push) Has been skipped
CI / windows node 24 / wine blocking (push) Has been skipped
CI / wine apt cache (push) Successful in 58s
CI / serial / linux (push) Has been skipped
Deploy documentation / build (push) Failing after 2m46s
Deploy documentation / deploy (push) Has been skipped
E2E (real DeepSeek API) / e2e (push) Failing after 1m18s
Sandbox / sandbox e2e (bwrap, ubuntu-latest) (push) Failing after 1m18s
Landlock Run / Matrix (push) Successful in 13s
Release (vendor) / Pack npm tarballs (push) Failing after 3m43s
Release (dsh) / Pack npm tarballs (push) Failing after 1m53s
Sandbox / sandbox e2e (landlock, ubuntu-24.04) (push) Failing after 1m51s
Release (vendor) / Publish to npm (push) Has been skipped
Release (dsh) / Publish to npm (push) Has been skipped
CI / node 24 / static (push) Has been cancelled
CI / node 24 / coverage (push) Has been cancelled
CI / node 24 / snapshots and artifacts (push) Has been cancelled
CI / windows node 24 / native complete (push) Has been cancelled
CI / serial / linux (self-hosted standby) (push) Has been cancelled
CI / serial / macos (push) Has been cancelled
CI / serial / windows (self-hosted standby) (push) Has been cancelled
CI / larger-runner-benchmark (16, linux, dsh-ubuntu-24-04-16core, typecheck) (push) Has been cancelled
CI / larger-runner-benchmark (16, windows, dsh-windows-2025-16core, production-site) (push) Has been cancelled
CI / larger-runner-benchmark (32, linux, dsh-ubuntu-24-04-32core, typecheck) (push) Has been cancelled
CI / larger-runner-benchmark (32, windows, dsh-windows-2025-32core, production-site) (push) Has been cancelled
CI / larger-runner-benchmark (4, linux, dsh-ubuntu-24-04-4core, typecheck) (push) Has been cancelled
CI / larger-runner-benchmark (4, windows, dsh-windows-2025-4core, production-site) (push) Has been cancelled
CI / larger-runner-benchmark (64, linux, dsh-ubuntu-24-04-64core, typecheck) (push) Has been cancelled
CI / larger-runner-benchmark (64, windows, dsh-windows-2025-64core, production-site) (push) Has been cancelled
CI / larger-runner-benchmark (8, linux, dsh-ubuntu-24-04-8core, typecheck) (push) Has been cancelled
CI / larger-runner-benchmark (8, windows, dsh-windows-2025-8core, production-site) (push) Has been cancelled
CI / larger-runner-benchmark (96, linux, dsh-ubuntu-24-04-96core, typecheck) (push) Has been cancelled
CI / larger-runner-benchmark (96, windows, dsh-windows-2025-96core, production-site) (push) Has been cancelled
CI / consolidated-runner-benchmark (16, linux, dsh-ubuntu-24-04-16core, 16) (push) Has been cancelled
CI / consolidated-runner-benchmark (16, windows, dsh-windows-2025-16core, 2) (push) Has been cancelled
CI / consolidated-runner-benchmark (32, linux, dsh-ubuntu-24-04-32core, 32) (push) Has been cancelled
CI / consolidated-runner-benchmark (32, windows, dsh-windows-2025-32core, 2) (push) Has been cancelled
CI / consolidated-runner-benchmark (4, linux, dsh-ubuntu-24-04-4core, 4) (push) Has been cancelled
CI / consolidated-runner-benchmark (4, windows, dsh-windows-2025-4core, 2) (push) Has been cancelled
CI / consolidated-runner-benchmark (64, linux, dsh-ubuntu-24-04-64core, 32) (push) Has been cancelled
CI / consolidated-runner-benchmark (64, windows, dsh-windows-2025-64core, 2) (push) Has been cancelled
CI / consolidated-runner-benchmark (8, linux, dsh-ubuntu-24-04-8core, 8) (push) Has been cancelled
CI / consolidated-runner-benchmark (8, windows, dsh-windows-2025-8core, 2) (push) Has been cancelled
CI / consolidated-runner-benchmark (96, linux, dsh-ubuntu-24-04-96core, 32) (push) Has been cancelled
CI / consolidated-runner-benchmark (96, windows, dsh-windows-2025-96core, 2) (push) Has been cancelled
CI / all checks passed (push) Has been cancelled
Sandbox / sandbox e2e (seatbelt, macos-latest) (push) Has been cancelled
Sandbox / sandbox e2e (landlock, ubuntu-24.04-arm) (push) Has been cancelled
Landlock Run / ${{ matrix.platform }} (push) Has been cancelled
Landlock Run / darwin (no platform package — degradation proof) (push) Has been cancelled

This commit is contained in:
2026-08-20 13:01:40 +07:00
parent 99f6f02fec
commit ed152416d5
111 changed files with 5038 additions and 43 deletions

View File

@@ -0,0 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# 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

View File

@@ -0,0 +1,90 @@
# Agent Note: OpenRouter cost and balance readouts in the web GUI
Status: implemented
English | [中文](2026-08-20-openrouter-cost-balance-readouts.zh.md)
> Scope: two new packages — `@deepseek-ai/dsh-openrouter-usage` (host gateway +
> projection) and `@deepseek-ai/dsh-client-ui-openrouter-usage` (web readouts) —
> composed into the web profile. The gateway resolves the same
> `OPENROUTER_API_KEY` credential that OpenRouter LLM routing uses.
## Problem
A user running the web GUI with LLM calls routed through OpenRouter has no
visibility into what those calls cost or how much account credit remains.
Token counts exist in the session log (`assistant/message.usage`, `assistant/chunk`
usage chunks) and the token-meter projection, but there is no monetary figure
anywhere in the repository, and no durable record of OpenRouter's per-model
pricing or account balance. The harness deliberately discards the provider's
own cost metadata (the pi-ai adapter zeroes `usage.cost`), so cost had to be
reconstructed from token counts against a pricing table fetched from
OpenRouter's public API.
## Decision
**Cost is a client-facing read-model projection, never a logged or model-visible
fact.** The gateway fetches OpenRouter's model pricing table (`GET /models`,
`pricing.prompt`/`completion` USD per token, flat `request` fee, disclosed
cache rates) and folds each session's logged usage against it in the
`openRouterCost` `SessionProjectionMap` entry. Balance comes from OpenRouter's
canonical balance endpoint `GET /credits` (available credits = `total_credits`
minus the spent `total_usage`, the figure the dashboard surfaces), merged with
`GET /auth/key` for the key label and the monthly token budget
(`usage`/`limit`); a standard `sk-or-v1` key returns **no** `credits` field on
`/auth/key`, so that endpoint alone would always read a null balance. Both
fetches go through plain `globalThis.fetch` with `redirect: 'error'`, the host
standard for credential-bearing outbound calls.
**The web surface is two slot entries.** A per-session cost readout registers
into `conversation.composer.dock` (the shipped stats line's dock), reading the
`openRouterCost` projection through the `useProjection` framework seat; a live
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.
**The API key is a credential reference, seeded identically to routing.**
`apiKeyEnv` (default `OPENROUTER_API_KEY`) resolves through the credentials
seam with an environment fallback exactly like `dsh-web-search-deepseek`,
captured per refresh in a thunk so a credentials or settings change applies on
the next tick without a rebuild.
**No key → no fetch, and the plugin stays inert.** No key produces an empty
pricing table (every step stays unpriced), an all-`null` balance, and the
client reads null-ish values it renders as absent — the same dormant posture
as the LLM adapters, not a load-time failure.
## Alternatives considered
- **Persisting per-request cost as a session event** — cost is derived from
events already logged (`assistant/message` usage, `assistant/chunk` usage);
an event would duplicate accounting, be priced at the wrong time (pricing
changes with `pricingRefreshMs`), and leak a client-facing figure into the
durable log. The projection stays the single read-model home.
- **Balance via `/auth/key` alone** — the plan's original shape; the real
endpoint response for a standard `sk-or-v1` key carries no `credits` field,
verified against OpenRouter's live API, so the snapshot would always read
null. `/credits` is the authoritative balance endpoint and `/auth/key` only
supplements label + monthly budget.
- **Reading `usage.cost` from pi-ai** — the harness deliberately discards
provider cost metadata (the adapter pins `NO_COST`); reconstructing from
tokens × table is the only available path and is documented as an estimate.
- **A settings card and a dedicated credentials UI** — deferred: the readouts
work with the existing credential the routing already uses; a card adds
surface without changing behavior.
## Consequences
`test:gui` stays green (3800 tests) and both packages carry unit tests plus a
real-composition loader test that boots the shipped YAML shape through the
vendored Loader, resolves the key from a credentials document, and asserts
the mocked `/models` and `/credits` fetches populate the projection and the
snapshot. The numbers are an estimate, not itemized billing: the projection
prices `tokens × pricing` per step, pricing is applied as of fold time
(refreshing pricing affects only cells folded afterward), cache rates fall
back to the prompt rate when undisclosed, and free-tier / promotional pricing
may differ from the model table — all documented under Known Limitations.
Nothing is model-visible: the agent loop, session log format, and SDKs are
untouched. Deferred: a settings card, and any per-generation billing fidelity
that would require logging OpenRouter generation ids.

View File

@@ -0,0 +1,32 @@
# Agent Note: OpenRouter cost and balance readouts in the web GUI
Status: implemented
[English](2026-08-20-openrouter-cost-balance-readouts.md) | 中文
> 范围:两个新包 —— `@deepseek-ai/dsh-openrouter-usage`(主机端网关 + 投影)与 `@deepseek-ai/dsh-client-ui-openrouter-usage`(Web 读数)—— 组合进 web profile。网关解析与 OpenRouter LLM 路由相同的 `OPENROUTER_API_KEY` 凭证。
## Problem
使用 Web GUI 并通过 OpenRouter 路由 LLM 调用的用户,无法看到这些调用花费了多少、账户还剩多少额度。token 计数存在于会话日志(`assistant/message.usage`、`assistant/chunk` 用量块)和 token-meter 投影中,但仓库里没有任何货币数字,也没有 OpenRouter 按模型定价或账户余额的持久记录。harness 有意丢弃 provider 自身的成本元数据(pi-ai 适配器将 `usage.cost` 清零),因此成本必须根据 token 计数与从 OpenRouter 公共 API 拉取的定价表重建。
## Decision
**费用是面向客户端的只读模型投影,绝不是被记录或模型可见的事实。** 网关拉取 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。两者在数据缺失时自我隐藏:费用在任何已计价步骤前隐藏,余额在快照尚无值时渲染空的破折号标记。
**API key 是凭证引用,与路由的初始化方式一致。**`apiKeyEnv`(默认 `OPENROUTER_API_KEY`)与 `dsh-web-search-deepseek` 完全一样,通过凭证接缝加环境回退解析,按刷新在 thunk 中捕获,使凭证或设置变更无需重建即可在下一周期生效。
**无 key → 不发请求,插件保持惰性。**无 key 产生空定价表(每个 step 都保持未计价)、全 `null` 余额,客户端把 null 渲染为缺失 —— 与 LLM 适配器相同的休眠姿态,而非加载期失败。
## Alternatives considered
- **将会话事件持久化每次请求费用** —— 费用派生自已记录的事件(`assistant/message` 用量、`assistant/chunk` 用量);事件会重复记账、在错误时间计价(`pricingRefreshMs` 变更定价),并使客户端数字泄漏进持久日志。投影保持唯一的读数模型归属。
- **仅用 `/auth/key` 读取余额** —— 计划的初始形态;真实端点响应中标准 `sk-or-v1` key 不携带 `credits` 字段(已对照 OpenRouter 线上 API 验证),因此快照总会是 null。`/credits` 是权威余额端点,`/auth/key` 只是补充标签与月度预算。
- **读 pi-ai 的 `usage.cost`** —— harness 刻意丢弃 provider 成本元数据(适配器钉定 `NO_COST`);从 token × 定价表重建是唯一可行路径,并记录为估算。
- **提供设置卡片与专用凭证 UI** —— 推迟:读数可以用路由已使用的既有凭证工作;卡片只是界面,不改变行为。
## Consequences
`test:gui` 保持全绿(3800 个测试),两个包都带单元测试加真实组合 loader 测试:把带变形的 YAML 形状(session + 投影注册表 + 凭证 + openrouter-usage)跑进 vendored Loader,从凭证文档解析 key,并断言被 mock 的 `/models` 与 `/credits` 请求填充投影与快照。数字是估算而非逐项账单:投影按 step 以 `tokens × pricing` 计价,计价格次折叠时生效(刷新定价只影响其后折叠的单元格),未披露时缓存费率回退到 prompt 费率,免费档位/促销价格可能与模型表不同 —— 全部记录在 Known Limitations。没有任何内容对模型可见:agent 循环、会话日志格式与 SDK 均未改动。推迟:设置卡片、以及需要记录 OpenRouter generation id 的逐代账单保真度。