@gehennawu/dsh-service 1.6.2 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/quota-adapters.js CHANGED
@@ -25,6 +25,30 @@ function quotaErrorCode(error) {
25
25
  return family === 'http-status' || family === 'upstream-status' ? raw : family
26
26
  }
27
27
 
28
+ // 业务错误信封(HTTP 200 + `{code,msg,success:false}`)里的错误码不是「响应格式异常」。
29
+ // 上游把「凭据被拒 / 套餐失效 / 服务自身故障」都塞进 200 信封,只看 HTTP 状态会把三者
30
+ // 一律误报成格式问题(智谱开放平台实测:凭据有效、订阅生效,配额端点仍回
31
+ // `{"code":500,"msg":"内部服务器错误","success":false}`;同一凭据的推理端点正常回 1308 限流)。
32
+ // 码表取自智谱《错误码》文档,只对语义明确的码定族;未知码仍走 bad-payload 并透出上游原话——不猜。
33
+ // 归属:凭据类码走与 HTTP 401/403 同一条路(换候选 → 链尾归 credential-rejected,行回 unconfigured
34
+ // 以便就地重填);套餐失效 = no-subscription;服务故障 = upstream-error(5xx 语义,不进
35
+ // QUOTA_UNUSABLE_ERROR_RE,故不会因配额服务抖动把渠道从子代理路由里摘掉——推理平面是好的)。
36
+ const QUOTA_ENVELOPE_AUTH_CODES = new Set([401, 403, 1000, 1001, 1003, 1005])
37
+ const QUOTA_ENVELOPE_NO_SUBSCRIPTION_CODES = new Set([1309, 1314])
38
+ const QUOTA_ENVELOPE_CREDENTIAL_CODES = new Set([1315])
39
+ const QUOTA_ENVELOPE_UPSTREAM_CODES = new Set([500, 1200, 1230, 1234])
40
+
41
+ /** 业务信封错误码 → 稳定错误码族(auth 由调用方按 401 处理);无映射返回 undefined。 */
42
+ function quotaEnvelopeErrorFamily(code) {
43
+ const value = Number(code)
44
+ if (!Number.isFinite(value)) return undefined
45
+ if (QUOTA_ENVELOPE_AUTH_CODES.has(value)) return 'auth'
46
+ if (QUOTA_ENVELOPE_NO_SUBSCRIPTION_CODES.has(value)) return 'no-subscription'
47
+ if (QUOTA_ENVELOPE_CREDENTIAL_CODES.has(value)) return 'credential-rejected'
48
+ if (QUOTA_ENVELOPE_UPSTREAM_CODES.has(value)) return 'upstream-error'
49
+ return undefined
50
+ }
51
+
28
52
  function sanitizeQuotaErrorDetail(value) {
29
53
  if (typeof value !== 'string') return undefined
30
54
  const cleaned = value.replace(/[\u0000-\u001f\u007f]+/g, ' ').replace(/\s+/g, ' ').trim()
@@ -165,6 +189,7 @@ function defineQuotaAdapter(definition, metadata = {}) {
165
189
  routeIds: Object.freeze([...(metadata.routeIds ?? [])]),
166
190
  defaultBaseURL: typeof metadata.defaultBaseURL === 'string' && metadata.defaultBaseURL !== '' ? metadata.defaultBaseURL : undefined,
167
191
  usageUrl: metadata.usageUrl,
192
+ usageBasePathname: typeof metadata.usageBasePathname === 'function' ? metadata.usageBasePathname : undefined,
168
193
  }))
169
194
  return adapter
170
195
  }
@@ -200,6 +225,12 @@ function adapterEndpoints(adapter, profile) {
200
225
  ? (adapterOwnsUndeclaredRoute(metadata.routeIds, profile) ? safeBaseUrl(metadata.defaultBaseURL, metadata.hosts) : undefined)
201
226
  : safeBaseUrl(declared, metadata.hosts)
202
227
  if (base === undefined) return []
228
+ // 适配器可对「显式 baseURL」声明用量路径归一(如 opencode-go 的协议组 baseUrl 不带 /v1,
229
+ // 通用 /usage 拼接会打到上游 404 网页);钩子返回 undefined 时走通用 /usage 拼接。
230
+ if (typeof metadata.usageBasePathname === 'function') {
231
+ const derived = metadata.usageBasePathname(base)
232
+ if (derived !== undefined) return [derived.toString()]
233
+ }
203
234
  base.pathname = `${base.pathname}/usage`.replace(/\/{2,}/g, '/')
204
235
  return [base.toString()]
205
236
  }
