sintbot 1.4.16 → 1.4.18

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.
@@ -24,6 +24,9 @@ const { codexCatalogWindows, windowForProvider } = require('../agent/modelwindow
24
24
  const claudestatus = require('../agent/claudestatus') // Claude 订阅登录态(卡片「检测登录」· doctor 同源)
25
25
  const claudemodels = require('../agent/claudemodels') // Claude 模型候选(仅登录后给)
26
26
  const claudeusage = require('../agent/claudeusage') // Claude 订阅额度(oauth/usage,cc-hud 同源;v0.11.3)
27
+ const { collectEnvConflicts } = require('../agent/envconflicts') // 模型卡「环境冲突」(B5):守护进程 env / settings.json / config.toml,只回键名
28
+ // 探不出来给空数组 —— 这行告警绝不能拖垮登录检测本身。
29
+ const envWarningsFor = (runner, provider) => { try { return collectEnvConflicts({ runner, provider }) } catch { return [] } }
27
30
  const codexAgent = require('../agent/codexcode') // codex --version 探针(概览健康,仅配了 codex provider 时)
28
31
  const { globalPlugins, codexPlugins, projectPlugins, mcpReachability } = require('../plugins')
29
32
  const gitrepo = require('../gitrepo') // 群项目 ↔ git 远端关联(「仓库地址」:init + origin,幂等)+ ls-remote 连通性探测
@@ -734,7 +737,7 @@ function createUiServer({ configPath, getLiveState = null, getConnHealth = null,
734
737
 
735
738
  leftovers: await leftoversState(),
736
739
 
737
- codexStatus: codexStatusCache ? { login: codexStatusCache.login, usage: codexStatusCache.usage } : null, // 前端加载回填,codex 卡片刷新不丢
740
+ codexStatus: codexStatusCache ? { login: codexStatusCache.login, usage: codexStatusCache.usage, envWarningsByProvider: codexStatusCache.envWarningsByProvider || {} } : null, // 前端加载回填,codex 卡片刷新不丢
738
741
  claudeStatus: claudeStatusCache ? claudeStatusCache.login : null, // 同上,Claude 订阅登录态
739
742
  // 服务器时区/时间(顶栏时钟):tz/offsetMin = bot 工作时区(cfg.timezone,默认北京)——
740
743
  // 群里/prompt 时间戳用的就是这个口径,运维看到的钟要和群里一致;hostTz 是主机系统时区
@@ -1033,7 +1036,7 @@ function createUiServer({ configPath, getLiveState = null, getConnHealth = null,
1033
1036
  codexModelsCache = { at: Date.now(), val }
1034
1037
  return sendJSON(res, 200, val)
1035
1038
  }
1036
- if (p === '/api/usage' && req.method === 'GET') { const cfg = loadConfig(configPath); return sendJSON(res, 200, await getUsage(cfg.providers)) }
1039
+ if (p === '/api/usage' && req.method === 'GET') { const cfg = loadConfig(configPath); return sendJSON(res, 200, await getUsage(cfg.providers, { bin: cfg.agent.bin })) }
1037
1040
  // 模型卡「额度」行的 ↻:只刷这一个 provider 且绕过两层缓存(见 usage.js refreshUsage)。
1038
1041
  // 分工 = 整页 /api/usage 走 5 分钟缓存,手点才现拉;codex 走自家 /api/codex/verify 的 force。
1039
1042
  if (p === '/api/usage/refresh' && req.method === 'POST') {
@@ -1041,7 +1044,7 @@ function createUiServer({ configPath, getLiveState = null, getConnHealth = null,
1041
1044
  const cfg = loadConfig(configPath)
1042
1045
  const prov = cfg.providers && cfg.providers[name]
1043
1046
  if (!prov) return sendJSON(res, 400, { error: `未知模型 '${name}'` })
1044
- return sendJSON(res, 200, { name, usage: await refreshUsage(name, prov) })
1047
+ return sendJSON(res, 200, { name, usage: await refreshUsage(name, prov, { bin: cfg.agent.bin }) })
1045
1048
  }
1046
1049
 
1047
1050
  if (p === '/api/provider-key' && req.method === 'POST') {
@@ -2473,10 +2476,12 @@ function createUiServer({ configPath, getLiveState = null, getConnHealth = null,
2473
2476
  // 前端不显示额度行,不拖垮登录检测本身。
2474
2477
  let usage = null
2475
2478
  if (login && login.ok && login.loggedIn) {
2476
- try { if (force) claudeusage.clearCache(); usage = await claudeusage.usageLine() } catch { usage = null }
2479
+ try { if (force) claudeusage.clearCache(); usage = await claudeusage.usageLine({ bin: cfg.agent.bin }) } catch { usage = null }
2477
2480
  }
2478
- claudeStatusCache = { at: Date.now(), login: { ...login, usage } }
2479
- return sendJSON(res, 200, { ...login, usage })
2481
+ // 订阅卡 = 无 baseUrl、无 key 的直连形态;环境里的 ANTHROPIC_API_KEY 对它是合法鉴权,核心据此不报。
2482
+ const envWarnings = envWarningsFor('claude', { baseUrl: null, hasApiKey: false })
2483
+ claudeStatusCache = { at: Date.now(), login: { ...login, usage, envWarnings } }
2484
+ return sendJSON(res, 200, { ...login, usage, envWarnings })
2480
2485
  }
2481
2486
  // Claude 模型下拉的候选。**未登录不给任何型号**(产品决定:没登录就别列)。
2482
2487
  // 已登录时 = 内置候选 + 本机账号的额外可选项;为什么不是官方实时清单,见 claudemodels.js 头部。
@@ -2498,8 +2503,9 @@ function createUiServer({ configPath, getLiveState = null, getConnHealth = null,
2498
2503
  // 会把 usage 字段洗掉,而 /api/claude/verify 的 60s 去抖随后会把这份缺 usage 的缓存原样
2499
2504
  // 吐回去 —— 表现就是卡片上的额度行凭空消失,而登录态一切正常,没人查得出为什么。
2500
2505
  if (login && login.ok) {
2501
- const keep = claudeStatusCache && claudeStatusCache.login && claudeStatusCache.login.usage
2502
- claudeStatusCache = { at: Date.now(), login: keep ? { ...login, usage: keep } : login }
2506
+ const prev = (claudeStatusCache && claudeStatusCache.login) || {}
2507
+ const keep = prev.usage
2508
+ claudeStatusCache = { at: Date.now(), login: { ...login, ...(keep ? { usage: keep } : {}), ...(prev.envWarnings ? { envWarnings: prev.envWarnings } : {}) } }
2503
2509
  }
2504
2510
  return sendJSON(res, 200, { ...claudemodels.listModels({ loggedIn: !!(login && login.ok && login.loggedIn) }), login })
2505
2511
  }
@@ -2516,11 +2522,14 @@ function createUiServer({ configPath, getLiveState = null, getConnHealth = null,
2516
2522
  const probe = await codexstatus.cachedAuthProbe('codex')
2517
2523
  const file = codexstatus.readAuthFile()
2518
2524
  const d = codexstatus.describeLogin(probe, file)
2525
+ // B5:每张 codex 卡各算一份(baseUrl / key 不同,核心的判据不同);OPENAI_* / config.toml 的 model_provider,只有键名
2526
+ const envWarningsByProvider = Object.fromEntries(Object.entries(loadConfig(configPath).providers || {})
2527
+ .filter(([, x]) => x && x.runner === 'codex').map(([n, x]) => [n, envWarningsFor('codex', x)]))
2519
2528
  const login = { ok: true, verified: !!probe.verified, loggedIn: !!(probe.ok || (!probe.verified && file.exists && !file.expired)), dead: !!probe.dead, mode: file.mode || 'chatgpt', level: d.level, text: d.text }
2520
2529
  let usage = probe.usage || null
2521
2530
  if (!usage && !probe.dead && codexStatusCache && codexStatusCache.usage) usage = codexStatusCache.usage // 探不到时保住旧额度(同前端那条「同一份状态两个消费者」的教训)
2522
- codexStatusCache = { at: Date.now(), login, usage } // cached into /api/state so the card survives a page reload
2523
- return sendJSON(res, 200, { login, usage })
2531
+ codexStatusCache = { at: Date.now(), login, usage, envWarningsByProvider } // cached into /api/state so the card survives a page reload
2532
+ return sendJSON(res, 200, { login, usage, envWarningsByProvider })
2524
2533
  }
2525
2534
  if (p === '/api/playground/run' && req.method === 'POST') {
2526
2535
  const { provider: provName, workdir, prompt, sessionId } = await body(req)
package/src/usage.js CHANGED
@@ -1,19 +1,44 @@
1
+ 'use strict'
2
+
1
3
  // Per-provider account balance / plan usage — the numbers cc-hud surfaces, for
2
4
  // the console. Server-side (keys never reach the browser), best-effort, cached
3
- // 5 min, 2.5s timeout. Endpoints mirror cc-hud (deepseek / glm / minimax); other
4
- // providers (volcano coding-plan, Claude) have no public balance API → skipped.
5
+ // 5 min. The host only moves bytes: which endpoint, which headers, how to render.
6
+ // **Every response body is judged by the binary core** (`usage_parse`, ABI 36):
7
+ // HTTP status + body → windows / balance / one of five error kinds. The host never
8
+ // re-reads a provider's JSON shape, so one rule set serves the console, /hud and
9
+ // the quota alert alike.
10
+ //
11
+ // Common rules (2026-11-12, cc-switch takeaways B1):
12
+ // · 15 s per request · real HTTP status goes to the core (network error ⇒ null)
13
+ // · `credentials` ⇒ the card shows「凭据失效」instead of going blank
14
+ // · `transient` ⇒ keep the last good value of that provider for 10 min
15
+ // · `malformed` / `none` ⇒ null (nothing to show)
16
+ // · never log a response body or a key; the last-good cache is keyed by a key hash
17
+
18
+ const crypto = require('node:crypto')
19
+ const { usageParse } = require('./agent/privatecore')
5
20
 
6
21
  const CACHE = new Map() // provider name -> { val, ts }
7
22
  const TTL = 5 * 60 * 1000
23
+ const REQUEST_TIMEOUT_MS = 15_000
24
+ const LAST_GOOD_TTL_MS = 10 * 60 * 1000
25
+ const LAST_GOOD = new Map() // kind|baseUrl|sha256(key)[:16] -> { val, ts }
8
26
 
9
- async function fetchJson(url, key, ms = 2500) {
27
+ const CREDENTIALS_VALUE = Object.freeze({ label: '额度', value: '凭据失效' })
28
+
29
+ // One request → { status, body }. Network error / timeout ⇒ status null (the core
30
+ // files it under `transient`). Non-JSON bodies travel as text so the core can still
31
+ // say `malformed`. The body is handed to the core and dropped — never logged.
32
+ async function request(url, headers, { fetchImpl = globalThis.fetch, timeoutMs = REQUEST_TIMEOUT_MS } = {}) {
10
33
  const ac = new AbortController()
11
- const timer = setTimeout(() => ac.abort(), ms)
34
+ const timer = setTimeout(() => ac.abort(), timeoutMs)
12
35
  try {
13
- const r = await fetch(url, { headers: { Authorization: `Bearer ${key}` }, signal: ac.signal })
14
- if (!r.ok) return null
15
- return await r.json()
16
- } catch { return null } finally { clearTimeout(timer) }
36
+ const r = await fetchImpl(url, { headers, signal: ac.signal })
37
+ const text = await r.text()
38
+ let body = text
39
+ try { body = JSON.parse(text) } catch { /* 非 JSON:原文交给核心判 malformed */ }
40
+ return { status: r.status, body }
41
+ } catch { return { status: null, body: null } } finally { clearTimeout(timer) }
17
42
  }
18
43
 
19
44
  // baseUrl → 哪一家的余额接口(null = 这家根本没有公开余额接口,如火山 coding plan)。
@@ -25,40 +50,107 @@ const balanceKind = (base = '') =>
25
50
  : base.includes('minimax') ? 'minimax'
26
51
  : null
27
52
 
28
- async function providerUsage(provider) {
53
+ // 核心的 windows[] → 与 Claude 订阅额度同一种一行:`5h:42% │ 7d:10% (5d)`。
54
+ // 复用 claudeusage.formatClaudeUsage,窗口名映射:session=5h · weekly=7d · weekly_scoped=模型名。
55
+ function formatWindows(windows, now = Date.now()) {
56
+ const { formatClaudeUsage } = require('./agent/claudeusage')
57
+ const limits = (Array.isArray(windows) ? windows : []).map((w) => ({
58
+ kind: w.kind === 'weekly' ? 'weekly_all' : w.kind,
59
+ percent: w.percentUsed,
60
+ resetsAt: w.resetsAt,
61
+ scopeName: w.scopeName || '',
62
+ }))
63
+ return formatClaudeUsage(limits, now)
64
+ }
65
+
66
+ const CURRENCY_SIGN = { CNY: '¥', RMB: '¥', USD: '$' }
67
+ function formatBalance(balance) {
68
+ if (!balance || typeof balance.amount !== 'number' || !Number.isFinite(balance.amount)) return null
69
+ const sign = CURRENCY_SIGN[String(balance.currency || '').toUpperCase()]
70
+ return sign ? `${sign}${balance.amount.toFixed(2)}` : `${balance.amount.toFixed(2)} ${balance.currency || ''}`.trim()
71
+ }
72
+
73
+ // 核心的成功结果 → 卡片一行;窗口优先(额度),否则余额;两者皆无 ⇒ null。
74
+ function render(parsed, now) {
75
+ const windows = formatWindows(parsed.windows, now)
76
+ if (windows) return { label: '额度', value: windows }
77
+ const balance = formatBalance(parsed.balance)
78
+ if (balance) return { label: '余额', value: balance }
79
+ return null
80
+ }
81
+
82
+ // 每家的取数顺序。返回核心的 usage_parse 结果(成功或失败),不在这里解释。
83
+ async function probeGlm(base, key, io) {
84
+ const host = base.includes('api.z.ai') ? 'https://api.z.ai' : 'https://open.bigmodel.cn'
85
+ const now = io.now()
86
+ // coding plan 配额(5 小时 / 周窗口)。鉴权头不带 Bearer(智谱监控接口的口径)。
87
+ const quota = await request(`${host}/api/monitor/usage/quota/limit`,
88
+ { Authorization: key, 'Accept-Language': 'en-US,en' }, io)
89
+ const q = usageParse('glm', 'quota', quota.status, quota.body, now)
90
+ if (q.ok || (q.error !== 'noplan' && q.error !== 'credentials' && q.error !== 'transient')) return q
91
+ // 没有 coding plan ⇒ 按量付费账户,退回余额;配额接口拒绝/抖动时也问一次余额 —— 两个接口
92
+ // 鉴权口径未必相同,只有**两边都**说凭据不对才显示「凭据失效」;任一边抖动按瞬时算(保上次好值)。
93
+ const bal = await request(`${host}/api/biz/account/query-customer-account-report`, { Authorization: `Bearer ${key}` }, io)
94
+ const b = usageParse('glm', 'balance', bal.status, bal.body, io.now())
95
+ if (b.ok) return b
96
+ if (q.error === 'credentials' && b.error === 'credentials') return q
97
+ if (q.error === 'transient' || b.error === 'transient') return { ok: false, error: 'transient', detail: 'glm quota/balance transient' }
98
+ return b.error === 'credentials' ? { ok: false, error: 'malformed', detail: 'glm balance credentials without quota agreement' } : b
99
+ }
100
+
101
+ async function probeMinimax(base, key, io) {
102
+ const host = base.includes('minimaxi.com') ? 'https://api.minimaxi.com' : 'https://api.minimax.io'
103
+ const headers = { Authorization: `Bearer ${key}` }
104
+ const plan = await request(`${host}/v1/api/openplatform/coding_plan/remains`, headers, io)
105
+ const p = usageParse('minimax', 'quota', plan.status, plan.body, io.now())
106
+ if (p.ok || (p.error !== 'malformed' && p.error !== 'none')) return p
107
+ // 旧接口(Token Plan):形状相同,只在新接口认不得时退回
108
+ const legacy = await request(`${host}/v1/token_plan/remains`, headers, io)
109
+ return usageParse('minimax', 'quota', legacy.status, legacy.body, io.now())
110
+ }
111
+
112
+ async function probeDeepseek(_base, key, io) {
113
+ const r = await request('https://api.deepseek.com/user/balance', { Authorization: `Bearer ${key}` }, io)
114
+ return usageParse('deepseek', 'balance', r.status, r.body, io.now())
115
+ }
116
+
117
+ const PROBES = { glm: probeGlm, minimax: probeMinimax, deepseek: probeDeepseek }
118
+
119
+ const keyHash = (key) => crypto.createHash('sha256').update(String(key)).digest('hex').slice(0, 16)
120
+
121
+ // 单个 provider → { label, value } | null。`io`:bin(cfg.agent.bin,登录态 claude 的兜底要用)
122
+ // 与可测注入(fetchImpl / now / timeoutMs)。
123
+ async function providerUsage(provider, io = {}) {
29
124
  const key = provider.apiKey
30
125
  const base = provider.baseUrl || ''
31
126
  // Claude 订阅登录形态(无 baseUrl、非 codex):额度走 oauth/usage(cc-hud 同源通道,
32
- // v0.11.3;凭据取本机 Claude Code 登录态,拿不到/未登录 ⇒ null 不显示)。
127
+ // v0.11.3;凭据取本机 Claude Code 登录态,拿不到/未登录 ⇒ 走 claudeusage 的兜底,仍可能 null)。
33
128
  if (!base && provider.runner !== 'codex') {
34
- const v = await require('./agent/claudeusage').usageLine()
129
+ const v = await require('./agent/claudeusage').usageLine(io.bin ? { bin: io.bin } : {})
35
130
  return v ? { label: '订阅额度', value: v } : null
36
131
  }
37
132
  if (!key) return null
38
133
 
39
134
  const kind = balanceKind(base)
40
- if (kind === 'deepseek') {
41
- const d = await fetchJson('https://api.deepseek.com/user/balance', key)
42
- const t = d && d.balance_infos && d.balance_infos[0] && d.balance_infos[0].total_balance
43
- return t ? { label: '余额', value: `¥${t}` } : null
44
- }
45
- if (kind === 'glm') {
46
- const host = base.includes('api.z.ai') ? 'https://api.z.ai' : 'https://open.bigmodel.cn'
47
- const d = await fetchJson(`${host}/api/biz/account/query-customer-account-report`, key)
48
- if (d && d.code && d.code !== 200) return null
49
- const inner = (d && d.data) || d || {}
50
- const v = inner.balance != null ? inner.balance : inner.total_balance != null ? inner.total_balance : inner.amount
51
- if (v == null) return null
52
- return { label: '余额', value: typeof v === 'number' ? `¥${v.toFixed(2)}` : `¥${String(v).replace(/^¥/, '')}` }
135
+ const probe = PROBES[kind]
136
+ if (!probe) return null // volcano / claude: no public balance API
137
+ const now = typeof io.now === 'function' ? io.now : Date.now
138
+ const ctx = { fetchImpl: io.fetchImpl || globalThis.fetch, timeoutMs: io.timeoutMs || REQUEST_TIMEOUT_MS, now }
139
+ const goodKey = `${kind}|${base}|${keyHash(key)}`
140
+
141
+ const parsed = await probe(base, key, ctx)
142
+ if (parsed.ok) {
143
+ const v = render(parsed, now())
144
+ if (v) LAST_GOOD.set(goodKey, { val: v, ts: now() })
145
+ return v
53
146
  }
54
- if (kind === 'minimax') {
55
- const host = base.includes('minimaxi.com') ? 'https://api.minimaxi.com' : 'https://api.minimax.io'
56
- const d = await fetchJson(`${host}/v1/token_plan/remains`, key)
57
- const m = d && d.model_remains && (d.model_remains.find((x) => x.current_interval_status === 1) || d.model_remains[0])
58
- if (!m) return null
59
- return { label: '额度', value: `5h:${Math.min(100, Math.max(0, Math.round(100 - m.current_interval_remaining_percent)))}%` } // 口径统一为「已用」(v0.11.8);接口给的是剩余 ⇒ 换算
147
+ if (parsed.error === 'credentials') { LAST_GOOD.delete(goodKey); return { ...CREDENTIALS_VALUE } }
148
+ if (parsed.error === 'transient') {
149
+ const last = LAST_GOOD.get(goodKey)
150
+ if (last && now() - last.ts < LAST_GOOD_TTL_MS) return last.val
151
+ LAST_GOOD.delete(goodKey)
60
152
  }
61
- return null // volcano / claude: no public balance API
153
+ return null // malformed / none / stale transient
62
154
  }
63
155
 
64
156
  // 能不能给这个 provider 查额度 —— getUsage 与 refreshUsage 共用的单一判据(两处各写一份
@@ -83,13 +175,13 @@ const hasUsageApi = (p) => !!p && p.runner !== 'codex' && !(p.baseUrl && !p.apiK
83
175
  && (isClaudeLogin(p) || !!balanceKind(p.baseUrl || ''))
84
176
 
85
177
  // Fetch balances for all keyed providers (resolved apiKey). Returns { name: {label,value} }.
86
- async function getUsage(providers = {}) {
178
+ async function getUsage(providers = {}, { bin } = {}) {
87
179
  const out = {}
88
180
  await Promise.all(Object.entries(providers).map(async ([name, p]) => {
89
181
  if (!queryable(p)) return
90
182
  const c = CACHE.get(name)
91
183
  if (c && Date.now() - c.ts < TTL) { out[name] = c.val; return }
92
- const v = await providerUsage(p)
184
+ const v = await providerUsage(p, bin ? { bin } : {})
93
185
  if (v) { CACHE.set(name, { val: v, ts: Date.now() }); out[name] = v }
94
186
  else if (c) out[name] = c.val // stale fallback
95
187
  }))
@@ -100,12 +192,12 @@ async function getUsage(providers = {}) {
100
192
  // 本模块的 5 分钟 CACHE,以及登录态 claude 那条通道自己的 60s TTL(与 /api/claude/verify 同口径)。
101
193
  // ⚠ 拉不到时把上一次的值留在缓存里 —— 手点刷新失败不该让卡片上原本显示着的额度行凭空消失
102
194
  // (旧 ts 保留 ⇒ 它照样算过期,下一次 getUsage 仍会重拉)。probe/now 注入是为了可单测。
103
- async function refreshUsage(name, provider, { probe = providerUsage, now = Date.now } = {}) {
195
+ async function refreshUsage(name, provider, { probe = providerUsage, now = Date.now, bin } = {}) {
104
196
  if (!queryable(provider)) { CACHE.delete(name); return null }
105
197
  const prev = CACHE.get(name)
106
198
  CACHE.delete(name)
107
199
  if (isClaudeLogin(provider)) { try { require('./agent/claudeusage').clearCache() } catch { /* best-effort */ } }
108
- const v = await probe(provider).catch(() => null)
200
+ const v = await probe(provider, bin ? { bin } : {}).catch(() => null)
109
201
  if (v) CACHE.set(name, { val: v, ts: now() })
110
202
  else if (prev) CACHE.set(name, prev)
111
203
  return v
@@ -121,7 +213,8 @@ async function refreshUsage(name, provider, { probe = providerUsage, now = Date.
121
213
  // 而这两句话给群成员的下一步完全相反(一个别再问,一个过会儿再问)。
122
214
  // 缓存:**成功压 60s,失败只压 10s**(与 claudeusage 同口径)—— 失败也压一分钟,等于一次
123
215
  // 偶发超时让整整一分钟查不到;完全不压又让连点 /hud 直捅端点限流。
124
- function createUsageProbe({ codexUsage = async () => null, probe = providerUsage, ttl = 60_000, failTtl = 10_000, now = Date.now } = {}) {
216
+ // `bin` = cfg.agent.bin:登录态 claude 的额度兜底要起它(见 claudeusage),不传就是 PATH 上的 claude。
217
+ function createUsageProbe({ codexUsage = async () => null, probe = providerUsage, ttl = 60_000, failTtl = 10_000, now = Date.now, bin } = {}) {
125
218
  const cache = new Map() // provider name + '|' + baseUrl -> { v, ts }
126
219
  return async (name, provider) => {
127
220
  const isCodex = !!provider && provider.runner === 'codex'
@@ -134,11 +227,11 @@ function createUsageProbe({ codexUsage = async () => null, probe = providerUsage
134
227
  let v = null
135
228
  try {
136
229
  if (isCodex) v = await codexUsage(provider)
137
- else { const u = await probe(provider); v = u ? `${u.label} ${u.value}` : null }
230
+ else { const u = await probe(provider, bin ? { bin } : {}); v = u ? `${u.label} ${u.value}` : null }
138
231
  } catch { v = null }
139
232
  cache.set(key, { v: v || null, ts: now() })
140
233
  return { supported: true, value: v || null }
141
234
  }
142
235
  }
143
236
 
144
- module.exports = { getUsage, providerUsage, refreshUsage, queryable, hasUsageApi, createUsageProbe }
237
+ module.exports = { getUsage, providerUsage, refreshUsage, queryable, hasUsageApi, createUsageProbe, REQUEST_TIMEOUT_MS, LAST_GOOD_TTL_MS }