dsh-quota —— 把 opencode-go / Grok / Command Code 的额度与用量摊到界面上
DSH 的模型选择器只告诉你「用哪个模型」,不告诉你「这个 provider 这个月还剩多少」、
「我刚用掉多少 token」。这个插件补的正是这两块:参考
yetone/magpie 的 internal/provider(quotas.go、
key_allowance.go、commandcode_plan.go、opencode_endpoints.go)与它的 Usage 页,
直接问厂商自己的接口,并把每次模型调用的 usage 记下来。
界面上是五块:
- composer 下方的常驻窄条(
conversation.composer.dock)—— 不用打开设置就能看到OpenCode Go 35% · Grok ⚠100% · 今日 ≈$0.42;过阈值的那家带 ⚠,点一下刷新。 鼠标停在那家身上会展开它的每一个窗口(滚动窗口 · 2% · 重置 1h 12m、一周 · 19% …、一个月 · 33% …),再加上套餐与余额 —— 用的是原生title,不会被 composer 的 overflow 裁掉。 composer 右侧那枚 chip 与它是同一份数字,默认关掉(showChip: true才回来); - composer 左侧的两格(
conversation.input.left)—— 打开的是哪个文件夹、它在哪个 git 分支上(游离 HEAD 时是短哈希;不是仓库就写「不在 git 仓库里」)。文件夹那格的title是 完整路径,分支那格是仓库根 + 分支名;15 秒问一次 Host,会话一换就重新问; - 设置 → 额度(
settings.section里的quota页)—— 页内分三栏:额度(每个 provider 的卡片:窗口进度条最紧的排最前 + 重置倒计时 + 凭据区,含保存 / 清除 / 测试)、用量(四宫格 + 七天条带 + 模型明细,带估算花费)、输入预测(开关 / 模型 / 提示词); - 列表上方的 + 添加供应商 —— 选一家厂商、贴 key(Grok 是加完再登录),写进 profile 的
cordis.patch.yml,重启还在;模型路由会一起写好,所以加完就能在模型选择器里选到它; - 卡片头上会写清这家有没有接进模型:
已接入模型/未接入模型/仅额度(Command Code 没有可用的聊天接口,只在这里盯额度)。
没有默认供应商:没配置就是空页,只有一句「还没有供应商」和添加入口 —— 凭空出现的卡片 会让人以为模型选择器里也有它们。
一、额度从哪来
| provider | 接口 | 读什么 |
|---|---|---|
| OpenCode Go | GET https://opencode.ai/zen/go/v1/usage |
usage.rolling / weekly / monthly 的 percent 与 resetsAt |
| Grok(SuperGrok / X Premium) | GET https://cli-chat-proxy.grok.com/v1/billing?format=credits |
config.creditUsagePercent、currentPeriod、productUsage(GrokBuild / Cursor / GrokBot 分项)、prepaidBalance、有 cap 时的 onDemand* |
| Command Code | GET https://api.commandcode.ai/alpha/billing/creditsGET https://api.commandcode.ai/alpha/billing/subscriptions |
windowLimits.fiveHour / weekly 的 used/cap/resetAt;credits.monthlyCredits + purchasedCredits + freeCredits 当月余额;订阅给出套餐名(Go / Pro / GOAT / Max / Ultra)、到期与是否续费 |
| DeepSeek 官方 API | GET https://api.deepseek.com/user/balance |
is_available,以及 balance_infos[0] 的 total_balance / granted_balance / topped_up_balance(按币种取符号:CNY ¥、USD $) |
| ChatGPT 订阅(Plus / Pro / Team,和 Codex 共用) | GET https://chatgpt.com/backend-api/wham/usage |
rate_limit 的窗口。按 limit_window_seconds 认成 5 小时或一周(团队套餐现在常常只回一周,而且放在 primary_window 上)。套餐名来自 plan_type。登录读 ~/.codex/auth.json 的 ChatGPT access token,不是 OpenAI API key |
| Claude 订阅(Pro / Max,和 Claude Code 共用) | GET https://api.anthropic.com/api/oauth/usage |
five_hour / seven_day 的 utilization(已经是 0–100 的百分比)和 resets_at。有单独的 Sonnet / Opus 窗口才画。套餐名来自 ~/.claude/.credentials.json 的 subscriptionType |
| 阿里云百炼 | —— | 没有公开的余额 / 用量接口(账单在阿里云 Billing API,要 AccessKey,不是 DashScope key),所以这张卡只存 key,不编数字 |
Command Code 的 billing/credits 不告诉你套餐,而当月额度的分母是套餐给的(GOAT = $70/月),
所以第二支接口并行只读一次、缓存 10 分钟(magpie 也是这么做的;它有时要 15–20 秒,
排在后面会把整张卡拖慢)。读不到就退回「只显示余额」。
卡片和左侧额度面板优先显示 5 小时窗口,其余窗口按已用比例排序。窄条优先显示 5 小时;告警仍检查所有窗口。左侧面板可刷新并滚动查看供应商。
DeepSeek 的卡是余额,不是窗口:官方只给金额、不给分母,所以它不画进度条,只报
余额 · ¥110.00 和构成(充值 / 赠金),is_available: false 时在金额前写明「can not pay」。
它用的是 DEEPSEEK_API_KEY;DSH 账号本身的余额仍然看「账号与余额」页 —— 两套账单不是一回事。
这几个端点在这台机器上真实跑通过,不是照文档猜的形状 —— test/live-probe.mjs 可随时复看。
二、用量从哪来
llm/stream 是 DSH 每次模型调用都要过的 waterfall。插件包住它:每个 chunk 原样传下去
(看不见任何差别),同时记住结束前那一个 usage chunk ——
{ inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens, reasoningTokens } ——
和这次调用的 provider / model。失败的调用(finish.kind = error | aborted)不记。
于是「谁发起的调用」都算得到:对话、压缩、自动起标题。数据落在一个小 JSON 里:
$DSH_HOME/.quota-usage.json # 默认 ~/.dsh/.quota-usage.json
{ "version": 1, "days": { "2026-10-20": { "totals": {...}, "models": { "opencode-go/deepseek-v4.1-flash": {...} } } } }
保留 30 天,页面看近 7 天 + 今天;写入是 2 秒防抖的一次原子替换(先写 .tmp-<pid> 再 rename),
插件卸载时再刷一次。计数不是业务数据,写失败只吞掉,不打扰模型调用。
页面上:TOKEN 总量(输入 / 输出)、缓存读取(含命中率 = 缓存 /(缓存 + 新输入))、
推理 token、调用次数,七天条带,以及按 provider / model 的明细(重的那条在前)。
花费:按目录价折算
build/prices.js 是从 pi-ai 目录里抽出来的价目表(USD / 百万 token,input / output / cacheRead / cacheWrite),由 tools/gen-prices.mjs 从 _ref/pi-ai/dist/providers/data/*.json
生成,覆盖当前这套 route 用到的 36 个模型。有价就出数:TOKEN 卡上写近 7 天的 ≈$3.17,
每条模型行写它自己的 ≈$0.013,窄条写今天。
这是估算,不是账单:OpenCode Go / Grok 是订阅制,不是按 token 计价;按目录价折算只是让你 横向比较「今天这几轮大概值多少钱」。要改价,在配置里覆盖:
prices:
"opencode-go/deepseek-v4.1-flash": { input: 0.15, output: 0.6, cacheRead: 0.003 }
阈值告警
alertAt(默认 90)与 alertCriticalAt(默认 97)按「最紧的那个窗口」判;余额卡按
balanceFloor(默认 5,同一币种单位)判。命中的卡片头挂 ⚠ 接近上限 / 已达上限,
窄条上那家的百分比变红并加 ⚠,页面顶部也会写一行「已达上限 · Grok」。
三、凭据:登录,或者存 key
插件自己不落任何秘密,两条路都走 DSH 的凭据缝(ctx.credentials):
Grok:设备码登录(会真的把 DSH 接通)
点「登录 Grok」→ Host 用 pi-ai 同一个 client_id(b1a00492-…)、同一套 scope 去
auth.x.ai/oauth2/device/code 领设备码 → 页面显示 user code 和带 code 的授权链接 →
你在浏览器里确认 → Host 轮询 /oauth2/token → 拿到 grant 后写进
~/.dsh/.credentials.yaml → records["llm-pi-ai/xai"]
{ kind: grant, payload: { type: oauth, access, refresh, expires } }
也就是 dsh-llm-pi-ai 自己读的那条记录 —— 所以在这里登录完,DSH 里选 Grok 模型就能直接对话。
token 快过期(< 5 分钟)时读额度会先用 refresh_token 续一次并回写,续不动就让卡片说「登录过期」。
「退出登录」删掉这条记录。
OpenCode Go / Command Code:API key
粘贴 key → POST /quota/key → ctx.credentials.set("OPENCODE_GO_API_KEY" | "COMMANDCODE_API_KEY", …)。
和 Models 页同一个存储,所以优先级也一样:凭据库 > 环境变量 > 项目 .env > ~/.dsh/.env;
卡片写明当前值来自哪一层,清除 只删凭据库那份。
Command Code 没存 key 时自动读它 CLI 的登录 ~/.commandcode/auth.json(apiKey + userName),
卡片显示「使用 Command Code CLI 的登录 · 用户名」。这条只读,所以没有「清除」。
阿里云百炼:只有 key
百炼的 DashScope key 是 OpenAI 兼容的(https://dashscope.aliyuncs.com/compatible-mode/v1),
但这版 pi-ai 的 provider 目录里没有百炼。所以除了把 key 存成 DASHSCOPE_API_KEY,添加这个
供应商时还会往 llm-pi-ai.providers 写一条路由:
yaml ali: displayName: 阿里云百炼 apiKeyEnv: DASHSCOPE_API_KEY api: openai-completions baseURL: https://dashscope.aliyuncs.com/compatible-mode/v1 models: [qwen3.8-max, qwen3.8-flash]
模型 id 是从 GET /compatible-mode/v1/models 的实际返回里挑的(那份列表有 178 个)。
之后调用产生的 token 照样进上面的用量统计。
四、添加 / 移除供应商
列表上方一个「+ 添加供应商」按钮。点开只有三样:厂商、可选的显示名、可选的 API key
(Grok 是加完在卡片里点登录)。不再问 id / 网关地址 / 凭据引用 —— 那些是给第二套网关
准备的,收在「高级」里。添加成功后自动收起,id 由 Host 从预设推出(就是模型路由的名字)。
写的是 profile 自己的 patch 层(cordis.patch.yml 里 id 为 dsh-quota 的那一行),
不是插件内部的状态文件 —— 所以重启、升级都还在,也能被别的工具读到。没有内置供应商:
不配置就是空页,不会替用户初始化五张卡。
现在有七种预设:opencode-go、xai-billing(Grok,走设备码登录)、commandcode、
deepseek-api(只有余额)、bailian(只存 key)、chatgpt(ChatGPT 订阅,读 Codex 登录)、
claude(Claude 订阅,读 Claude Code 登录)。已经加过的厂商不再出现在下拉里。
添加时同时接模型,这就是它和以前不一样的地方:
| 预设 | 模型侧 |
|---|---|
opencode-go / deepseek-api |
pi-ai 目录认识它们:只往 llm-pi-ai.providers 补一条 apiKeyEnv,模型目录原样保留 |
xai-billing(Grok) |
补 xai: {},认证走登录记录(写 apiKeyEnv 会盖掉登录,所以不写) |
bailian |
目录里没有:写完整路由(api / baseURL / models) |
commandcode |
没有可用的聊天接口:只做额度,卡片上标「仅额度」 |
chatgpt |
接入 openai-codex 目录路由,不写 API key;额度读取 Codex 本机登录 |
claude |
接入 anthropic 目录路由,不写 API key;额度读取 Claude Code 本机登录 |
额度读取和模型认证是两套登录:接入模型后,前往 DSH 的模型供应商设置完成对应 OAuth 登录。插件不会复制 CLI 令牌到模型凭据库。既有路由会保留原配置;如果已有 apiKeyEnv,它会优先于 OAuth,可能使用 API 计费。Codex 的目录路由由上游标为 legacy;Claude 订阅是否能在第三方客户端使用取决于供应商当前政策和服务端。CODEX_HOME 和 CLAUDE_CONFIG_DIR 可指定 CLI 登录目录。
写失败时额度行不会落盘 —— 不会出现「卡片有、模型里没有」的半截状态。与凭据有关的规则
没变:apiKeyEnv 填的是名字,不是 key(填成 key 会被 400 拒掉;配置里已经是 key 形状
的,读的时候会被丢掉、退回预设的引用)。移除时:插件自己写的路由会一起删,pi-ai 目录里
本来就有的路由保留 —— 那可能早就在给模型用了。
卡片头会写明 已接入模型 / 未接入模型 / 仅额度;写着「未接入模型」的那张卡旁边有
「接入模型」,点一下只补模型路由,不会把卡片加成第二张 —— 老配置(比如百炼那条路由还叫
ali)也能接上:插件按 baseURL 认得出同一个网关。
卡片里的模型清单
每张已接入模型的卡里有一条可展开的「▸ 模型」:
| 动作 | 做什么 |
|---|---|
| 展开 | 列出这条路由当前提供的模型(llm.listModels(provider)) |
| 获取模型 | 调 llm.discoverModels('llm-pi-ai', { provider, baseURL?, api? }):pi-ai 目录认识的按目录回答(不发请求);百炼这种手工路由才拿存着的 key 去问 GET {baseURL}/models。结果以候选行出现,点「加入」才写进去 |
| 添加模型 | 手填 id + 可选显示名,追加到 llm-pi-ai.providers[provider].models |
| 删除 | 从同一份 models 里去掉;最后一条不让删(pi-ai 会拒绝解析不出模型的路由) |
写的是模型路由自己的 models 列表,所以模型选择器下一次请求就能看到。代价要记住:
目录路由一旦被写上了 models,就固定成这份清单、不再跟随 pi-ai 目录(页面上有一行小字
写着);删掉的模型可以再加回来。
五、输入预测(输入框里的灰字)
参考 yepyeel/dsh-prompt-suggestion 的做法, 在这里重写了一份,和额度共用一套 Host。行为:
- 一轮回答结束(最后一条事件是
turn/end且reason.kind = completed)、输入框空着、 会话没在跑 → 隔debounceMs(默认 700ms)去问一次模型; - 模型答案洗掉引号/列表符号、只留第一行,作为灰字贴在光标后面(
conversation.input.overlay); 输入框空着时它会顶掉 placeholder(两者都在左上角),光标量不出矩形(空段落常见)就退回 编辑区文字起点; - Tab 或 → 填入(
inputActions.captureInsertion()+insertText),Esc 忽略,永不自动发送; 一旦你开始打字,灰字和那次请求一起作废(AbortController); - 用哪个模型:设置里指定就听指定的,否则跟随当前会话的选择,再否则用 profile 的默认模型。
预测本身是一次真实的模型调用,会算进上面的用量与花费 —— 不这样反而会骗自己。
设置页最下面就是它的开关、模型下拉和提示词(留空用内置的那段)。
Host 侧只有 build/prediction.js 一个文件:suggest(sessionId) 读 ctx.sessions 里那个会话的
事件流与 deriveMessages(),调 ctx.llm.stream(...) 收 text-delta。
六、安装
从 GitHub 安装:
dsh plugin --profile desktop add github:xuluoluo/dsh-quota
本地目录重装/更新:
plugin_manager install_bundle target = D:\Documents\deepseek-harness\default-workspace\plugins\dsh-quota
生效规则:客户端 JS 每次页面加载都重新读盘,改 build/client.js 后刷新页面即可;
Host 侧(index.js / providers.js / usage.js / xai-oauth.js / workspace.js)换了代码要重启 DSH。
七、配置(都是可选的)
- id: dsh-quota
name: 'dsh-allowance'
config:
enabled: true # false = 整页停用(chip 也消失)
refreshMs: 60000 # 额度快照缓存与自动刷新间隔(10s–1h)
timeoutMs: 8000 # 单个厂商接口超时
providers: # 不写就是空页;写了就是整份替换,页面上的「添加」也是写到这儿
- id: opencode-go
preset: opencode-go
apiKeyEnv: OPENCODE_GO_API_KEY
- id: grok
preset: xai-billing
record: llm-pi-ai/xai
- id: commandcode
preset: commandcode
apiKeyEnv: COMMANDCODE_API_KEY
- id: deepseek
preset: deepseek-api
apiKeyEnv: DEEPSEEK_API_KEY
- id: bailian # 目录里没有 → 需要 llm-pi-ai.providers 里那条路由
preset: bailian
apiKeyEnv: DASHSCOPE_API_KEY
- id: my-oc # 换网关:baseURL 替换预设的默认根路径
preset: opencode-go
baseURL: https://opencode.ai/inference/go
predict: # 输入预测(默认关)
enabled: true
provider: opencode-go # 留空则跟随会话模型
model: deepseek-v4.1-flash
systemPrompt: '' # 留空用内置提示词
maxTokens: 120
maxContextMessages: 12
debounceMs: 700
showChip: true # 把 composer 右侧那枚 chip 打开(默认只有下方窄条)
alertAt: 90 # 最紧窗口到这个百分比 → ⚠ 接近上限
alertCriticalAt: 97 # 再到这个 → ⚠ 已达上限(红色)
balanceFloor: 5 # 余额卡低于这个数(同币种单位)也算告警
prices: # 覆盖/补充 build/prices.js 的目录价(USD / 百万 token)
"opencode-go/deepseek-v4.1-flash": { input: 0.15, output: 0.6, cacheRead: 0.003 }
八、HTTP 面(前端与 Host 之间)
都是同源 JSON,写在 build/index.js 的 ROUTES:
| 方法 | 路径 | 作用 |
|---|---|---|
| GET | /quota/state |
快照:卡片 + 用量 + 预设清单(额度缓存 refreshMs) |
| GET | /quota/workspace?sessionId= |
composer 左侧那两格:{root, name, branch, gitRoot, detached};branch: null = 不是仓库 |
| POST | /quota/refresh |
强制重读额度 |
| POST | /quota/key |
{id, key} → 写凭据库 |
| POST | /quota/clear |
{id} → key 就 unset,oauth 就删记录 |
| POST | /quota/test |
{id} → 真打一次厂商接口,回 {ok, detail}(页面上的「测试」按钮) |
| POST | /quota/suggest |
{sessionId} → {suggestion, reason};关掉时 reason: disabled,不碰模型 |
| GET | /quota/models |
预测的模型下拉:{groups: [{provider, name, models}]} |
| POST | /quota/predict |
{enabled?, provider?, model?, systemPrompt?, maxTokens?, debounceMs?} → 写进 patch |
| POST | /quota/providers |
{preset, label?, key?, baseURL?, apiKeyEnv?, id?} → 写额度行 + 接模型路由 + 存 key |
| POST | /quota/providers/attach |
{id} → 只补这家缺的模型路由(卡片上的「接入模型」) |
| GET | /quota/provider/models?id= |
这家的模型清单:{provider, models: [{id, name, contextWindow?, maxTokens?}]} |
| POST | /quota/provider/models/discover |
{id} → 问厂商要模型(catalog 直接答):{ok, models} |
| POST | /quota/provider/models/add |
{id, model: {id, name?}} → 追加到 llm-pi-ai.providers[].models |
| POST | /quota/provider/models/remove |
{id, modelId} → 从那份 models 里去掉(至少留一条) |
| POST | /quota/providers/remove |
{id} → 从 patch 移除;插件自己写的模型路由顺带删掉,pi-ai 目录里的路由保留 |
| POST | /quota/login/start |
领设备码,返回 userCode / 授权链接 |
| GET | /quota/login/status?id= |
pending / success / failed / cancelled |
| POST | /quota/login/cancel |
取消一次登录 |
同源规则:只拦「别的站点的页面」。桌面渲染进程向自己的 Host 发请求时既不带 Origin 也不带
Sec-Fetch-Site,所以没有 Origin 就当作本机客户端放行;带了 Origin 的,只有 loopback
(127.0.0.1 / localhost / [::1],任意端口)、Sec-Fetch-Site: same-origin|none、或与 Host
一致才放行,其余回 403 并把 origin / host / site 写进响应 —— 页面直接显示,这类问题下次能自己
说出原因(上一版就是被“写操作必须有 Origin”挡掉了自己的「保存」「移除」)。
安全边界(别忽略):这些路由由 Host 的 webServer 直接注册,不经过 DSH 页面那层 token 校验
(GET / 要 token,GET /quota/state 不要)。它们只回额度数字、套餐名、token 计数和
「凭据是否配置 / 来自哪一层」,从不回 token 或 key 明文;写操作受上面的同源规则约束。
本机任意进程都能读到这些数字 —— 在「本机进程本来就能读 ~/.dsh/.credentials.yaml」的前提下,
这不算新增暴露面。
九、验证
cd plugins\dsh-quota
node test\providers.test.mjs # 四个 reader 的纯逻辑(38 项)
$env:QUOTA_LIVE='1'; node test\providers.test.mjs # 再打一次真接口(本机凭据)
node test\host.test.mjs # Host:路由、凭据、续期、登录、供应商、模型清单、工作区、用量、同源(108 项)
node test\client.test.mjs # Client:注册、取数、渲染、用量、成本、告警、窄条、工作区、模型清单、预测灰字、存 key、登录、加供应商(98 项)
node test\prediction.test.mjs # 预测:摘录、清洗、空闲判断、模型选择(22 项)
node test\workspace.test.mjs # 工作区:文件夹名、分支、游离 HEAD、探针缓存(10 项)
node tools\grok-login.test.mjs # grok-login 的 YAML 写入回归(读本机凭据文件)
装好之后真进程上也能直接看:
curl.exe -s http://127.0.0.1:62756/quota/state # 卡片 + usage + presets
curl.exe -s -o NUL -w '%{http_code}' -X POST -H 'content-type: application/json' `
-H 'origin: http://127.0.0.1:62756' --data '{}' http://127.0.0.1:62756/quota/refresh
本机实测(2026-10-20 那次装完时):
opencode-go rolling 2% weekly 19% monthly 33%
grok weekly 100%(本周额度用尽) GrokBuild 92 / Cursor 5 / GrokBot 3
commandcode 5h 0% weekly 12% 当月 $69.93 / $70.00 plan=GOAT 10-22 到期不续
没验到的:真实浏览器里的像素级观感(没有浏览器控制)。client bundle 的行为用最小 hooks 运行时跑通了,slot 注册与文档里的名字一致。
十、回滚
plugin_manager remove_bundle target = dsh-allowance,或删掉 profile patch 里那一行;- Grok 登录写的是
llm-pi-ai/xai,删掉它等于退出登录,不影响其它记录; ~/.dsh/.quota-usage.json可以随便删,删掉只是用量清零。
十一、目录
package.json dsh.bundle.patch + dsh.client + exports/files
cordis.patch.yml insert: dsh-quota
icon.svg 自绘的仪表盘图标
locale/{en,zh}.json meta.title / meta.description
build/providers.js 四个 reader + fetch(纯函数)
build/usage.js 用量折叠 / 汇总 / 花费折算 / 落盘(纯函数 + 一个小 store)
build/prices.js 目录价表(由 tools/gen-prices.mjs 生成,USD / 百万 token)
build/prediction.js 输入预测:读会话、问模型、洗答案(纯函数 + 一个小服务)
build/workspace.js 工作区:文件夹名 / git 分支 / 探针缓存(exec 注入,可离线测)
build/xai-oauth.js 设备码 / 续期 / grant 形状(fetch 注入)
build/index.js Host:配置、快照、路由、登录、供应商、工作区、用量接线
build/client.js Client:chip + 工作区两格 + 设置页(纯 JS,无构建步骤)
test/*.mjs 上面那三组测试 + live-probe
tools/*.mjs 与本机凭据打交道的命令行工具(不进 npm 包,见 package.json 的 files)
grok-login.mjs 走设备码登录并把 grant 写进 ~/.dsh/.credentials.yaml
grok-verify.mjs 验证那条凭据能不能直接当 xAI API 凭据用
grok-login.test.mjs grok-login 的 YAML 写入回归
gen-prices.mjs 从 pi-ai 目录重生成 build/prices.js
asar.mjs 从 DSH 的 app.asar 里 list / cat / extract 包源码
tools/ 里的脚本要读 YAML,用的是仓库外 _ref/yaml 的本地副本;那份第三方代码不进仓库,
缺席时脚本会明确报错退出。
No comments yet. Be the first to write one.