@@ -253,7 +284,6 @@ function createEndpointAdapter(options) {
253
284
  continue
254
285
  }
255
286
  if (windows.length === 0) {
256
- const error = new Error('bad-payload')
257
287
  const sanitize = context.sanitizeErrorDetail ?? sanitizeQuotaErrorDetail
258
288
  // 业务信封(`{code!==0, msg}`)与 OpenAI 风格错误(`{error:{message}}`)两族;
259
289
  // 只认这两族,成功信封里的 `message:"OK"` 之类不当错误原因透出。
@@ -265,6 +295,12 @@ function createEndpointAdapter(options) {
265
295
  ? quotaUpstreamErrorMessage(payload.error)
266
296
  : undefined
267
297
  const detail = sanitize(envelopeMessage ?? upstreamMessage)
298
+ // 信封错误码定族:凭据类与 HTTP 401/403 同路(换候选,链尾归 credential-rejected)。
299
+ const family = payload !== null && typeof payload === 'object'
300
+ ? quotaEnvelopeErrorFamily(payload.code)
301
+ : undefined
302
+ const error = new Error(family === undefined || family === 'auth' ? 'bad-payload' : family)
303
+ if (family === 'auth') error.status = 401
268
304
  if (detail !== undefined) error.detail = detail
269
305
  parseFailure ??= annotate(error, endpoint)
270
306
  lastError = parseFailure
@@ -291,6 +327,7 @@ function createEndpointAdapter(options) {
291
327
  routeIds: options.routeIds,
292
328
  defaultBaseURL: options.defaultBaseURL,
293
329
  usageUrl: options.usageUrl,
330
+ usageBasePathname: options.usageBasePathname,
294
331
  })
295
332
  return adapter
296
333
  }
@@ -392,7 +429,10 @@ function normalizeOpenRouterCredits(payload) {
392
429
  const data = payload?.data !== null && typeof payload?.data === 'object' ? payload.data : payload
393
430
  const total = Number(data?.total_credits ?? data?.credits)
394
431
  const used = Number(data?.total_usage ?? data?.usage)
395
- if (!Number.isFinite(total) || total <= 0 || !Number.isFinite(used)) return { windows: [] }
432
+ if (!Number.isFinite(total) || !Number.isFinite(used)) return { windows: [] }
433
+ // 未充值/免费渠道 key:官方 credits 端点照常 200,但 total_credits 为 0——这不是「响应格式异常」,
434
+ // 而是余额确为零;百分比在 0/0 上无意义,改给可读余额文本(与其它余额型 provider 同一形态)。
435
+ if (total <= 0) return { windows: [{ id: 'credits', text: `$${Math.max(0, total - used).toFixed(2)}` }] }
396
436
  return { windows: [{ id: 'credits', percent: Math.max(0, Math.min(100, Math.round((used / total) * 100))) }] }
397
437
  }
398
438
 
@@ -653,6 +693,156 @@ async function fetchStepFunStepPlanUsage({ credential, signal, requestJson }) {
653
693
  return windows
654
694
  }
655
695
 
