dsh-provider-quote
Manifest validDSH Plugin: Request a quote from the currently used model provider (balance + unit price, referencing cc-switch's "Service Provider Inquiry")
provider-quote · 对正在使用的模型提供商询价
参考 cc-switch 的「服务商询价」,把同一件事做进 DSH:当前在用哪个提供商,就查它的余额与单价。
| 问的问题 | 数据来源 |
|---|---|
| 我还有多少钱? | 按 baseURL 自动分派厂商余额适配器(见下表),也可直接粘贴 cc-switch 的 usage_script |
| 这个模型多少钱? | OpenRouter 公开价格目录 https://openrouter.ai/api/v1/models(输入 / 输出 / 缓存读写,USD per 1M tokens),可本地覆盖 |
| 我能用哪些模型? | 提供商自己的 GET /models(OpenAI 兼容) |
装什么
一个 bundle,两半:
- 宿主
index.js—— 包根转发(export * from './lib/host.js')lib/host.js—— 宿主实现;lib/pricing.js—— 与 DSH 无关的纯询价引擎(适配器、价格目录、报价与文本渲染)- 工具
provider_quote—— agent 直接问价:provider_quote()/provider_quote({ all: true })/provider_quote({ provider: 'openrouter' })/provider_quote({ models: true }) - 路由
GET /api/provider-quote/summary(?refresh=1跳过 30s 报价缓存)GET /api/provider-quote/refresh(同上,路径别名)GET /api/provider-quote/ping—— 自检:版本、当前加载的模块 URL、目录缓存里的模型数
- 可选的定时预热(
autoRefreshMinutes)
- 客户端(
client.js) —— 两个挂载点:- 对话输入框下方的常驻胶囊(slot
conversation.composer.dock,与自带的stats同一行):● 余额 12.4 USD—— 只显示当前会话选中的提供商的余额;在输入框的模型选择器里换提供商,胶囊立刻跟着换(报价本身含所有提供商的余额,不需要重新询价);点一下立即重新询价(走?refresh=1,跳过宿主的 30s 缓存),查询中是「余额查询中…」,失败是「余额没查到」,选中的提供商没被收录时是「余额未知」并自动重查一次;悬停可见提供商、余额来源与失败原因;每 5 分钟静默刷新一次。要看各提供商余额、模型单价与目录候选,用下面的设置页面板。 - 「设置 → 模型」页底部的询价面板(slot
settings.models.footer):当前提供商的余额、当前模型的单价、其余提供商的概览,带「刷新询价」按钮。 - 两个组件共用同一份取数(模块内 store),不会重复请求宿主。
- 对话输入框下方的常驻胶囊(slot
安装
# 在会话里用 plugin_manager 指定工作区目录(不要在 profile 目录里手写 package.json / 跑 pnpm)
plugin_manager action=install_bundle target=<本插件的绝对路径,如 ~/dev/dsh-provider-quote>
装完看返回的 application 与 warnings:applied 即生效;宿主代码改动后需要重启 DSH 才会加载新代码(见文末「注意」)。
余额适配器
adapters: auto(默认)时按 baseURL 的 host 优先、其余适配器兜底依次尝试;每次查询会记录试过的 URL 与 HTTP 状态,失败也给出可解释的原因。
| id | 接口 | 适用 |
|---|---|---|
openrouter | GET {origin}/api/v1/credits(+ /api/v1/key) | OpenRouter 官方:余额 = total_credits − total_usage |
deepseek-official | GET {origin}/user/balance | DeepSeek 官方:balance_infos[](CNY/USD),is_available |
relay-user-balance | GET {base}/user/balance、{origin}/user/balance | 多数中转站的通用布局:balance / remaining / is_active / unit |
one-api | GET {base}/api/user/self | one-api / New API 布局:quota(÷500000 估算 USD,仅参考) |
openai-billing | GET {base}/dashboard/billing/subscription + /usage | 旧版 OpenAI 计费接口 |
script:<host> | 你配置的 scripts | 任意厂商:直接粘贴 cc-switch 的 usage_script |
自定义脚本与 cc-switch 完全兼容,模板变量 {{baseUrl}} / {{apiKey}} 会被替换:
scripts:
relay.example.com: >-
({ request: { url: "{{baseUrl}}/user/balance", method: "GET",
headers: { "Authorization": "Bearer {{apiKey}}", "User-Agent": "cc-switch/1.0" } },
extractor: function (response) { return { isValid: response.is_active || true, remaining: response.balance, unit: "USD" }; } })
以上这条就是从 cc-switch 的 cc-switch.db → providers.meta.usage_script 里原样抄来的:把你在 cc-switch 里给某个供应商配好的脚本粘到 scripts.<host>,行为与 cc-switch 一致(插件不直接读 cc-switch 的数据库,只认这个格式)。
中转站与其他网关
「中转站」指第三方把各家官方 API 转卖或聚合出来的一个 OpenAI 兼容端点(常见 one-api、New API 面板,也有自研网关)。在 DSH 里它就是一个 baseURL 不同的普通提供商,但有几个特点值得知道:
- 余额接口各家不同,没有统一标准:
GET {base}/user/balance(多数中转站,字段balance/remaining/is_active/unit)、one-api 系的GET {base}/api/user/self(quota,÷500000 估算 USD)、旧版 OpenAI 的/dashboard/billing/*;也有部分网关(只转发、不做结算的那种)根本没有公开余额接口。插件按baseURL的 host 优先匹配、其余适配器兜底逐个试,查不到也会列出试过的 URL 与 HTTP 状态,而不是留白。 - 有倍率:中转站通常按自己的倍率计费(cc-switch 里叫
cost_multiplier)。插件显示的单价来自 OpenRouter 公开目录,是参考价;对不上就把实付价写进priceOverrides(可直接给input/output/cacheRead/cacheWrite)。 - 模型会重名:同一个 id(例如
deepseek-flash)在不同站可能是不同后端,所以插件不猜价 —— 匹配不到就写「未匹配到价格」并给出目录候选,由你确认后再priceOverrides锚定。 - key 走凭据名:配置里写
apiKeyEnv: MY_RELAY_API_KEY这类名字,插件用ctx.credentials按需解析(改 key 立即生效),从不落盘、不打印明文。 - 同一站可以配多个条目:比如同一个中转站分成「按月套餐」和「按量付费」两个 key,写两个 provider id 即可分别看余额与单价。
单价匹配
模型 id 依次按 priceOverrides.<model>.openrouterId → 提供商返回的 openrouter.slug → 完整 id → 裸模型名 → 归一化名(去厂商前缀与符号)匹配目录;匹配不到就如实写「未匹配到价格」,不猜价,并在括注里给出目录中的候选(suggestions,最多 3 条):
deepseek-flash 未定价(候选:deepseek/deepseek-v4-flash、deepseek/deepseek-v4.1-flash、deepseek/deepseek-v4-flash-0731)
看到候选就说明「大概是哪个型号」,确认后写进 priceOverrides 即可(两形态都支持):
priceOverrides:
deepseek-flash: { openrouterId: deepseek/deepseek-chat } # 指向目录里的对应条目
my-local-model: { input: 0.3, output: 0.9, cacheRead: 0.03, cacheWrite: 0.4 }
价格目录缓存
目录一次抓 458 个模型,默认缓存 6 小时(catalogTtlMs):
- 缓存里存的是整个目录(不是摘要),重放缓存时仍能正常定价;
- 抓取失败(超时 / 非 JSON / 返回 0 个模型)时保留上一次成功的目录并标记
stale,面板显示「· 沿用缓存」,文本输出写「沿用上次成功获取(重取失败: …)」; - 完全没有可用目录时才显示「价格目录不可用」。
提供商从哪来
- 自动发现:读取 profile 里各插件行的生效配置(
configEditor),凡出现providers: { id: { baseURL, apiKeyEnv, models } }形状即登记 —— 例如官方llm-pi-ai适配器里配的中转站条目。解析不出baseURL的条目(如凭据型 provider 的占位行)会被跳过,只有你在config.providers里显式写了才保留。 - API key:
apiKeyEnv指向的凭据名经ctx.credentials解析(每次查询重新解析,改 key 立即生效),插件从不读写、也不打印明文。 - 兜底:插件自己的
config.providers可覆盖或补全(baseURL/apiKeyEnv/adapters/skipModels)。 - 内置路由:像
deepseek-official(DeepSeek 官方账号,凭据型 provider,配置里不写baseURL)这类官方提供商由插件内置映射到官方端点 ——deepseek-official→https://api.deepseek.com+ 凭据DEEPSEEK_API_KEY,因此切到它也能查到余额(GET /user/balance,CNY)。
配置(cordis.patch.yml 中该行的 config)
| 键 | 默认 | 含义 |
|---|---|---|
enabled | true | 关掉整行 |
adapters | auto | 'auto' 或适配器 id 数组(写死集合) |
timeoutMs | 12000 | 单个 HTTP 请求超时 |
catalogTtlMs | 21600000 | 价格目录缓存(6h) |
autoRefreshMinutes | 0 | 后台预热间隔,0 = 关闭 |
scripts | {} | host/glob → cc-switch usage_script |
priceOverrides | {} | 模型 → 目录 id 或直给单价 |
providers | {} | provider id → { baseURL, apiKeyEnv, adapters, skipModels } |
演示(模拟数据)
下面这段是 node tests/demo-openrouter.mjs 的输出:以 OpenRouter 为例,用 fetch 桩喂进 credits / key / 价格目录 / 模型列表,跑真实的报价管线(适配器分派、目录解析、单价匹配、文本渲染都是真的),余额与单价数字是编的:
$ node tests/demo-openrouter.mjs
# 提供商询价 2026-10-10T03:41:55.634Z
当前使用:openrouter · anthropic/claude-sonnet-4.5
价格目录:https://openrouter.ai/api/v1/models(458 个模型)
## OpenRouter ★ 使用中
- baseURL: https://openrouter.ai/api
- 余额: 12.4 USD · 总额 25 USD,已用 12.6 USD · 账户有效 · 来源 GET /api/v1/credits
- 单价(USD / 1M tokens,2/2 个模型已定价):
- ★ anthropic/claude-sonnet-4.5: 入 $3 / 出 $15 / 缓存读 $0.3(USD / 1M tokens) ←目录 anthropic/claude-sonnet-4.5
- openai/gpt-5-mini: 入 $0.25 / 出 $2 / 缓存读 $0.025(USD / 1M tokens) ←目录 openai/gpt-5-mini
在 DSH 里等价的取数方式(换成你自己的提供商会返回真实数字):
curl -s "http://127.0.0.1:<DSH 端口>/api/provider-quote/summary" # 加 ?refresh=1 跳过 30s 缓存
curl -s "http://127.0.0.1:<DSH 端口>/api/provider-quote/ping" # 自检:版本、模块 URL、目录缓存模型数
$ curl -s "http://127.0.0.1:<DSH 端口>/api/provider-quote/ping"
{"ok":true,"version":"0.3.0","entry":"lib/host.js","catalogModels":458,
"adapters":"auto","providers":["openrouter","deepseek-official"]}
agent 侧则直接用工具:provider_quote() / provider_quote({ models: true }) / provider_quote({ provider: 'openrouter' })。
自测
node tests/demo-openrouter.mjs # 演示:fetch 桩喂 OpenRouter 数据跑真实报价管线(不联网、不需要 key)
node tests/smoke.mjs --offline # 16 项:只跑纯函数与缓存逻辑(含 fetch 桩,不联网)—— 任何机器都能直接跑
node tests/client-smoke.mjs # 11 项:客户端 bundle 的模块工厂 + 两个 slot 注册 + 面板渲染 + 余额胶囊(显示/点击刷新/失败态/跟随提供商选择)
联网的那一段(真实余额 / 模型列表 / 价格目录)仓库里不含任何真实端点,要自己用环境变量把目标传进去 —— 目标清单是你自己的,不会进版本库:
DSH_QUOTE_LIVE='[{"id":"openrouter","name":"OpenRouter","baseURL":"https://openrouter.ai/api",
"apiKeyEnv":"OPENROUTER_API_KEY","models":["deepseek/deepseek-chat"],
"expectBalance":true,"expectModels":true}]' \
DSH_CREDENTIALS=~/.dsh/.credentials.yaml node tests/smoke.mjs
每条目标支持 usageScript(cc-switch 脚本,按该 host 生效)、priceOverrides、expectBalance / expectModels / expectFailure;凭据只从 DSH_CREDENTIALS(默认 ~/.dsh/.credentials.yaml)读取,从不打印明文,不设 DSH_QUOTE_LIVE 就跳过该段。
lib/pricing.js 不 import 任何 DSH 模块,所以适配器可以在 DSH 之外独立验证 —— 这也是上面脚本能直接跑的原因。
注意与已知边界
- 改宿主代码后必须重启 DSH:loader 按「解析后的模块 URL」缓存模块,换文件名(新 URL)或
plugin_manager set_bundle关/开可以在运行中切到新代码代,同一 URL 的旧模块实例换不掉。客户端半(client.js)不受影响:保存后set_bundle关/开即会在页面上重新挂载(无需重启)。 - 价格是 OpenRouter 目录参考价;中转站常有自己的倍率,差额需用
priceOverrides校正。 - 部分网关(只转发不结算的那种)没有公开余额接口,面板会明确显示「额度没查到」并列出试过的接口,与 cc-switch 的表现一致。
- 面板不会写配置:
scripts/priceOverrides/providers目前手写在cordis.patch.yml的 config 里(改完重启 DSH 生效)。 - 插件只读不写:查询余额会带上你的 API key 去对应厂商自己的接口,插件不发往任何第三方;
client.js也只在本地渲染,不上报任何数据。
开源协议
MIT © 2026 miku05231
你可以自由使用、修改、分发(保留版权与许可声明即可)。插件不自带任何依赖、不收集数据;查询余额时会把凭据发往你自己配置的提供商端点,这是该功能本身必需的。
Comments
Loading…