@dsh-plugins/dsh-llm-hub 1.3.2 → 1.4.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/CHANGELOG.md +74 -0
- package/README.md +36 -27
- package/lib/client.js +48 -83
- package/lib/index.js +54 -118
- package/package.json +19 -9
- package/types/globals.d.ts +3 -0
- package/types/index.d.ts +62 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,80 @@
|
|
|
2
2
|
|
|
3
3
|
本项目遵循 [Semantic Versioning](https://semver.org/lang/zh-CN/)。
|
|
4
4
|
|
|
5
|
+
## [1.4.0] - 2026-09-26
|
|
6
|
+
|
|
7
|
+
### 🔄 多账号 key 轮换:消除独立配置区域
|
|
8
|
+
|
|
9
|
+
**用户反馈**:1.3.0 的 `keyPool[provider]` 独立区域是反模式 —— 配置与已有 `apiKeyEnv` 字段脱节,
|
|
10
|
+
需要去一个「多账号 key 轮换」专属 UI 卡才能加第二把 key。**正确做法是让 `apiKeyEnv` 字段直接支持 string[]**——
|
|
11
|
+
DSH 在同一个 provider 字段里多填几把 env 名就完事。
|
|
12
|
+
|
|
13
|
+
#### 变更
|
|
14
|
+
|
|
15
|
+
- **新增 `apiKeyEnv` 字段支持 string[]**:`['KEY_1', 'KEY_2', 'KEY_3']` 自动 round-robin。
|
|
16
|
+
- **向后兼容**:旧 string 配置 `apiKeyEnv: 'KEY'` 完全不变。
|
|
17
|
+
- **删除** `settings.dsh-llm-hub.keyPool[provider]` 独立配置区域 —— 1.3.0 引入,1.4.0 收回。
|
|
18
|
+
- **删除** host 半 `keyPoolOf()` / `rotateKeyPool()` 函数 / `keyPoolIndex` / `keyPoolLastChosen` Map。
|
|
19
|
+
- **删除** 路由 `GET /api/dsh-llm-hub/keypool/status`。
|
|
20
|
+
- **删除** client 半 KeyPoolCard / slot `id: dsh-llm-hub-keypool` (order 73) / keyPool locale / keyPool CSS。
|
|
21
|
+
- **保留** `runtimeMarks`(401/403/402/QUOTA_EXCEEDED 失败追踪)—— 失败 key 信息
|
|
22
|
+
合并进现有可用性看板。
|
|
23
|
+
|
|
24
|
+
#### 配置示例
|
|
25
|
+
|
|
26
|
+
```yaml
|
|
27
|
+
llm-deepseek:
|
|
28
|
+
apiKeyEnv: DEEPSEEK_API_KEY # 单 key(向后兼容)
|
|
29
|
+
# 或
|
|
30
|
+
llm-deepseek:
|
|
31
|
+
apiKeyEnv: # 多 key 轮换
|
|
32
|
+
- DEEPSEEK_API_KEY_1
|
|
33
|
+
- DEEPSEEK_API_KEY_2
|
|
34
|
+
- DEEPSEEK_API_KEY_3
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
#### 测试
|
|
38
|
+
|
|
39
|
+
- **删** `test/host-keypool.test.mjs`(测的是已删的 `keyPoolOf/rotateKeyPool`)
|
|
40
|
+
- **加** `test/host-multi-key.test.mjs`(9 条 case 覆盖 `pickRotatedEnv`):
|
|
41
|
+
单 key / 多 key round-robin / 跨 provider 互不干扰 / 空数组 / undefined / null /
|
|
42
|
+
array 含空字符串 / 全空字符串 / 单元素数组
|
|
43
|
+
- 测试总数 92 → 93
|
|
44
|
+
|
|
45
|
+
#### 用户故事
|
|
46
|
+
|
|
47
|
+
| 用户 | 之前(1.3.0) | 现在(1.4.0) |
|
|
48
|
+
|---|---|---|
|
|
49
|
+
| 「给 DeepSeek 加第二把 key」 | 跳到「多账号 key 轮换」独立区 | **同一个 DeepSeek 行的 apiKeyEnv 字段加一行** |
|
|
50
|
+
| 「看现在用的是哪把 key」 | keyPool 卡看 | 健康看板(avail 卡)里 `runtimeMarks` 字段 |
|
|
51
|
+
| 「不用 keyPool」 | 一行多余配置 | 完全没这个概念 |
|
|
52
|
+
|
|
53
|
+
#### 内部
|
|
54
|
+
|
|
55
|
+
- 新增 host 函数 `pickRotatedEnv(refs, provider)` —— 输入 string | string[],返 `{ref, index, total}` 或 undefined。
|
|
56
|
+
- 索引 `keyRotationIndex: Map<provider, number>` 模块级(per-provider 独立 round-robin)。
|
|
57
|
+
- `connectionFacts` 三段解析顺序(向后兼容):
|
|
58
|
+
1. `request.apiKey` 一次性覆盖
|
|
59
|
+
2. `section.apiKeyEnv` 是 string[] → round-robin 取下一个
|
|
60
|
+
3. `section.apiKeyEnv` 是 string → 旧的单 key 路径
|
|
61
|
+
|
|
62
|
+
#### 不在 1.4.0 范围
|
|
63
|
+
|
|
64
|
+
- **失败 key 自动跳过**:1.4.0 收集真实失败场景再加;现在失败 key 信息进 `runtimeMarks` 标记,下一轮探测可看到。
|
|
65
|
+
- **(provider, model) 级轮换**:1.4.0 粒度仍是 provider 级;细化到 model 级的场景待评估(DeepSeek-chat 和 DeepSeek-reasoner 用不同 key 池的需求场景不明确)。
|
|
66
|
+
|
|
67
|
+
## [1.3.3] - 2026-09-25
|
|
68
|
+
|
|
69
|
+
### ✨ 订阅制配额可视化进度条与重置倒计时
|
|
70
|
+
|
|
71
|
+
- **📊 订阅制可视化进度条胶囊(Subscription Quota Gauge)**:
|
|
72
|
+
- 参考 `claude-usage-statusline` 经典轨道式设计,在 Provider 卡片中引入高质感平滑迷你进度条;
|
|
73
|
+
- 动态三色渐变指示:`> 50%` 充裕翡翠绿(`#10b981`)、`15%~50%` 预警金黄(`#f59e0b`)、`<= 15%` 告急珊瑚红(`#ef4444`);
|
|
74
|
+
- 同时支持主限额(如 MiniMax 5 小时滚动配额、智谱 5h 配额)与次级周限额的双重进度条并列展示;
|
|
75
|
+
- **⏱️ 重置时刻与倒计时精准指示(Reset Time Tracking)**:
|
|
76
|
+
- 在配额进度条后实时标注重置时刻(如 `(15:40 重置)`),悬浮卡片提供完整余量与周期说明;
|
|
77
|
+
- 适配器底层自动解析服务端返回的 `current_interval_end_time` / `next_reset_time` / `expire_time` 真实重置时刻。
|
|
78
|
+
|
|
5
79
|
## [1.3.2] - 2026-09-24
|
|
6
80
|
|
|
7
81
|
### 🐛 修复与健壮性提升
|
package/README.md
CHANGED
|
@@ -302,41 +302,50 @@ key 形态:`provider/model`。provider 严格(要去 settings 段查),mo
|
|
|
302
302
|
|
|
303
303
|
未配置 aliases 段时 UI 退回只显示 `model.id`,与没装本插件时一致。
|
|
304
304
|
|
|
305
|
-
## 多账号 key 轮换(1.
|
|
305
|
+
## 多账号 key 轮换(1.4.0 改设计)
|
|
306
306
|
|
|
307
|
-
|
|
308
|
-
撞 rate limit / 配额上限)。**不**自动跳过失败 key —— 用户从 debug 卡看到「上次
|
|
309
|
-
失败用的是 X」后,下次请求自然轮换过去;自动跳过失败 key 等 1.4 收集真实失败场景再加。
|
|
307
|
+
### 设计动机
|
|
310
308
|
|
|
311
|
-
|
|
309
|
+
**之前(1.3.0)**:`settings.dsh-llm-hub.keyPool[provider] = [{name, env}, ...]` 独立配置区域。
|
|
310
|
+
**1.4.0 改**:用户反馈「何必要单独做一个区域」——`apiKeyEnv` 字段直接支持 string[],
|
|
311
|
+
DSH 在同一个 provider 字段里多填几把 env 名就完事,**不需要去任何新区域**。
|
|
312
|
+
keyPool 概念 / 独立 slot / 独立 UI 卡 —— 全部删除。
|
|
313
|
+
|
|
314
|
+
### 配置形态
|
|
312
315
|
|
|
313
316
|
```yaml
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
env: DEEPSEEK_API_KEY_3
|
|
323
|
-
modelgo:
|
|
324
|
-
- name: '轮换 A'
|
|
325
|
-
env: MODELGO_KEY_1
|
|
326
|
-
- name: '轮换 B'
|
|
327
|
-
env: MODELGO_KEY_2
|
|
317
|
+
llm-deepseek:
|
|
318
|
+
apiKeyEnv: DEEPSEEK_API_KEY # 老写法:1 把 key(向后兼容)
|
|
319
|
+
# 或
|
|
320
|
+
llm-deepseek:
|
|
321
|
+
apiKeyEnv: # 新写法:多把 key 轮换
|
|
322
|
+
- DEEPSEEK_API_KEY_1
|
|
323
|
+
- DEEPSEEK_API_KEY_2
|
|
324
|
+
- DEEPSEEK_API_KEY_3
|
|
328
325
|
```
|
|
329
326
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
327
|
+
**用户操作**:在 DSH 设置 → 模型 → DeepSeek 行的「API Key Env」字段,**多填几个 env 名**,
|
|
328
|
+
DSH 自动 round-robin。其它流程(连接、探针、余额、统计)都不变。
|
|
329
|
+
|
|
330
|
+
### 行为
|
|
331
|
+
|
|
332
|
+
- **单 key**(string):行为与 1.3.0 完全一致 —— `apiKeyEnv: 'DEEPSEEK_API_KEY'`。
|
|
333
|
+
- **多 key**(string[]):host 半**按 provider 维护 round-robin 索引**,每次解析连接取下一个。
|
|
334
|
+
- **失败 key 不自动跳过** —— 下次请求自然轮换过去。失败信息进 `runtimeMarks`,
|
|
335
|
+
下次探针会标出来。**不**藏、不假装能用、不静默吞请求。
|
|
336
|
+
- **向后兼容**:所有 1.3.x 之前的配置(单 key)继续工作。
|
|
337
|
+
|
|
338
|
+
### 为什么删独立的"多账号 key 轮换"UI 卡
|
|
333
339
|
|
|
334
|
-
|
|
335
|
-
|
|
340
|
+
1.4.0 之前有一个「多账号 key 轮换」独立 card(在 footer order 73)。**反模式**:
|
|
341
|
+
- 用户故事是「我已经在 DeepSeek 那一行填过 env 名,再加几把就行」,不需要跳到任何新区域。
|
|
342
|
+
- 独立区域让用户看不到**和自己已配 provider 的关联**,新人更难看懂。
|
|
343
|
+
- UI 跟 source-of-truth(settings.yaml 的 `apiKeyEnv` 字段)分离 —— 两份真相。
|
|
336
344
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
345
|
+
删了之后:
|
|
346
|
+
- 用户在 ModelsSection 同一个 DeepSeek 行加 env 名 → 多 key 自动轮换
|
|
347
|
+
- debug 信息统一进「健康看板」卡(跟 availability / latency 一起看)
|
|
348
|
+
- 没有「我不知道去哪配置」这种认知成本
|
|
340
349
|
|
|
341
350
|
## 外部 harness 子代理(装了才出现)
|
|
342
351
|
|
package/lib/client.js
CHANGED
|
@@ -75,8 +75,6 @@ window.__ModuleLoader__.load({
|
|
|
75
75
|
const ROUTING_URL = '/api/dsh-llm-hub/routing/resolve'
|
|
76
76
|
/** 模型别名:把长 id 显示成短名(人话)。 */
|
|
77
77
|
const ALIASES_URL = '/api/dsh-llm-hub/aliases'
|
|
78
|
-
/** 多账号 key 轮换:debug 路由,看每个 provider 当前轮到哪个 key。 */
|
|
79
|
-
const KEYPOOL_URL = '/api/dsh-llm-hub/keypool/status'
|
|
80
78
|
|
|
81
79
|
const LOCALES = {
|
|
82
80
|
zh: {
|
|
@@ -158,10 +156,6 @@ window.__ModuleLoader__.load({
|
|
|
158
156
|
routingNoRecommendation: '全部不可用',
|
|
159
157
|
routingLoading: '路由解析中…',
|
|
160
158
|
aliasesMissing: '尚未配置别名',
|
|
161
|
-
keypoolTitle: '多账号 key 轮换',
|
|
162
|
-
keypoolRound: 'round-robin',
|
|
163
|
-
keypoolActiveTip: '当前优先使用',
|
|
164
|
-
keypoolNextTip: '下次轮换目标',
|
|
165
159
|
heroTitle: 'LLM 矩阵概览',
|
|
166
160
|
heroProviders: '活跃提供方',
|
|
167
161
|
heroModels: '可用模型',
|
|
@@ -273,10 +267,6 @@ window.__ModuleLoader__.load({
|
|
|
273
267
|
routingNoRecommendation: 'none available',
|
|
274
268
|
routingLoading: 'resolving routing…',
|
|
275
269
|
aliasesMissing: 'no aliases configured',
|
|
276
|
-
keypoolTitle: 'Key rotation',
|
|
277
|
-
keypoolRound: 'round-robin',
|
|
278
|
-
keypoolActiveTip: 'Currently active',
|
|
279
|
-
keypoolNextTip: 'Next target',
|
|
280
270
|
heroTitle: 'LLM Matrix Overview',
|
|
281
271
|
heroProviders: 'Active providers',
|
|
282
272
|
heroModels: 'Models',
|
|
@@ -510,22 +500,6 @@ window.__ModuleLoader__.load({
|
|
|
510
500
|
'.dsh-llm-hub-routing__name{color:var(--dsw-alias-label-primary);font-weight:600}',
|
|
511
501
|
'.dsh-llm-hub-routing__suggestion{color:var(--dsw-alias-state-warn-primary);padding-top:2px}',
|
|
512
502
|
'.dsh-llm-hub-routing__reason{color:var(--dsw-alias-state-error-primary);padding-top:2px}',
|
|
513
|
-
// 多账号 key 轮换
|
|
514
|
-
'.dsh-llm-hub-keypool{display:flex;flex-direction:column;gap:6px;margin-top:10px;',
|
|
515
|
-
'padding:8px 10px;border-radius:6px;',
|
|
516
|
-
'border:.5px solid var(--dsw-alias-border-l1);background:transparent;',
|
|
517
|
-
'font-size:11.5px;line-height:16px;color:var(--dsw-alias-label-secondary)}',
|
|
518
|
-
'.dsh-llm-hub-keypool__head{display:flex;align-items:center;justify-content:space-between;gap:8px;flex-wrap:wrap}',
|
|
519
|
-
'.dsh-llm-hub-keypool__title{color:var(--dsw-alias-label-primary);font-weight:600;display:inline-flex;align-items:center;gap:5px}',
|
|
520
|
-
'.dsh-llm-hub-keypool__rows{display:flex;flex-direction:column;gap:3px}',
|
|
521
|
-
'.dsh-llm-hub-keypool__row{display:flex;align-items:center;gap:8px;flex-wrap:wrap;padding:4px 6px;border-radius:4px;background:var(--dsw-alias-bg-layer-2,#1c1f24);border:.5px solid var(--dsw-alias-border-l1)}',
|
|
522
|
-
'.dsh-llm-hub-keypool__row.is-active{border-color:var(--dsw-alias-state-success-primary,#10b981);box-shadow:0 0 8px rgba(16,185,129,.15)}',
|
|
523
|
-
'.dsh-llm-hub-keypool__name{color:var(--dsw-alias-label-primary);font-weight:600;min-width:120px;font-family:ui-monospace,SFMono-Regular,monospace}',
|
|
524
|
-
'.dsh-llm-hub-keypool__meta{font-size:10.5px;opacity:.7}',
|
|
525
|
-
'.dsh-llm-hub-keypool__chosen{display:flex;align-items:baseline;gap:4px;',
|
|
526
|
-
'color:var(--dsw-alias-label-primary)}',
|
|
527
|
-
'.dsh-llm-hub-keypool__chosen small{color:var(--dsw-alias-label-secondary);opacity:.75;font-size:10.5px}',
|
|
528
|
-
'.dsh-llm-hub-keypool__pending{color:var(--dsw-alias-label-secondary);opacity:.6}',
|
|
529
503
|
// Hero 全景统计大横条
|
|
530
504
|
'.dsh-llm-hub-hero{display:grid;grid-template-columns:repeat(auto-fit,minmax(120px,1fr));gap:6px;margin:12px 0 6px 0}',
|
|
531
505
|
'.dsh-llm-hub-hero__card{display:flex;flex-direction:column;gap:2px;padding:8px 10px;border-radius:6px;border:.5px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-2,rgba(255,255,255,.02));backdrop-filter:blur(6px)}',
|
|
@@ -547,6 +521,18 @@ window.__ModuleLoader__.load({
|
|
|
547
521
|
'.dsh-llm-hub-latency-badge.is-fast{background:rgba(16,185,129,.08);color:var(--dsw-alias-state-success-primary,#10b981);border:.5px solid rgba(16,185,129,.2)}',
|
|
548
522
|
'.dsh-llm-hub-latency-badge.is-normal{background:rgba(234,179,8,.08);color:var(--dsw-alias-state-warn-primary,#eab308);border:.5px solid rgba(234,179,8,.2)}',
|
|
549
523
|
'.dsh-llm-hub-latency-badge.is-slow{background:rgba(239,68,68,.08);color:var(--dsw-alias-state-error-primary,#ef4444);border:.5px solid rgba(239,68,68,.2)}',
|
|
524
|
+
// 订阅制配额可视化进度条与重置倒计时(参考 claude-usage-statusline 规范)
|
|
525
|
+
'.dsh-llm-hub-quota-group{display:inline-flex;align-items:center;gap:6px;flex-wrap:wrap;vertical-align:middle}',
|
|
526
|
+
'.dsh-llm-hub-quota-bar{display:inline-flex;align-items:center;gap:4.5px;padding:1.5px 6px;border-radius:4px;background:var(--dsw-alias-bg-layer-2,rgba(255,255,255,.03));border:.5px solid var(--dsw-alias-border-l1,rgba(255,255,255,.08));font-size:10.5px;line-height:1;transition:all .15s ease}',
|
|
527
|
+
'.dsh-llm-hub-quota-bar:hover{border-color:var(--dsw-alias-brand-primary,rgba(59,130,246,.4));background:var(--dsw-alias-bg-layer-3,rgba(255,255,255,.06))}',
|
|
528
|
+
'.dsh-llm-hub-quota-label{font-weight:600;color:var(--dsw-alias-label-primary);font-size:10px}',
|
|
529
|
+
'.dsh-llm-hub-quota-track{width:46px;height:4.5px;border-radius:2.5px;background:rgba(255,255,255,.12);overflow:hidden;position:relative;display:inline-block;vertical-align:middle}',
|
|
530
|
+
'.dsh-llm-hub-quota-fill{height:100%;border-radius:2.5px;display:block;transition:width .3s cubic-bezier(0.4,0,0.2,1)}',
|
|
531
|
+
'.dsh-llm-hub-quota-fill.is-high{background:linear-gradient(90deg,#10b981,#34d399)}',
|
|
532
|
+
'.dsh-llm-hub-quota-fill.is-mid{background:linear-gradient(90deg,#f59e0b,#fbbf24)}',
|
|
533
|
+
'.dsh-llm-hub-quota-fill.is-low{background:linear-gradient(90deg,#ef4444,#f87171)}',
|
|
534
|
+
'.dsh-llm-hub-quota-pct{font-variant-numeric:tabular-nums;font-weight:600;color:var(--dsw-alias-label-primary);font-size:10.5px}',
|
|
535
|
+
'.dsh-llm-hub-quota-time{font-size:9.5px;color:var(--dsw-alias-label-secondary);opacity:.8;font-variant-numeric:tabular-nums;display:inline-flex;align-items:center;margin-left:1px}',
|
|
550
536
|
// Webkubor 插件全家桶互导矩阵
|
|
551
537
|
'.dsh-suite-dock{margin-top:14px;padding:14px 16px;background:var(--dsw-alias-bg-layer-2,rgba(255,255,255,0.03));border:1px solid var(--dsw-alias-border-l1,rgba(255,255,255,0.08));border-radius:10px;backdrop-filter:blur(10px)}',
|
|
552
538
|
'.dsh-suite-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:10px;flex-wrap:wrap;gap:8px}',
|
|
@@ -861,14 +847,44 @@ window.__ModuleLoader__.load({
|
|
|
861
847
|
return `${t('balance')} ${symbol}${first.amount}${voucher}`
|
|
862
848
|
}
|
|
863
849
|
const meaning = balance.meaning === 'used' ? t('used') : t('remain')
|
|
864
|
-
const
|
|
850
|
+
const isUsed = balance.meaning === 'used'
|
|
851
|
+
const bars = []
|
|
865
852
|
for (const item of items.slice(0, 2)) {
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
853
|
+
const remainPct = isUsed ? Math.max(0, 100 - (item.percent ?? 0)) : (item.percent ?? 0)
|
|
854
|
+
const fillPct = Math.min(Math.max(remainPct, 0), 100)
|
|
855
|
+
const levelClass = fillPct > 50 ? 'is-high' : (fillPct > 20 ? 'is-mid' : 'is-low')
|
|
856
|
+
const labelText = item.label || '5h'
|
|
857
|
+
const resetTimeNote = item.resetsAt ? `(${item.resetsAt} 重置)` : ''
|
|
858
|
+
bars.push(h('span', {
|
|
859
|
+
key: labelText,
|
|
860
|
+
className: 'dsh-llm-hub-quota-bar',
|
|
861
|
+
title: `${labelText} ${meaning} ${item.percent}%${resetTimeNote ? ` · ${resetTimeNote}` : ''}`
|
|
862
|
+
},
|
|
863
|
+
h('span', { className: 'dsh-llm-hub-quota-label' }, labelText),
|
|
864
|
+
h('span', { className: 'dsh-llm-hub-quota-track' },
|
|
865
|
+
h('span', { className: `dsh-llm-hub-quota-fill ${levelClass}`, style: { width: `${fillPct}%` } })
|
|
866
|
+
),
|
|
867
|
+
h('span', { className: 'dsh-llm-hub-quota-pct' }, `${item.percent}%`),
|
|
868
|
+
resetTimeNote ? h('span', { className: 'dsh-llm-hub-quota-time' }, resetTimeNote) : null
|
|
869
|
+
))
|
|
870
|
+
if (typeof item.weeklyPercent === 'number') {
|
|
871
|
+
const weeklyRemain = isUsed ? Math.max(0, 100 - item.weeklyPercent) : item.weeklyPercent
|
|
872
|
+
const weeklyFill = Math.min(Math.max(weeklyRemain, 0), 100)
|
|
873
|
+
const weeklyClass = weeklyFill > 50 ? 'is-high' : (weeklyFill > 20 ? 'is-mid' : 'is-low')
|
|
874
|
+
bars.push(h('span', {
|
|
875
|
+
key: 'week',
|
|
876
|
+
className: 'dsh-llm-hub-quota-bar',
|
|
877
|
+
title: `${t('week')} ${meaning} ${item.weeklyPercent}%`
|
|
878
|
+
},
|
|
879
|
+
h('span', { className: 'dsh-llm-hub-quota-label' }, t('week')),
|
|
880
|
+
h('span', { className: 'dsh-llm-hub-quota-track' },
|
|
881
|
+
h('span', { className: `dsh-llm-hub-quota-fill ${weeklyClass}`, style: { width: `${weeklyFill}%` } })
|
|
882
|
+
),
|
|
883
|
+
h('span', { className: 'dsh-llm-hub-quota-pct' }, `${item.weeklyPercent}%`)
|
|
884
|
+
))
|
|
885
|
+
}
|
|
869
886
|
}
|
|
870
|
-
|
|
871
|
-
return `${t('planQuota')} ${level}${parts.join(' / ')}`
|
|
887
|
+
return h('span', { className: 'dsh-llm-hub-quota-group' }, bars)
|
|
872
888
|
}
|
|
873
889
|
|
|
874
890
|
/**
|
|
@@ -1378,50 +1394,6 @@ window.__ModuleLoader__.load({
|
|
|
1378
1394
|
}
|
|
1379
1395
|
|
|
1380
1396
|
/**
|
|
1381
|
-
* 多账号 key 轮换卡:展示当前轮换池状态与活跃 Key
|
|
1382
|
-
*/
|
|
1383
|
-
function KeyPoolCard(props) {
|
|
1384
|
-
const t = typeof props.t === 'function' ? props.t : fallbackT
|
|
1385
|
-
const [report, setReport] = React.useState(null)
|
|
1386
|
-
React.useEffect(() => {
|
|
1387
|
-
let alive = true
|
|
1388
|
-
fetch(KEYPOOL_URL, { headers: { accept: 'application/json' } })
|
|
1389
|
-
.then((res) => res.json())
|
|
1390
|
-
.then((body) => { if (alive) setReport(body !== null && typeof body === 'object' ? body : null) })
|
|
1391
|
-
.catch(() => { if (alive) setReport(null) })
|
|
1392
|
-
return () => { alive = false }
|
|
1393
|
-
}, [])
|
|
1394
|
-
if (report === null) return null
|
|
1395
|
-
const providers = Array.isArray(report.providers) ? report.providers : []
|
|
1396
|
-
if (providers.length === 0) return null
|
|
1397
|
-
ensureStyle()
|
|
1398
|
-
const formatTime = (ms) => {
|
|
1399
|
-
if (typeof ms !== 'number' || ms <= 0) return '—'
|
|
1400
|
-
const d = new Date(ms)
|
|
1401
|
-
return `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}:${String(d.getSeconds()).padStart(2, '0')}`
|
|
1402
|
-
}
|
|
1403
|
-
const rows = providers.map((entry) => {
|
|
1404
|
-
const chosen = entry.lastChosen
|
|
1405
|
-
const isActive = chosen !== null
|
|
1406
|
-
return h('div', {
|
|
1407
|
-
key: entry.provider,
|
|
1408
|
-
className: 'dsh-llm-hub-keypool__row' + (isActive ? ' is-active' : '')
|
|
1409
|
-
},
|
|
1410
|
-
h('span', { className: 'dsh-llm-hub-health-dot ' + (isActive ? 'is-ok' : '') }),
|
|
1411
|
-
h('span', { className: 'dsh-llm-hub-keypool__name' }, entry.provider),
|
|
1412
|
-
h('span', { className: 'dsh-llm-hub-keypool__meta' }, `${entry.total} keys`),
|
|
1413
|
-
chosen === null
|
|
1414
|
-
? h('span', { className: 'dsh-llm-hub-keypool__pending' }, '—')
|
|
1415
|
-
: h('span', { className: 'dsh-llm-hub-keypool__chosen' },
|
|
1416
|
-
h('span', null, chosen.name),
|
|
1417
|
-
h('small', null, ` #${chosen.index + 1} / ${entry.total} · ${formatTime(chosen.at)}`)))
|
|
1418
|
-
})
|
|
1419
|
-
return h('div', { className: 'dsh-llm-hub-keypool' },
|
|
1420
|
-
h('div', { className: 'dsh-llm-hub-keypool__head' },
|
|
1421
|
-
h('span', { className: 'dsh-llm-hub-keypool__title' }, '🔄 ' + t('keypoolTitle')),
|
|
1422
|
-
h('span', { className: 'dsh-llm-hub-subtext' }, t('keypoolRound'))),
|
|
1423
|
-
h('div', { className: 'dsh-llm-hub-keypool__rows' }, rows))
|
|
1424
|
-
}
|
|
1425
1397
|
|
|
1426
1398
|
/**
|
|
1427
1399
|
* 当前路由卡 —— 显示用户当前激活的 (provider, model) + 状态 + 推荐的 fallback。
|
|
@@ -1951,13 +1923,6 @@ window.__ModuleLoader__.load({
|
|
|
1951
1923
|
locale: NS,
|
|
1952
1924
|
inject: () => ({ t, availability })
|
|
1953
1925
|
}, HealthCard))
|
|
1954
|
-
ctx.slots.inject('settings.models.footer', () => ctx.slots.register({
|
|
1955
|
-
name: 'settings.models.footer',
|
|
1956
|
-
id: 'dsh-llm-hub-keypool',
|
|
1957
|
-
order: 73,
|
|
1958
|
-
locale: NS,
|
|
1959
|
-
inject: () => ({ t })
|
|
1960
|
-
}, KeyPoolCard))
|
|
1961
1926
|
ctx.slots.inject('settings.models.footer', () => ctx.slots.register({
|
|
1962
1927
|
name: 'settings.models.footer',
|
|
1963
1928
|
id: 'dsh-llm-hub-routing',
|
package/lib/index.js
CHANGED
|
@@ -180,22 +180,27 @@ const ROUTING_RESOLVE_PATH = '/api/dsh-llm-hub/routing/resolve'
|
|
|
180
180
|
/** 模型别名:客户端渲染 model id 时显示短名(典型场景:deepseek-reasoner → 推理)。 */
|
|
181
181
|
const ALIASES_PATH = '/api/dsh-llm-hub/aliases'
|
|
182
182
|
/**
|
|
183
|
-
*
|
|
183
|
+
* 1.4.0 多 Key 轮换纯函数:支持 string(单 key)或 string[](多 key round-robin)。
|
|
184
184
|
*
|
|
185
|
-
*
|
|
186
|
-
*
|
|
187
|
-
*
|
|
188
|
-
*
|
|
189
|
-
* session 持久化(重启会从 0 开始)。失败的 key 不会自动跳过 —— 用户从
|
|
190
|
-
* debug 路由看到「上次失败的是 X」,下次请求自然轮换过去。这是 1.x 的轻量方案,
|
|
191
|
-
* 真要自动跳过失败 key 等 1.1 收集了真实失败场景再加。
|
|
192
|
-
* - **不覆盖显式 key**:request.apiKey 是一次性覆盖(发现草稿用的),绕过
|
|
193
|
-
* 轮换直接用它;轮换索引不动。
|
|
194
|
-
* - **provider 范围**:DeepSeek 官方直连用 NS = 'llm-deepseek' 作为 key,pi-ai
|
|
195
|
-
* provider 用 provider 自己的 id(modelgo / minimax ...);keyPool 段里
|
|
196
|
-
* 直接用 provider id 作 key。
|
|
185
|
+
* @param {string|string[]|undefined} refs - 单 Key 或多 Key 环境变量名数组
|
|
186
|
+
* @param {string} provider - Provider 标识(用于维护独立索引)
|
|
187
|
+
* @param {Map<string, number>} [stateMap] - 轮换游标存储
|
|
188
|
+
* @returns {{ ref: string, index: number, total: number } | undefined}
|
|
197
189
|
*/
|
|
198
|
-
|
|
190
|
+
export function pickRotatedEnv(refs, provider, stateMap) {
|
|
191
|
+
if (typeof refs === 'string' && refs.length > 0) {
|
|
192
|
+
return { ref: refs, index: 0, total: 1 }
|
|
193
|
+
}
|
|
194
|
+
if (Array.isArray(refs)) {
|
|
195
|
+
const list = refs.filter((r) => typeof r === 'string' && r.trim().length > 0)
|
|
196
|
+
if (list.length === 0) return undefined
|
|
197
|
+
const current = stateMap ? (stateMap.get(provider) ?? 0) : 0
|
|
198
|
+
const index = current % list.length
|
|
199
|
+
if (stateMap) stateMap.set(provider, (current + 1) % list.length)
|
|
200
|
+
return { ref: list[index], index, total: list.length }
|
|
201
|
+
}
|
|
202
|
+
return undefined
|
|
203
|
+
}
|
|
199
204
|
/** 路由配置从 settings 哪一段读 —— 顶层 dsh-llm-hub 段下,与 warning 同级。 */
|
|
200
205
|
const ROUTING_KEY = 'routing'
|
|
201
206
|
/** 探针结论的保鲜期;过期后下次读取顺手在后台重探,不阻塞下拉。 */
|
|
@@ -522,56 +527,23 @@ export function apply(ctx) {
|
|
|
522
527
|
/**
|
|
523
528
|
* 读 `settings.dsh-llm-hub.keyPool[provider]` 段。
|
|
524
529
|
*
|
|
525
|
-
* 每条 entry 是 `{ name, env }` —— name 是人话(debug 用),env 是
|
|
526
|
-
* 凭据服务引用的环境变量名;后者用 `process.env[ref]` 直接读,**不**走
|
|
527
|
-
* credentials 服务,因为这些 key 多半不会进凭据库(轮换场景通常是
|
|
528
|
-
* 测试 / 临时 key,手贴环境变量更简单)。
|
|
529
|
-
*
|
|
530
|
-
* @returns 规范化后的 entry 数组(缺 name 或 env 的过滤掉);没配或非数组返 `undefined`。
|
|
531
|
-
*/
|
|
532
|
-
const keyPoolOf = (provider) => {
|
|
533
|
-
const section = sectionOf() ?? {}
|
|
534
|
-
const raw = section.keyPool
|
|
535
|
-
if (raw === null || typeof raw !== 'object') return undefined
|
|
536
|
-
const entries = raw[provider]
|
|
537
|
-
if (!Array.isArray(entries)) return undefined
|
|
538
|
-
const out = []
|
|
539
|
-
for (const entry of entries) {
|
|
540
|
-
if (entry === null || typeof entry !== 'object') continue
|
|
541
|
-
const name = typeof entry.name === 'string' && entry.name.trim().length > 0 ? entry.name.trim() : undefined
|
|
542
|
-
const env = typeof entry.env === 'string' && entry.env.trim().length > 0 ? entry.env.trim() : undefined
|
|
543
|
-
if (env === undefined) continue
|
|
544
|
-
out.push({ name: name ?? env, env })
|
|
545
|
-
}
|
|
546
|
-
return out.length > 0 ? out : undefined
|
|
547
|
-
}
|
|
548
|
-
|
|
549
530
|
/**
|
|
550
|
-
*
|
|
551
|
-
* @
|
|
531
|
+
* 多 Key 自动轮换索引 —— per provider 维护独立的自增游标。
|
|
532
|
+
* @type {Map<string, number>}
|
|
552
533
|
*/
|
|
553
|
-
const
|
|
554
|
-
const entries = keyPoolOf(provider)
|
|
555
|
-
if (entries === undefined) return undefined
|
|
556
|
-
const current = keyPoolIndex.current.get(provider) ?? 0
|
|
557
|
-
const index = current % entries.length
|
|
558
|
-
keyPoolIndex.current.set(provider, (current + 1) % entries.length)
|
|
559
|
-
const entry = entries[index]
|
|
560
|
-
keyPoolLastChosen.current.set(provider, { entry, index, at: Date.now() })
|
|
561
|
-
return { entry, index }
|
|
562
|
-
}
|
|
534
|
+
const keyRotationIndex = new Map()
|
|
563
535
|
|
|
564
536
|
/**
|
|
565
537
|
* 解析本次调用要用的连接事实。
|
|
566
538
|
*
|
|
567
539
|
* 解析顺序(向后兼容):
|
|
568
540
|
* 1) request.apiKey 一次性覆盖 —— 跳过轮换、原样返回(不污染索引)
|
|
569
|
-
* 2)
|
|
570
|
-
*
|
|
541
|
+
* 2) section.apiKeyEnv 是 string[] → 按 provider 轮换取下一个 ref,
|
|
542
|
+
* 再走 credentials 服务或 process.env 解析密钥
|
|
543
|
+
* 3) section.apiKeyEnv 是 string → 原单 key 路径(行为完全不变)
|
|
571
544
|
*
|
|
572
|
-
* 返 ref 不变 ——
|
|
573
|
-
*
|
|
574
|
-
* 看到当前用的是哪个 env。
|
|
545
|
+
* 返 ref 不变 —— 客户端拿到的是凭据引用;下游诊断(探针 / 余额路由)
|
|
546
|
+
* 能继续看到当前用的是哪个 env。
|
|
575
547
|
* @param request - 可选的一次性覆盖(发现草稿会带 baseURL / apiKey / provider)。
|
|
576
548
|
* @returns baseURL、凭据引用与可用密钥。
|
|
577
549
|
*/
|
|
@@ -590,14 +562,10 @@ export function apply(ctx) {
|
|
|
590
562
|
return { baseURL, ref, apiKey: explicit }
|
|
591
563
|
}
|
|
592
564
|
const providerKey = label(request.provider) ?? DEEPSEEK_PROVIDER
|
|
593
|
-
const
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
// 配了 keyPool 但 env var 没值 —— 退回旧单 key 路径,不让 keyPool
|
|
598
|
-
// 静默吞掉请求。
|
|
599
|
-
}
|
|
600
|
-
const ref = label(request.apiKeyEnv, section.apiKeyEnv) ?? DEFAULT_API_KEY_ENV
|
|
565
|
+
const picked = pickRotatedEnv(section.apiKeyEnv, providerKey, keyRotationIndex)
|
|
566
|
+
const ref = picked === undefined
|
|
567
|
+
? (label(request.apiKeyEnv, section.apiKeyEnv) ?? DEFAULT_API_KEY_ENV)
|
|
568
|
+
: picked.ref
|
|
601
569
|
let apiKey
|
|
602
570
|
const credentials = ctx.get('credentials')
|
|
603
571
|
if (credentials !== undefined) {
|
|
@@ -923,10 +891,21 @@ export function apply(ctx) {
|
|
|
923
891
|
if (row === null || typeof row !== 'object') continue
|
|
924
892
|
const percent = percentage(row.current_interval_remaining_percent)
|
|
925
893
|
if (percent === undefined) continue
|
|
894
|
+
let resetsAt = undefined
|
|
895
|
+
const rawTime = row.current_interval_end_time ?? row.end_time ?? row.reset_time
|
|
896
|
+
if (typeof rawTime === 'number' && rawTime > 0) {
|
|
897
|
+
const date = new Date(rawTime > 1e11 ? rawTime : rawTime * 1000)
|
|
898
|
+
if (!isNaN(date.getTime())) {
|
|
899
|
+
resetsAt = `${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`
|
|
900
|
+
}
|
|
901
|
+
} else if (typeof rawTime === 'string' && rawTime.trim() !== '') {
|
|
902
|
+
resetsAt = rawTime.trim()
|
|
903
|
+
}
|
|
926
904
|
items.push({
|
|
927
905
|
label: label(row.model_name) ?? 'plan',
|
|
928
906
|
percent,
|
|
929
|
-
weeklyPercent: percentage(row.current_weekly_remaining_percent)
|
|
907
|
+
weeklyPercent: percentage(row.current_weekly_remaining_percent),
|
|
908
|
+
resetsAt
|
|
930
909
|
})
|
|
931
910
|
}
|
|
932
911
|
return { items }
|
|
@@ -953,7 +932,17 @@ export function apply(ctx) {
|
|
|
953
932
|
if (tier === undefined) continue
|
|
954
933
|
const percent = percentage(lim.percentage)
|
|
955
934
|
if (percent === undefined) continue
|
|
956
|
-
|
|
935
|
+
let resetsAt = undefined
|
|
936
|
+
const rawTime = lim.reset_time ?? lim.next_reset_time ?? body?.data?.expire_time
|
|
937
|
+
if (typeof rawTime === 'number' && rawTime > 0) {
|
|
938
|
+
const date = new Date(rawTime > 1e11 ? rawTime : rawTime * 1000)
|
|
939
|
+
if (!isNaN(date.getTime())) {
|
|
940
|
+
resetsAt = `${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`
|
|
941
|
+
}
|
|
942
|
+
} else if (typeof rawTime === 'string' && rawTime.trim() !== '') {
|
|
943
|
+
resetsAt = rawTime.trim()
|
|
944
|
+
}
|
|
945
|
+
items.push({ label: tier, percent, resetsAt })
|
|
957
946
|
}
|
|
958
947
|
return { level: label(body?.data?.level), items }
|
|
959
948
|
}
|
|
@@ -1841,10 +1830,6 @@ export function apply(ctx) {
|
|
|
1841
1830
|
// 对照官方价目表,CSV 导出(`GET /api/dsh-llm-hub/usage/export`)已经做了。
|
|
1842
1831
|
const usageTable = { current: null }
|
|
1843
1832
|
const usageReady = { current: false }
|
|
1844
|
-
// 多账号 key 轮换:每个 provider 一个 round-robin 索引。重启后归零,跨 session
|
|
1845
|
-
// 不持久化 —— 失败的 key 不会自动跳过,下次自然轮换过去(看 debug 路由查清)。
|
|
1846
|
-
const keyPoolIndex = { current: new Map() }
|
|
1847
|
-
const keyPoolLastChosen = { current: new Map() }
|
|
1848
1833
|
const recordUsage = (entry) => {
|
|
1849
1834
|
const table = usageTable.current
|
|
1850
1835
|
if (table === null) return
|
|
@@ -2371,55 +2356,6 @@ export function apply(ctx) {
|
|
|
2371
2356
|
}
|
|
2372
2357
|
}), 'dsh-llm-hub: aliases route')
|
|
2373
2358
|
|
|
2374
|
-
/**
|
|
2375
|
-
* KeyPool 调试路由 —— 让 client 看到每个 provider 当前轮到了哪个 entry。
|
|
2376
|
-
* 不暴露 apiKey 实际值(只有 name + env 名);401 / 403 类失败时用户从
|
|
2377
|
-
* 这里看「轮到哪个 env 上次失败」,下次请求轮换过去。
|
|
2378
|
-
*/
|
|
2379
|
-
const keyPoolDebugPayload = () => {
|
|
2380
|
-
const section = sectionOf() ?? {}
|
|
2381
|
-
const raw = section.keyPool
|
|
2382
|
-
const pool = raw !== null && typeof raw === 'object' ? raw : {}
|
|
2383
|
-
const providers = []
|
|
2384
|
-
for (const [provider, entries] of Object.entries(pool)) {
|
|
2385
|
-
if (!Array.isArray(entries)) continue
|
|
2386
|
-
const lastChosen = keyPoolLastChosen.current.get(provider)
|
|
2387
|
-
providers.push({
|
|
2388
|
-
provider,
|
|
2389
|
-
total: entries.length,
|
|
2390
|
-
lastChosen: lastChosen === undefined ? null : {
|
|
2391
|
-
name: lastChosen.entry.name,
|
|
2392
|
-
env: lastChosen.entry.env,
|
|
2393
|
-
index: lastChosen.index,
|
|
2394
|
-
at: lastChosen.at
|
|
2395
|
-
},
|
|
2396
|
-
nextIndex: keyPoolIndex.current.get(provider) ?? 0
|
|
2397
|
-
})
|
|
2398
|
-
}
|
|
2399
|
-
return { ok: true, providers }
|
|
2400
|
-
}
|
|
2401
|
-
|
|
2402
|
-
ctx.effect(() => ctx.webServer.register({
|
|
2403
|
-
kind: 'exact',
|
|
2404
|
-
path: KEYPOOL_STATUS_PATH,
|
|
2405
|
-
handler: (request, response) => {
|
|
2406
|
-
if (request.method !== 'GET' && request.method !== 'HEAD') {
|
|
2407
|
-
response.writeHead(405, { allow: 'GET, HEAD' })
|
|
2408
|
-
response.end()
|
|
2409
|
-
return
|
|
2410
|
-
}
|
|
2411
|
-
if (!sameOrigin(request)) {
|
|
2412
|
-
sendJson(response, 403, { ok: false, error: 'cross-origin reads are refused' })
|
|
2413
|
-
return
|
|
2414
|
-
}
|
|
2415
|
-
if (request.method === 'HEAD') {
|
|
2416
|
-
response.writeHead(200, { 'content-type': 'application/json; charset=utf-8' })
|
|
2417
|
-
response.end()
|
|
2418
|
-
return
|
|
2419
|
-
}
|
|
2420
|
-
sendJson(response, 200, keyPoolDebugPayload())
|
|
2421
|
-
}
|
|
2422
|
-
}), 'dsh-llm-hub: keypool status route')
|
|
2423
2359
|
|
|
2424
2360
|
// ── harness 子代理 ────────────────────────────────────────────────────────
|
|
2425
2361
|
|
package/package.json
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dsh-plugins/dsh-llm-hub",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "DSH 模型管理层:网关可达性探测、模型目录拉取、DeepSeek + MiniMax/智谱/Moonshot/StepFun 余额与配额、本月用量统计、余额预警、Provider 健康看板,再把本机装好的 codex / claude / agy 注册成子代理(装了才出现)。零运行时依赖,不改动 DSH 安装。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
|
+
"types": "./types/index.d.ts",
|
|
7
8
|
"exports": {
|
|
8
9
|
".": {
|
|
10
|
+
"types": "./types/index.d.ts",
|
|
9
11
|
"default": "./lib/index.js"
|
|
10
12
|
},
|
|
11
13
|
"./client": {
|
|
@@ -26,7 +28,7 @@
|
|
|
26
28
|
}
|
|
27
29
|
},
|
|
28
30
|
"scripts": {
|
|
29
|
-
"deploy": "rsync -av --delete lib/ ~/.dsh/profiles/web/node_modules/@dsh-plugins/dsh-llm-hub/lib/ && rsync -av package.json cordis.patch.yml README.md README.en.md LICENSE ~/.dsh/profiles/web/node_modules/@dsh-plugins/dsh-llm-hub/",
|
|
31
|
+
"deploy": "rsync -av --delete lib/ ~/.dsh/profiles/web/node_modules/@dsh-plugins/dsh-llm-hub/lib/ && rsync -av types package.json cordis.patch.yml README.md README.en.md LICENSE ~/.dsh/profiles/web/node_modules/@dsh-plugins/dsh-llm-hub/",
|
|
30
32
|
"preview": "npm run deploy && open http://127.0.0.1:3080",
|
|
31
33
|
"check": "for f in lib/*.js; do node --check \"$f\" || exit 1; done",
|
|
32
34
|
"test": "node --test"
|
|
@@ -38,14 +40,21 @@
|
|
|
38
40
|
"deepseek",
|
|
39
41
|
"model-discovery",
|
|
40
42
|
"balance",
|
|
41
|
-
"
|
|
42
|
-
"
|
|
43
|
-
"
|
|
43
|
+
"quota",
|
|
44
|
+
"rate-limit",
|
|
45
|
+
"usage",
|
|
46
|
+
"tokens",
|
|
47
|
+
"cost",
|
|
48
|
+
"dashboard",
|
|
49
|
+
"routing",
|
|
50
|
+
"failover",
|
|
51
|
+
"key-rotation",
|
|
52
|
+
"api-keys",
|
|
53
|
+
"harness",
|
|
44
54
|
"subagent",
|
|
45
55
|
"codex",
|
|
46
|
-
"claude
|
|
47
|
-
"
|
|
48
|
-
"harness"
|
|
56
|
+
"claude",
|
|
57
|
+
"agy"
|
|
49
58
|
],
|
|
50
59
|
"license": "MIT",
|
|
51
60
|
"repository": {
|
|
@@ -65,6 +74,7 @@
|
|
|
65
74
|
"README.en.md",
|
|
66
75
|
"README.md",
|
|
67
76
|
"cordis.patch.yml",
|
|
68
|
-
"lib"
|
|
77
|
+
"lib",
|
|
78
|
+
"types"
|
|
69
79
|
]
|
|
70
80
|
}
|
package/types/index.d.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @dsh-plugins/dsh-llm-hub TypeScript Definitions
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export const name: 'dsh-llm-hub'
|
|
6
|
+
export const inject: string[]
|
|
7
|
+
|
|
8
|
+
export const MAX_RESPONSE_BYTES: number
|
|
9
|
+
export const AVAILABILITY_TTL_MS: number
|
|
10
|
+
export const STUCK_PROBE_MS: number
|
|
11
|
+
export const REQUEST_TIMEOUT_MS: number
|
|
12
|
+
|
|
13
|
+
export interface UsageRecord {
|
|
14
|
+
id: string
|
|
15
|
+
provider: string
|
|
16
|
+
model: string
|
|
17
|
+
month: string
|
|
18
|
+
tokensIn: number
|
|
19
|
+
tokensOut: number
|
|
20
|
+
tokensTotal: number
|
|
21
|
+
costUsd: number
|
|
22
|
+
calls: number
|
|
23
|
+
updatedAt: number
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const UsageRecordSchema: {
|
|
27
|
+
readonly id: 'string'
|
|
28
|
+
readonly provider: 'string'
|
|
29
|
+
readonly model: 'string'
|
|
30
|
+
readonly month: 'string'
|
|
31
|
+
readonly tokensIn: 'number'
|
|
32
|
+
readonly tokensOut: 'number'
|
|
33
|
+
readonly tokensTotal: 'number'
|
|
34
|
+
readonly costUsd: 'number'
|
|
35
|
+
readonly calls: 'number'
|
|
36
|
+
readonly updatedAt: 'number'
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function normalizeUsageForTest(usage: any): {
|
|
40
|
+
tokensIn: number
|
|
41
|
+
tokensOut: number
|
|
42
|
+
tokensTotal: number
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function recordUsageInto(table: any, entry: any): void
|
|
46
|
+
export function readBounded(response: any, url: string): Promise<string>
|
|
47
|
+
export function maskKey(key: string | null | undefined): string
|
|
48
|
+
|
|
49
|
+
export interface RotatedEnvResult {
|
|
50
|
+
ref: string
|
|
51
|
+
index: number
|
|
52
|
+
total: number
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function pickRotatedEnv(
|
|
56
|
+
refs: string | string[] | undefined,
|
|
57
|
+
provider: string,
|
|
58
|
+
stateMap?: Map<string, number>
|
|
59
|
+
): RotatedEnvResult | undefined
|
|
60
|
+
|
|
61
|
+
export function apply(ctx: any): void
|
|
62
|
+
|