696
+ // ─── Command Code(command-goat)账号额度 ────────────────────────────────────
697
+ // 数据源是 Command Code 的账号额度面(官方 CLI `cmd /usage` 与 Studio 用量页同一组端点,
698
+ // 全部是宿主常量、无 baseURL 派生):`GET /alpha/whoami`(取 org.id,个人号 org 为 null)→
699
+ // `GET /alpha/billing/credits?orgId=`(credits 三源 + windowLimits.fiveHour/weekly 的 used/cap/resetAt)→
700
+ // `GET /alpha/billing/subscriptions?orgId=`(planId/status/currentPeriodStart/currentPeriodEnd)→
701
+ // `GET /alpha/usage/summary?orgId=&since=`(本计费周期 totalCost/totalCount/totalTokens)。
702
+ // 认证与推理同 key(Bearer <CMD_API_KEY>,`user_*` 前缀)。归一化口径与 Studio 用量页一致:
703
+ // 余额窗 = monthlyCredits + purchasedCredits + freeCredits,另出「本周期已用」与「套餐」两个文本窗;
704
+ // 窗口 used/cap 是 Credit 数(非 0..1 比例)→ 折算已用百分比,绝对数原样下发(客户端做缩写);
705
+ // resetAt 是毫秒时间戳。
706
+ const COMMAND_CODE_WHOAMI_URL = 'https://api.commandcode.ai/alpha/whoami'
707
+ const COMMAND_CODE_CREDITS_URL = 'https://api.commandcode.ai/alpha/billing/credits'
708
+ const COMMAND_CODE_SUBSCRIPTIONS_URL = 'https://api.commandcode.ai/alpha/billing/subscriptions'
709
+ const COMMAND_CODE_USAGE_URL = 'https://api.commandcode.ai/alpha/usage/summary'
710
+
711
+ /** 金额归一到两位小数字符串:负数/非有限/空值拒绝。两个「数值化陷阱」都要堵:
712
+ * `Number('')` 与 `Number(null)` 都是 0——上游缺字段会被伪装成 $0.00 余额。 */
713
+ function normalizeCommandCodeMoney(value) {
714
+ if (value === null || value === undefined) return null
715
+ if (typeof value === 'string' && value.trim() === '') return null
716
+ const amount = Number(value)
717
+ if (!Number.isFinite(amount) || amount < 0) return null
718
+ return (Math.round(amount * 100) / 100).toFixed(2)
719
+ }
720
+
721
+ /** Command Code 窗口(五小时/周)→ 已用百分比窗口。used/cap 是绝对 Credit 数,
722
+ * 折算成已用 % 并保留绝对数;cap<=0 或 used 缺失/非法时跳过该窗(不伪造 0%)。
723
+ * `unit:'usd'` 声明绝对数的量纲:Command Code 的 Credit 与美元 1:1(实测
724
+ * `monthlyCredits + fiveHour.used` 恰等于该计划总额 70),故两列都该带 `$`——
725
+ * 缺量纲时客户端按 token 数缩写渲染,`14` 会被误读成别的量。 */
726
+ function pushCommandCodeWindow(windows, id, entry) {
727
+ if (entry === null || typeof entry !== 'object') return
728
+ // `Number(null) === 0` 同款坑:缺 used/cap 时先显式挡掉,否则会造出「已用 0 / 上限 0」的假窗。
729
+ if (entry.used === null || entry.used === undefined || entry.cap === null || entry.cap === undefined) return
730
+ const used = Number(entry.used)
731
+ const cap = Number(entry.cap)
732
+ if (!Number.isFinite(used) || used < 0 || !Number.isFinite(cap) || cap <= 0) return
733
+ const resetsAt = normalizeResetTimestamp(entry.resetAt)
734
+ windows.push({
735
+ id,
736
+ kindKey: id,
737
+ percent: Math.max(0, Math.min(100, Math.round((used / cap) * 100))),
738
+ used: Math.round(used * 100) / 100,
739
+ limit: Math.round(cap * 100) / 100,
740
+ unit: 'usd',
741
+ ...(resetsAt !== undefined ? { resetsAt } : {}),
742
+ })
743
+ }
744
+
745
+ /** 四端点原始载荷 → 窗口。credits 响应自带嵌套信封 `{credits:{monthlyCredits,…},windowLimits}`、
746
+ * subscriptions 是 `{success,data:{planId,…}}`、summary 与 whoami 是平铺对象——各自按真实形状取。
747
+ * 文本窗口只放可读金额与套餐标识(宿主不拼句子,窗口名走客户端词典);
748
+ * summary 缺席时「本周期已用」整行不下发,绝不拿剩余冒充总额。 */
749
+ function normalizeCommandCodeQuota(payload) {
750
+ const windows = []
751
+ if (payload === null || typeof payload !== 'object') return { windows }
752
+ const rawCredits = payload.credits !== null && typeof payload.credits === 'object' ? payload.credits : null
753
+ const creditSources = rawCredits?.credits !== null && typeof rawCredits?.credits === 'object' ? rawCredits.credits : null
754
+ if (creditSources !== null) {
755
+ const monthly = normalizeCommandCodeMoney(creditSources.monthlyCredits)
756
+ const purchased = normalizeCommandCodeMoney(creditSources.purchasedCredits)
757
+ const free = normalizeCommandCodeMoney(creditSources.freeCredits)
758
+ if (monthly !== null || purchased !== null || free !== null) {
759
+ const remaining = Number(monthly ?? 0) + Number(purchased ?? 0) + Number(free ?? 0)
760
+ windows.push({ id: 'balance', kindKey: 'balance', text: `$${remaining.toFixed(2)}` })
761
+ }
762
+ }
763
+ const rawSummary = payload.summary !== null && typeof payload.summary === 'object' ? payload.summary : null
764
+ const spent = rawSummary === null ? null : normalizeCommandCodeMoney(rawSummary.totalCost)
765
+ if (spent !== null) windows.push({ id: 'period-spend', kindKey: 'period-spend', text: `$${spent}` })
766
+ const subscription = payload.subscription?.data !== null && typeof payload.subscription?.data === 'object'
767
+ ? payload.subscription.data
768
+ : payload.subscription !== null && typeof payload.subscription === 'object' ? payload.subscription : null
769
+ const rawPlan = typeof subscription?.planId === 'string' ? subscription.planId.trim() : ''
770
+ if (rawPlan !== '') {
771
+ const periodEnd = normalizeResetTimestamp(subscription?.currentPeriodEnd)
772
+ windows.push({
773
+ id: 'plan',
774
+ kindKey: 'plan-name',
775
+ text: rawPlan.replace(/[_-]+/g, ' ').trim().slice(0, MAX_QUOTA_PROVIDER_NAME),
776
+ ...(periodEnd !== undefined ? { resetsAt: periodEnd } : {}),
777
+ })
778
+ }
779
+ const limits = rawCredits?.windowLimits !== null && typeof rawCredits?.windowLimits === 'object' ? rawCredits.windowLimits : null
780
+ if (limits !== null) {
781
+ pushCommandCodeWindow(windows, 'five-hour', limits.fiveHour)
782
+ pushCommandCodeWindow(windows, 'weekly', limits.weekly)
783
+ }
784
+ return { windows }
785
+ }
786
+
787
+ /** Command Code 编排:whoami 取 orgId(认证失败即止,不再打后续端点)→ credits/subscriptions
788
+ * 并发取 → usage/summary 带上订阅周期起点(官方 CLI 同序,让花费口径锚在当个计费周期)。
789
+ * 个人号 org=null,orgId 为空时端点不带查询参数;401/403 一律归一 credential-rejected
790
+ * (错 key 要能就地重填)。三段额度面独立失败不拖垮整行——有任一段可用即展示,
791
+ * 全不可用才抛首个端点的稳定错误码(原话随 detail 透出)。 */
792
+ async function fetchCommandCodeQuota({ credential, signal, requestJson }) {
793
+ if (typeof requestJson !== 'function') throw new Error('transport-unavailable')
794
+ const raw = String(credential ?? '').trim()
795
+ if (raw === '') throw new Error('credential-missing')
796
+ // 发现链已按 policy.format 加了 Bearer;粘贴带回的 `Authorization: Bearer x` 整头也容错。
797
+ const token = raw.replace(/^authorization:\s*/i, '').replace(/^bearer\s+/i, '')
798
+ if (token === '') throw new Error('credential-missing')
799
+ const authorization = `Bearer ${token}`
800
+ const blocking = (error) => error?.message === 'http-status:401' || error?.message === 'http-status:403'
801
+ const get = async (endpoint) => {
802
+ try {
803
+ return { ok: true, value: await requestJson(endpoint, { authorization, signal }) }
804
+ } catch (error) {
805
+ if (blocking(error)) {
806
+ // 错 key 归凭据类(行回 unconfigured 以便就地重填),但上游原话照旧透出——
807
+ // 统一失败信封要求「为什么被拒」可读,不能只剩一个家族码。
808
+ const rejected = new Error('credential-rejected')
809
+ if (typeof error?.detail === 'string' && error.detail !== '') rejected.detail = error.detail
810
+ throw rejected
811
+ }
812
+ return { ok: false, error }
813
+ }
814
+ }
815
+ const whoami = await get(COMMAND_CODE_WHOAMI_URL)
816
+ if (whoami.ok !== true) {
817
+ // 认证面都拿不到:orgId 无处可取,后三个端点必然同样失败,直接如实报首个错误。
818
+ throw whoami.error
819
+ }
820
+ const orgId = typeof whoami.value?.org?.id === 'string' && whoami.value.org.id.trim() !== '' ? whoami.value.org.id.trim() : ''
821
+ const query = orgId === '' ? '' : `?orgId=${encodeURIComponent(orgId)}`
822
+ const [creditsResult, subscriptionResult] = await Promise.all([
823
+ get(`${COMMAND_CODE_CREDITS_URL}${query}`),
824
+ get(`${COMMAND_CODE_SUBSCRIPTIONS_URL}${query}`),
825
+ ])
826
+ const subscription = subscriptionResult.ok === true ? subscriptionResult.value : null
827
+ const periodStart = typeof subscription?.data?.currentPeriodStart === 'string' ? subscription.data.currentPeriodStart.trim() : ''
828
+ const summaryParams = periodStart === ''
829
+ ? query
830
+ : `${query === '' ? '?' : `${query}&`}since=${encodeURIComponent(periodStart)}`
831
+ const summaryResult = await get(`${COMMAND_CODE_USAGE_URL}${summaryParams}`)
832
+ const failures = [creditsResult, subscriptionResult, summaryResult].filter((result) => result.ok !== true)
833
+ const windows = normalizeCommandCodeQuota({
834
+ credits: creditsResult.ok === true ? creditsResult.value : null,
835
+ subscription,
836
+ summary: summaryResult.ok === true ? summaryResult.value : null,
837
+ }).windows
838
+ if (windows.length === 0) {
839
+ const first = failures[0]
840
+ if (first !== undefined) throw first.error
841
+ throw new Error('bad-payload:shape')
842
+ }
843
+ return windows
844
+ }
845
+
656
846
  const CLIPROXY_CODEX_USAGE_URL = 'https://chatgpt.com/backend-api/wham/usage'
657
847
  const CLIPROXY_GEMINI_QUOTA_URL = 'https://cloudcode-pa.googleapis.com/v1internal:retrieveUserQuota'
658
848
  // antigravity 额度候选(v1.4.8 修订 + 复核收敛为 daily-only):CPA 的 antigravity 流量默认全打
@@ -1058,6 +1248,17 @@ function createQuotaAdapterCatalog() {
1058
1248
  routeIds: ['opencode-go'],
1059
1249
  defaultBaseURL: 'https://opencode.ai/zen/go/v1',
1060
1250
  usageUrl: 'https://opencode.ai/',
1251
+ // 显式 baseURL 也踩同一个坑:注册表 anthropic-messages 组给的是 `…/zen/go`(不带 /v1),
1252
+ // 通用 `/usage` 拼接会打到 `…/zen/go/usage` → 上游 404 HTML 网页(实测 404 text/html;
1253
+ // 同域 `…/zen/go/v1/usage` 无 key 401 JSON、带 key 200)。只归一 zen 网关两种形状,
1254
+ // 其余自定义/反代路径返回 undefined 交回通用 `/usage` 拼接(不猜 /v1)。
1255
+ usageBasePathname(base) {
1256
+ const pathname = base.pathname.replace(/\/+$/, '')
1257
+ if (pathname.endsWith('/zen/go')) base.pathname = `${pathname}/v1/usage`
1258
+ else if (pathname.endsWith('/zen/go/v1')) base.pathname = `${pathname}/usage`
1259
+ else return undefined
1260
+ return base
1261
+ },
1061
1262
  }),
1062
1263
  createEndpointAdapter({
1063
1264
  kind: 'zai-coding-cn',
@@ -1147,6 +1348,15 @@ function createQuotaAdapterCatalog() {
1147
1348
  entryKey: 'editToken',
1148
1349
  usageUrl: 'https://platform.stepfun.com/plan-usage',
1149
1350
  }),
1351
+ createComposedAdapter({
1352
+ kind: 'command-goat',
1353
+ fetch: fetchCommandCodeQuota,
1354
+ keyHints: ['COMMAND_CODE_API_KEY', 'COMMANDCODE_API_KEY'],
1355
+ hosts: ['api.commandcode.ai', 'commandcode.ai'],
1356
+ // 查询面是固定账号面(与推理 baseURL 无关):未声明端点的同名渠道按路由 id 认领。
1357
+ routeIds: ['command-goat'],
1358
+ usageUrl: 'https://commandcode.ai/settings/usage',
1359
+ }),
1150
1360
  ])
1151
1361
  }
1152
1362
 
@@ -1184,12 +1394,14 @@ export {
1184
1394
  cliproxyProjectFor,
1185
1395
  createQuotaAdapterCatalog,
1186
1396
  fetchCliproxyUsage,
1397
+ fetchCommandCodeQuota,
1187
1398
  fetchStepFunStepPlanUsage,
1188
1399
  fetchXiaomiTokenPlanUsage,
1189
1400
  findQuotaAdapter,
1190
1401
  normalizeAntigravityModels,
1191
1402
  normalizeAntigravityQuotaSummary,
1192
1403
  normalizeCodexRateLimit,
1404
+ normalizeCommandCodeQuota,
1193
1405
  normalizeDeepseekBalance,
1194
1406
  normalizeGeminiBuckets,
1195
1407
  normalizeKimiBalance,