dsh-router-core 0.3.9 → 0.3.11
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/README.md +1 -1
- package/docs/suppliers.md +5 -1
- package/lib/client-registry.js +62 -8
- package/lib/client.js +62 -8
- package/lib/index.js +207 -18
- package/lib/suppliers/nvidia.js +6 -1
- package/lib/suppliers/opencode.js +6 -1
- package/lib/suppliers/openrouter.js +12 -2
- package/lib/types/data-dir.d.ts +11 -0
- package/lib/types/llm/adapter.d.ts +9 -5
- package/lib/types/router/index.d.ts +15 -0
- package/lib/types/router/types.d.ts +5 -1
- package/lib/types/shared.d.ts +2 -0
- package/lib/types/supplier-config.d.ts +6 -0
- package/lib/types/suppliers/contract.d.ts +26 -1
- package/package.json +1 -1
- package/src/client/RouterView.tsx +2 -2
- package/src/client/StatsTab.tsx +4 -2
- package/src/client/SupplierDetail.tsx +57 -4
- package/src/client/router.css +14 -0
- package/src/data-dir.test.ts +54 -0
- package/src/data-dir.ts +46 -0
- package/src/index.ts +53 -9
- package/src/llm/adapter.test.ts +34 -0
- package/src/llm/adapter.ts +34 -1
- package/src/router/index.ts +34 -2
- package/src/router/models-cache.test.ts +39 -1
- package/src/router/precise-dispatch.test.ts +2 -2
- package/src/router/types.ts +5 -1
- package/src/router/usage.test.ts +2 -2
- package/src/shared.ts +2 -0
- package/src/supplier-config.test.ts +15 -0
- package/src/supplier-config.ts +37 -1
- package/src/suppliers/contract.ts +27 -1
- package/src/suppliers/loader.test.ts +66 -1
- package/src/suppliers/loader.ts +29 -4
- package/src/suppliers/nvidia/plugin.ts +6 -1
- package/src/suppliers/opencode/plugin.ts +6 -1
- package/src/suppliers/openrouter/plugin.ts +11 -1
|
@@ -35,7 +35,7 @@ function supplier(id: string, models: ModelInfo[], delayMs = 0) {
|
|
|
35
35
|
models.map((m) => ({ ...m, enabled: true })),
|
|
36
36
|
getAlias: (): string => id,
|
|
37
37
|
accounts: (): SupplierAccountNow[] => accts,
|
|
38
|
-
chatOnce: async (uid: string, req: { rawBody: string }): Promise<ChatOnceResult> => {
|
|
38
|
+
chatOnce: async (uid: string, _lv: string, req: { rawBody: string }): Promise<ChatOnceResult> => {
|
|
39
39
|
const m = (JSON.parse(req.rawBody) as { model?: string }).model ?? ''
|
|
40
40
|
if (!models.some((mm) => mm.id === m)) {
|
|
41
41
|
return { ok: false, state: 'no_such_model', message: `unknown model ${m}` }
|
|
@@ -671,3 +671,41 @@ test('流式:剥掉上游多发一次的空白 tool_call name/id', async () =>
|
|
|
671
671
|
assert.equal(callId, 'chatcmpl-tool-a7ee5a0d3c361c7f', 'callId 也不能被空串冲掉')
|
|
672
672
|
assert.deepEqual(JSON.parse(args), { command: 'echo hello' }, 'arguments 正常累积')
|
|
673
673
|
})
|
|
674
|
+
|
|
675
|
+
/**
|
|
676
|
+
* 组合上下文窗口 —— dsh 的自动压缩靠它算阈值(默认窗口用到 80% 触发)。
|
|
677
|
+
* adapter 报不出 contextWindow 时,dsh-compaction-basic 直接抛错并**静默
|
|
678
|
+
* 关闭自动压缩**,上下文于是一路涨到模型硬上限才炸。
|
|
679
|
+
*/
|
|
680
|
+
test('组合窗口:取组合内模型的最小值,单位 K → token', async () => {
|
|
681
|
+
const router = new Router('')
|
|
682
|
+
addSupplier(router, supplier('a', [
|
|
683
|
+
{ id: 'a-1', context_length: 128 }, // 128K
|
|
684
|
+
{ id: 'a-2', context_length: 1000 }, // 1M
|
|
685
|
+
]).s)
|
|
686
|
+
await router.modelsOf('a') // 填缓存
|
|
687
|
+
const w = router.comboContextWindow({ id: 'x', name: 'x', strategy: 'fallback', models: ['a,a-1', 'a,a-2'] })
|
|
688
|
+
assert.equal(w, 128_000, '取最小的那个(fallback 到任意一条分支都得成立)')
|
|
689
|
+
})
|
|
690
|
+
|
|
691
|
+
test('组合窗口:供应商还没缓存过 → undefined(不猜,绝不打上游)', () => {
|
|
692
|
+
const router = new Router('')
|
|
693
|
+
addSupplier(router, supplier('a', [{ id: 'a-1', context_length: 128 }]).s)
|
|
694
|
+
// 故意不调 modelsOf:模拟冷启动
|
|
695
|
+
assert.equal(
|
|
696
|
+
router.comboContextWindow({ id: 'x', name: 'x', strategy: 'fallback', models: ['a,a-1'] }),
|
|
697
|
+
undefined,
|
|
698
|
+
'没缓存就报 undefined —— 压缩这轮跳过,下次就有,比撒谎安全',
|
|
699
|
+
)
|
|
700
|
+
})
|
|
701
|
+
|
|
702
|
+
test('组合窗口:模型都没报 context_length → undefined', async () => {
|
|
703
|
+
const router = new Router('')
|
|
704
|
+
addSupplier(router, supplier('a', [{ id: 'a-1' }]).s)
|
|
705
|
+
await router.modelsOf('a')
|
|
706
|
+
assert.equal(
|
|
707
|
+
router.comboContextWindow({ id: 'x', name: 'x', strategy: 'fallback', models: ['a,a-1'] }),
|
|
708
|
+
undefined,
|
|
709
|
+
'一家都没报就别编一个数(traework/nvidia 目前就是如此)',
|
|
710
|
+
)
|
|
711
|
+
})
|
|
@@ -27,7 +27,7 @@ interface Spy {
|
|
|
27
27
|
modelsWithEnabled: () => Promise<Array<ModelInfo & { enabled: boolean }>>
|
|
28
28
|
getAlias: () => string
|
|
29
29
|
accounts: () => SupplierAccountNow[]
|
|
30
|
-
chatOnce: (uid: string, req: { rawBody: string }) => Promise<ChatOnceResult>
|
|
30
|
+
chatOnce: (uid: string, lv: string, req: { rawBody: string }) => Promise<ChatOnceResult>
|
|
31
31
|
dispose: () => void
|
|
32
32
|
}
|
|
33
33
|
}
|
|
@@ -46,7 +46,7 @@ function spy(id: string, modelId: string, alias = id): Spy {
|
|
|
46
46
|
modelsWithEnabled: async () => [{ id: modelId, enabled: true }],
|
|
47
47
|
getAlias: () => alias,
|
|
48
48
|
accounts: () => [{ uid: `${id}-u1`, credits: 0, state: 'ok' }],
|
|
49
|
-
chatOnce: async (_uid, req) => {
|
|
49
|
+
chatOnce: async (_uid, _lv, req) => {
|
|
50
50
|
calls.n += 1
|
|
51
51
|
const m = (JSON.parse(req.rawBody) as { model?: string }).model ?? ''
|
|
52
52
|
seen.push(m) // 记核心传来的**原样**全名,剥前缀是插件内部的事
|
package/src/router/types.ts
CHANGED
|
@@ -55,6 +55,8 @@ export interface ChatRequest {
|
|
|
55
55
|
rawBody: string
|
|
56
56
|
stream: boolean
|
|
57
57
|
model: string
|
|
58
|
+
/** 推理等级(reasoning effort),供应商各自决定怎么映射到上游。缺省 = 'auto'。 */
|
|
59
|
+
lv?: string
|
|
58
60
|
}
|
|
59
61
|
|
|
60
62
|
/**
|
|
@@ -68,6 +70,8 @@ export interface Supplier {
|
|
|
68
70
|
readonly priority: number
|
|
69
71
|
/** 图标(URL 或 SVG data URI),面板供应商卡片显示。可选。 */
|
|
70
72
|
readonly icon?: string
|
|
73
|
+
/** 「添加 API key」弹窗提示文案(缺省前端用通用文案)。可选。 */
|
|
74
|
+
readonly apiKeyHint?: string
|
|
71
75
|
status(): SupplierStatus
|
|
72
76
|
listModels(force?: boolean): Promise<ModelInfo[]> | ModelInfo[]
|
|
73
77
|
/** 面板展示的模型(含启用/自定义标记)。 */
|
|
@@ -79,7 +83,7 @@ export interface Supplier {
|
|
|
79
83
|
/** 账号「现在状态」(从 status() 派生,插件只报它观察到的)。空 = 无账号供应商。 */
|
|
80
84
|
accounts(): SupplierAccountNow[]
|
|
81
85
|
/** 对单个账号调一次上游。不遍历账号、不管冷却、不写 res。 */
|
|
82
|
-
chatOnce(uid: string, req: ChatRequest): Promise<ChatOnceResult>
|
|
86
|
+
chatOnce(uid: string, lv: string, req: ChatRequest): Promise<ChatOnceResult>
|
|
83
87
|
/** 删除链接(清理凭证;内部能力,js 契约不要求实现)。 */
|
|
84
88
|
removeLink?(uid: string): Promise<boolean>
|
|
85
89
|
dispose(): void
|
package/src/router/usage.test.ts
CHANGED
|
@@ -489,7 +489,7 @@ interface Spy {
|
|
|
489
489
|
modelsWithEnabled: () => Promise<Array<ModelInfo & { enabled: boolean }>>
|
|
490
490
|
getAlias: () => string
|
|
491
491
|
accounts: () => SupplierAccountNow[]
|
|
492
|
-
chatOnce: (uid: string, req: { rawBody: string }) => Promise<ChatOnceResult>
|
|
492
|
+
chatOnce: (uid: string, lv: string, req: { rawBody: string }) => Promise<ChatOnceResult>
|
|
493
493
|
dispose: () => void
|
|
494
494
|
}
|
|
495
495
|
}
|
|
@@ -506,7 +506,7 @@ function spy(id: string, modelId: string, reply: ChatOnceResult): Spy {
|
|
|
506
506
|
modelsWithEnabled: async () => [{ id: modelId, enabled: true }],
|
|
507
507
|
getAlias: () => id,
|
|
508
508
|
accounts: () => [{ uid: `${id}-u1`, credits: 0, state: 'ok' }],
|
|
509
|
-
chatOnce: async (_uid, req) => {
|
|
509
|
+
chatOnce: async (_uid, _lv, req) => {
|
|
510
510
|
calls.n += 1
|
|
511
511
|
let m = ''
|
|
512
512
|
try {
|
package/src/shared.ts
CHANGED
|
@@ -70,6 +70,8 @@ export interface RouterHealthResponse {
|
|
|
70
70
|
name: string
|
|
71
71
|
/** 图标(URL 或 SVG data URI)。 */
|
|
72
72
|
icon?: string
|
|
73
|
+
/** 「添加 API key」弹窗提示文案(缺省前端用通用文案)。 */
|
|
74
|
+
apiKeyHint?: string
|
|
73
75
|
/** 面板能力集合(登录/签到/连接池/模型管理等,按存在性)。 */
|
|
74
76
|
capabilities?: string[]
|
|
75
77
|
/** 来源:内置 / 用户目录 / 外部插件。 */
|
|
@@ -96,3 +96,18 @@ test('按供应商隔离:同 uid 不同供应商互不干扰', () => {
|
|
|
96
96
|
assert.equal(store.getCredits('codebuddy', 'key-1'), 100)
|
|
97
97
|
assert.equal(store.getCredits('traework', 'key-1'), 200)
|
|
98
98
|
})
|
|
99
|
+
|
|
100
|
+
test('连接显示别名:set 落盘重开还在;空名删除别名回原始', () => {
|
|
101
|
+
const { store, file } = tempStore()
|
|
102
|
+
assert.equal(store.getAccountName('traework', 't1'), undefined, '没设过就是 undefined')
|
|
103
|
+
store.setAccountName('traework', 't1', '主号')
|
|
104
|
+
assert.equal(store.getAccountName('traework', 't1'), '主号')
|
|
105
|
+
store.setAccountName('traework', 't2', '') // 空名不建别名
|
|
106
|
+
assert.equal(store.getAccountName('traework', 't2'), undefined)
|
|
107
|
+
// 落盘:重开还在
|
|
108
|
+
const reopened = new SupplierConfigStore(join(file, '..', 'state.json'))
|
|
109
|
+
assert.equal(reopened.getAccountName('traework', 't1'), '主号')
|
|
110
|
+
// 空串清空:回到供应商原始昵称
|
|
111
|
+
store.setAccountName('traework', 't1', ' ')
|
|
112
|
+
assert.equal(new SupplierConfigStore(join(file, '..', 'state.json')).getAccountName('traework', 't1'), undefined)
|
|
113
|
+
})
|
package/src/supplier-config.ts
CHANGED
|
@@ -32,6 +32,8 @@ export interface SupplierConfig {
|
|
|
32
32
|
poolStrategy: PoolStrategy
|
|
33
33
|
/** 积分缓存:uid → 剩余额度(最后一次从插件拿到的非 -1 值)。 */
|
|
34
34
|
credits: Record<string, number>
|
|
35
|
+
/** 每连接显示别名:uid → 显示名(空/缺省用供应商原始昵称,纯展示不改路由键)。 */
|
|
36
|
+
accountNames: Record<string, string>
|
|
35
37
|
}
|
|
36
38
|
|
|
37
39
|
interface ConfigFile {
|
|
@@ -48,6 +50,16 @@ function readCredits(raw: unknown): Record<string, number> {
|
|
|
48
50
|
return out
|
|
49
51
|
}
|
|
50
52
|
|
|
53
|
+
/** 读盘时的连接显示别名校验:只留非空字符串。 */
|
|
54
|
+
function readNames(raw: unknown): Record<string, string> {
|
|
55
|
+
const out: Record<string, string> = {}
|
|
56
|
+
if (typeof raw !== 'object' || raw === null) return out
|
|
57
|
+
for (const [uid, v] of Object.entries(raw as Record<string, unknown>)) {
|
|
58
|
+
if (typeof v === 'string' && v.trim() !== '') out[uid] = v.trim()
|
|
59
|
+
}
|
|
60
|
+
return out
|
|
61
|
+
}
|
|
62
|
+
|
|
51
63
|
/** 通用供应商配置存储。 */
|
|
52
64
|
export class SupplierConfigStore {
|
|
53
65
|
private fp = ''
|
|
@@ -78,7 +90,7 @@ export class SupplierConfigStore {
|
|
|
78
90
|
get(supplierId: string): SupplierConfig {
|
|
79
91
|
let cfg = this.bySupplier.get(supplierId)
|
|
80
92
|
if (!cfg) {
|
|
81
|
-
cfg = { alias: '', disabled: [], custom: [], poolOrder: [], poolStrategy: 'fallback', credits: {} }
|
|
93
|
+
cfg = { alias: '', disabled: [], custom: [], poolOrder: [], poolStrategy: 'fallback', credits: {}, accountNames: {} }
|
|
82
94
|
this.bySupplier.set(supplierId, cfg)
|
|
83
95
|
}
|
|
84
96
|
return cfg
|
|
@@ -139,6 +151,28 @@ export class SupplierConfigStore {
|
|
|
139
151
|
this.saveLocked()
|
|
140
152
|
}
|
|
141
153
|
|
|
154
|
+
// ---- 每连接显示别名(连接池改名,纯展示;uid 仍是路由/匹配键) ----
|
|
155
|
+
|
|
156
|
+
/** 读某连接的显示别名;没设或为空返回 `undefined`(用供应商原始昵称)。 */
|
|
157
|
+
getAccountName(supplierId: string, uid: string): string | undefined {
|
|
158
|
+
const n = this.get(supplierId).accountNames[uid]
|
|
159
|
+
return typeof n === 'string' && n !== '' ? n : undefined
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** 设置连接显示名。空名 = 删除别名(回到供应商原始昵称)。 */
|
|
163
|
+
setAccountName(supplierId: string, uid: string, name: string): void {
|
|
164
|
+
const cfg = this.get(supplierId)
|
|
165
|
+
const clean = (name ?? '').trim()
|
|
166
|
+
if (clean === '') {
|
|
167
|
+
if (cfg.accountNames[uid] === undefined) return
|
|
168
|
+
delete cfg.accountNames[uid]
|
|
169
|
+
this.saveLocked()
|
|
170
|
+
return
|
|
171
|
+
}
|
|
172
|
+
cfg.accountNames[uid] = clean
|
|
173
|
+
this.saveLocked()
|
|
174
|
+
}
|
|
175
|
+
|
|
142
176
|
// ---- 积分缓存(核心统一持久化,插件只管报) ----
|
|
143
177
|
|
|
144
178
|
/**
|
|
@@ -189,6 +223,7 @@ export class SupplierConfigStore {
|
|
|
189
223
|
poolOrder: Array.isArray(c.poolOrder) ? c.poolOrder.filter((u) => typeof u === 'string') : [],
|
|
190
224
|
poolStrategy: c.poolStrategy === 'round-robin' ? 'round-robin' : 'fallback',
|
|
191
225
|
credits: readCredits(c.credits),
|
|
226
|
+
accountNames: readNames(c.accountNames),
|
|
192
227
|
})
|
|
193
228
|
}
|
|
194
229
|
}
|
|
@@ -204,6 +239,7 @@ export class SupplierConfigStore {
|
|
|
204
239
|
poolOrder: [...cfg.poolOrder],
|
|
205
240
|
poolStrategy: cfg.poolStrategy,
|
|
206
241
|
credits: { ...cfg.credits },
|
|
242
|
+
accountNames: { ...cfg.accountNames },
|
|
207
243
|
}
|
|
208
244
|
}
|
|
209
245
|
try {
|
|
@@ -23,6 +23,21 @@ export interface SupplierEnv {
|
|
|
23
23
|
store: SupplierConfigStore
|
|
24
24
|
/** 通用凭证存储(账号凭证,核心统一管)。 */
|
|
25
25
|
credentials: CredentialStore
|
|
26
|
+
/**
|
|
27
|
+
* 迟到的失败上报(可选)——**只在响应已提交之后**才用。
|
|
28
|
+
*
|
|
29
|
+
* 流式请求一旦写出第一个字节就绑死(HTTP 语义),此时上游再报错已经换不了
|
|
30
|
+
* 号、也改不了状态码。但「这个号坏了」这件事对**后续**请求仍然有价值:
|
|
31
|
+
* 不报上来,它就会继续留在池里被轮转选中,每次都白撞一次同一个错误。
|
|
32
|
+
*
|
|
33
|
+
* 核心实现 = `pool.noteFailure(uid, model, state, message)`,按该状态的规则
|
|
34
|
+
* 冷却/禁用。真实案例:TRAE 免费通道对某账号拒绝某模型时发 HTTP 200 +
|
|
35
|
+
* 流内 `code=4008`,三个号里两个如此,round-robin 下 2/3 请求直接失败
|
|
36
|
+
* (2026-09-08 实测)——有了这条通道,坏号被冷一次就退出轮转。
|
|
37
|
+
*
|
|
38
|
+
* 插件拿不到也不用兜底:不调用就退化成今天的行为,不影响正确性。
|
|
39
|
+
*/
|
|
40
|
+
onLateFailure?: (uid: string, model: string, state: AccountState, message: string) => void
|
|
26
41
|
}
|
|
27
42
|
|
|
28
43
|
/** 通用供应商工厂:一个 js 模块 export 它(或 default export 它),loader 调用得到实例。 */
|
|
@@ -97,6 +112,13 @@ export interface SupplierModule {
|
|
|
97
112
|
/** 图标(URL 或 SVG data URI),面板供应商卡片显示。可选。 */
|
|
98
113
|
readonly icon?: string
|
|
99
114
|
|
|
115
|
+
/**
|
|
116
|
+
* 「添加 API key」弹窗的提示文案(HTML 未转义前是纯文本)。可选。
|
|
117
|
+
* 缺省时前端用通用文案。供应商若走 `addApiKey` 且有自己的 key 获取方式,
|
|
118
|
+
* 应提供它——否则所有供应商弹窗都显示同一句通用提示(如默认会写 OpenRouter)。
|
|
119
|
+
*/
|
|
120
|
+
readonly apiKeyHint?: string
|
|
121
|
+
|
|
100
122
|
// ---- 核心(必须) ----
|
|
101
123
|
/** 报账号「现在状态」。冷却/禁用/错误累计由核心叠加,插件不算这些。 */
|
|
102
124
|
status(): SupplierStatusNow
|
|
@@ -107,10 +129,14 @@ export interface SupplierModule {
|
|
|
107
129
|
* 选号/回退/健康判定全是核心的活。
|
|
108
130
|
* 无账号供应商(如 opencode)传空 uid,忽略即可。
|
|
109
131
|
*
|
|
132
|
+
* `lv` 是推理等级(reasoning effort),`'auto'` 表示不显式下发;
|
|
133
|
+
* 供应商各自决定怎么把 `lv` 映射到上游(OpenAI 兼容一般是
|
|
134
|
+
* `reasoning_effort` 字段)。
|
|
135
|
+
*
|
|
110
136
|
* 失败原因通过返回值的 `message` 报(核心在测试模型时直接用它做诊断提示),
|
|
111
137
|
* 插件不必自己存一份「上次失败」的状态。
|
|
112
138
|
*/
|
|
113
|
-
chatOnce(uid: string, req: ChatRequest): Promise<ChatOnceResult>
|
|
139
|
+
chatOnce(uid: string, lv: string, req: ChatRequest): Promise<ChatOnceResult>
|
|
114
140
|
dispose(): void
|
|
115
141
|
|
|
116
142
|
// ---- 差异化能力(可选,按存在性暴露端点/UI) ----
|
|
@@ -15,7 +15,7 @@ import { join } from 'node:path'
|
|
|
15
15
|
import { wrapModule } from './loader.ts'
|
|
16
16
|
import { SupplierConfigStore } from '../supplier-config.ts'
|
|
17
17
|
import type { CredentialStore } from '../credential-store.ts'
|
|
18
|
-
import type { SupplierModule, SupplierAccountNow, ChatOnceResult } from './contract.ts'
|
|
18
|
+
import type { SupplierEnv, SupplierModule, SupplierAccountNow, ChatOnceResult } from './contract.ts'
|
|
19
19
|
import type { ChatRequest } from '../router/types.ts'
|
|
20
20
|
|
|
21
21
|
const UNKNOWN = -1
|
|
@@ -116,3 +116,68 @@ test('不持久化(store fp 为空)时也不炸', async () => {
|
|
|
116
116
|
const loaded = wrapModule(plugin('codebuddy', [[acct('cb-1', 50)]]), e, 'test')
|
|
117
117
|
assert.equal(loaded.supplier.status().accounts[0]?.credits, 50)
|
|
118
118
|
})
|
|
119
|
+
|
|
120
|
+
// ---------------------------------------------------------------------------
|
|
121
|
+
// onLateFailure:流式响应已提交后才发现的失败,要落回本供应商的池
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* 造一个会调用 env.onLateFailure 的插件(模拟 traework 的流式 4008 上报)。
|
|
126
|
+
* 记录它拿到的 env —— 用于验证「factory 先跑、回调后挂」这个顺序下插件仍能用。
|
|
127
|
+
*/
|
|
128
|
+
function latePlugin(id: string, seen: SupplierEnv[]): SupplierModule {
|
|
129
|
+
let captured: SupplierEnv | undefined
|
|
130
|
+
return {
|
|
131
|
+
id,
|
|
132
|
+
name: id,
|
|
133
|
+
status: () => ({ id, name: id, accounts: [acct('u1', 0), acct('u2', 0)] }),
|
|
134
|
+
listModels: () => [],
|
|
135
|
+
getAlias: () => id,
|
|
136
|
+
chatOnce: async (_uid: string, _req: ChatRequest): Promise<ChatOnceResult> => ({ ok: true, status: 200, body: '{}' }),
|
|
137
|
+
dispose: () => {},
|
|
138
|
+
// 真实插件(traework)在 factory 里存下 env,调用时才读 onLateFailure
|
|
139
|
+
__capture: (env: SupplierEnv) => { captured = env; seen.push(env) },
|
|
140
|
+
__report: (uid: string, model: string) => captured?.onLateFailure?.(uid, model, 'quota', 'boom'),
|
|
141
|
+
} as unknown as SupplierModule
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
test('onLateFailure 挂到 env 上,且冷却作用于本供应商的池(坏号退出选号)', () => {
|
|
145
|
+
const { env: e } = env()
|
|
146
|
+
const seen: SupplierEnv[] = []
|
|
147
|
+
const p = latePlugin('supA', seen)
|
|
148
|
+
const loaded = wrapModule(p, e, 'test')
|
|
149
|
+
assert.notEqual(e.onLateFailure, undefined, 'wrapModule 之后回调已挂上(插件必须调用时才读)')
|
|
150
|
+
// 模拟插件在 factory 里存下 env —— 此刻再读 onLateFailure 才有值
|
|
151
|
+
;(p as unknown as { __capture(env: SupplierEnv): void }).__capture(e)
|
|
152
|
+
|
|
153
|
+
// 上报前:两个号都健康
|
|
154
|
+
const before = loaded.supplier.status().accounts.map((a) => a.cooling)
|
|
155
|
+
assert.deepEqual(before, [false, false])
|
|
156
|
+
// 上报 u1 在模型 m1 上的配额失败
|
|
157
|
+
;(p as unknown as { __report(uid: string, model: string): void }).__report('u1', 'm1')
|
|
158
|
+
const after = new Map(loaded.supplier.status().accounts.map((a) => [a.uid, a.cooling]))
|
|
159
|
+
assert.equal(after.get('u1'), true, '上报后该号应进入冷却')
|
|
160
|
+
assert.equal(after.get('u2'), false, '冷却是 (模型,号) 粒度,不能连坐同供应商其它号')
|
|
161
|
+
// 且真的退出选号:只剩 u2
|
|
162
|
+
assert.equal(loaded.supplier.pool.pick(loaded.supplier.accounts(), [], 'fallback', 'm1'), 'u2')
|
|
163
|
+
assert.equal(loaded.supplier.pool.pick(loaded.supplier.accounts(), [], 'fallback', 'm2'), 'u1', '换个模型不受影响')
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
test('onLateFailure 不串供应商:各自落到自己的池', () => {
|
|
167
|
+
const { env: ea } = env()
|
|
168
|
+
const { env: eb } = env()
|
|
169
|
+
const loadedA = wrapModule(latePlugin('supA', []), ea, 'testA')
|
|
170
|
+
const loadedB = wrapModule(latePlugin('supB', []), eb, 'testB')
|
|
171
|
+
ea.onLateFailure?.('u1', 'm1', 'quota', 'boom')
|
|
172
|
+
const coolA = new Map(loadedA.supplier.status().accounts.map((a) => [a.uid, a.cooling]))
|
|
173
|
+
const coolB = new Map(loadedB.supplier.status().accounts.map((a) => [a.uid, a.cooling]))
|
|
174
|
+
assert.equal(coolA.get('u1'), true)
|
|
175
|
+
assert.equal(coolB.get('u1'), false, 'A 的失败不能冷到 B 的号')
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
test('onLateFailure 空 uid(无账号供应商)→ 不记,不炸', () => {
|
|
179
|
+
const { env: e } = env()
|
|
180
|
+
const loaded = wrapModule(latePlugin('supA', []), e, 'test')
|
|
181
|
+
assert.doesNotThrow(() => e.onLateFailure?.('', 'm1', 'quota', 'boom'))
|
|
182
|
+
assert.deepEqual(loaded.supplier.status().accounts.map((a) => a.cooling), [false, false])
|
|
183
|
+
})
|
package/src/suppliers/loader.ts
CHANGED
|
@@ -83,13 +83,23 @@ const DIFF_CAPS = [
|
|
|
83
83
|
'checkinNow',
|
|
84
84
|
] as const
|
|
85
85
|
|
|
86
|
+
/**
|
|
87
|
+
* 每个供应商一份 env 副本:`onLateFailure` 是**按供应商**绑到本供应商的池上
|
|
88
|
+
* 的(见 wrapModule),共用一个 env 对象会让所有插件的上报都落到最后一个
|
|
89
|
+
* 供应商的池里。副本是浅拷贝,store/credentials 仍是同一实例。
|
|
90
|
+
*/
|
|
91
|
+
function ownEnv(env: SupplierEnv): SupplierEnv {
|
|
92
|
+
return { ...env }
|
|
93
|
+
}
|
|
94
|
+
|
|
86
95
|
/** 从文件构造 LoadedSupplier。 */
|
|
87
96
|
async function loadOne(file: string, env: SupplierEnv, source: 'builtin' | 'user'): Promise<LoadedSupplier> {
|
|
88
97
|
const mod = await importSupplierFile(file)
|
|
98
|
+
const mine = ownEnv(env)
|
|
89
99
|
// 支持 factory: 模块本身就是工厂函数
|
|
90
100
|
let instance: SupplierModule
|
|
91
101
|
if (typeof mod === 'function') {
|
|
92
|
-
instance = (mod as unknown as SupplierFactory)(
|
|
102
|
+
instance = (mod as unknown as SupplierFactory)(mine)
|
|
93
103
|
if (typeof instance !== 'object' || instance === null || typeof instance.id !== 'string' || instance.id === '') {
|
|
94
104
|
throw new Error(`供应商 factory 返回无效: ${file}`)
|
|
95
105
|
}
|
|
@@ -97,7 +107,7 @@ async function loadOne(file: string, env: SupplierEnv, source: 'builtin' | 'user
|
|
|
97
107
|
} else {
|
|
98
108
|
instance = mod
|
|
99
109
|
}
|
|
100
|
-
return wrapModule(instance,
|
|
110
|
+
return wrapModule(instance, mine, file, source)
|
|
101
111
|
}
|
|
102
112
|
|
|
103
113
|
/**
|
|
@@ -119,14 +129,29 @@ export function wrapModule(instance: SupplierModule, env: SupplierEnv, source: s
|
|
|
119
129
|
// 冷却键带 supplierId(跨供应商同名模型不串),故构造时注入本供应商 id。
|
|
120
130
|
const pool = new AccountPool(instance.id)
|
|
121
131
|
|
|
132
|
+
// 迟到失败的上报通道:流式响应已提交后插件才发现的错误,经它落回同一个池。
|
|
133
|
+
// **直接挂在插件持有的那个 env 对象上**(不新建副本)——factory 在建 pool
|
|
134
|
+
// 之前就跑完了,插件若构造时读了 env 就永远拿不到这个字段。插件必须在
|
|
135
|
+
// **调用时**读 `env.onLateFailure`(见契约注释),此刻它已经挂好。
|
|
136
|
+
env.onLateFailure = (uid, model, state, message): void => {
|
|
137
|
+
if (uid === '') return // 无账号供应商(如 opencode)没号可记
|
|
138
|
+
pool.noteFailure(uid, model, state, message)
|
|
139
|
+
}
|
|
140
|
+
|
|
122
141
|
const supplier: Supplier = {
|
|
123
142
|
id: instance.id,
|
|
124
143
|
name: instance.name,
|
|
125
144
|
priority: instance.priority ?? 0,
|
|
126
145
|
icon: (instance as { icon?: string }).icon,
|
|
146
|
+
apiKeyHint: (instance as { apiKeyHint?: string }).apiKeyHint,
|
|
127
147
|
status: (): SupplierStatus => {
|
|
128
148
|
const now = instance.status()
|
|
129
|
-
|
|
149
|
+
// 连接池显示别名覆盖:uid 不变,只改面板展示名
|
|
150
|
+
const accounts = now.accounts.map((a) => {
|
|
151
|
+
const alias = env.store.getAccountName(instance.id, a.uid)
|
|
152
|
+
return alias === undefined || alias === a.nickname ? a : { ...a, nickname: alias }
|
|
153
|
+
})
|
|
154
|
+
return { id: now.id, name: now.name, accounts: pool.decorate(hydrateCredits(instance.id, env.store, accounts)) }
|
|
130
155
|
},
|
|
131
156
|
listModels: (force?: boolean): Promise<ModelInfo[]> | ModelInfo[] => instance.listModels(force),
|
|
132
157
|
// 通用能力:模型启用状态/自定义由核心 SupplierConfigStore 合并
|
|
@@ -146,7 +171,7 @@ export function wrapModule(instance: SupplierModule, env: SupplierEnv, source: s
|
|
|
146
171
|
getAlias: (): string => env.store.get(instance.id).alias || instance.id,
|
|
147
172
|
customModelIds: (): string[] => [...env.store.get(instance.id).custom],
|
|
148
173
|
accounts: (): SupplierAccountNow[] => instance.status().accounts,
|
|
149
|
-
chatOnce: (uid: string, req: ChatRequest): Promise<ChatOnceResult> => instance.chatOnce(uid, req),
|
|
174
|
+
chatOnce: (uid: string, lv: string, req: ChatRequest): Promise<ChatOnceResult> => instance.chatOnce(uid, lv, req),
|
|
150
175
|
// 删除链接:数据删除是通用能力,凭证清理由供应商内部实现(js 契约不要求)
|
|
151
176
|
removeLink: isFn((instance as unknown as Record<string, unknown>).removeLink)
|
|
152
177
|
? (uid: string): Promise<boolean> => (instance as unknown as { removeLink(uid: string): Promise<boolean> }).removeLink(uid)
|
|
@@ -245,7 +245,7 @@ export default function factory(env: SupplierEnv): SupplierModule {
|
|
|
245
245
|
return true
|
|
246
246
|
},
|
|
247
247
|
/** 对单个 key 调一次上游。选号/冷却/换号是核心的活,这里只报结果。 */
|
|
248
|
-
async chatOnce(uid: string, req: ChatRequest): Promise<ChatOnceResult> {
|
|
248
|
+
async chatOnce(uid: string, lv: string, req: ChatRequest): Promise<ChatOnceResult> {
|
|
249
249
|
const base = stripAlias(req.model, currentAlias())
|
|
250
250
|
if (!(await allModels(false)).some((m) => m.id === base)) {
|
|
251
251
|
const msg = `unknown model ${JSON.stringify(req.model)}`
|
|
@@ -261,6 +261,11 @@ export default function factory(env: SupplierEnv): SupplierModule {
|
|
|
261
261
|
try {
|
|
262
262
|
const obj = JSON.parse(body) as Record<string, unknown>
|
|
263
263
|
obj.model = base
|
|
264
|
+
if (lv !== 'auto' && lv !== '' && lv !== 'none' && lv !== 'off') obj.reasoning_effort = lv
|
|
265
|
+
else if (lv === 'none' || lv === 'off') {
|
|
266
|
+
delete obj.reasoning_effort
|
|
267
|
+
delete obj.reasoning_summary
|
|
268
|
+
}
|
|
264
269
|
body = JSON.stringify(obj)
|
|
265
270
|
} catch {
|
|
266
271
|
// 保持原样
|
|
@@ -82,7 +82,7 @@ export default function factory(env: SupplierEnv): SupplierModule {
|
|
|
82
82
|
listModels: (force?: boolean): Promise<ModelInfo[]> => allModels(!!force),
|
|
83
83
|
// testModel 由 dsh-router 核心统一走 chatOnce 路径(无账号,无需回退)
|
|
84
84
|
/** 无账号供应商:uid 恒为空,只认 free 模型。 */
|
|
85
|
-
async chatOnce(_uid: string, req: ChatRequest): Promise<ChatOnceResult> {
|
|
85
|
+
async chatOnce(_uid: string, lv: string, req: ChatRequest): Promise<ChatOnceResult> {
|
|
86
86
|
// 只认 free 模型,其他模型交给别的供应商
|
|
87
87
|
if (!isFreeModel(req.model)) {
|
|
88
88
|
const msg = `unknown free model ${JSON.stringify(req.model)}`
|
|
@@ -93,6 +93,11 @@ export default function factory(env: SupplierEnv): SupplierModule {
|
|
|
93
93
|
try {
|
|
94
94
|
const obj = JSON.parse(body) as Record<string, unknown>
|
|
95
95
|
obj.model = stripAlias(String(obj.model ?? req.model), currentAlias())
|
|
96
|
+
if (lv !== 'auto' && lv !== '' && lv !== 'none' && lv !== 'off') obj.reasoning_effort = lv
|
|
97
|
+
else if (lv === 'none' || lv === 'off') {
|
|
98
|
+
delete obj.reasoning_effort
|
|
99
|
+
delete obj.reasoning_summary
|
|
100
|
+
}
|
|
96
101
|
body = JSON.stringify(obj)
|
|
97
102
|
} catch {
|
|
98
103
|
// 保持原样
|
|
@@ -18,6 +18,10 @@ export const name = 'OpenRouter'
|
|
|
18
18
|
export const priority = 10 // 免费直连(opencode=0)之后
|
|
19
19
|
/** 面板图标。 */
|
|
20
20
|
export const icon = 'data:image/png;base64,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'
|
|
21
|
+
/**
|
|
22
|
+
* 「添加 API key」弹窗提示(OpenRouter API key 获取页)。
|
|
23
|
+
*/
|
|
24
|
+
export const apiKeyHint = '填入 OpenRouter API key(https://openrouter.ai/settings/keys),可起名字区分多个 key。'
|
|
21
25
|
|
|
22
26
|
const BASE = 'https://openrouter.ai/api/v1'
|
|
23
27
|
const MODELS_URL = `${BASE}/models`
|
|
@@ -109,6 +113,7 @@ export default function factory(env: SupplierEnv): SupplierModule {
|
|
|
109
113
|
name,
|
|
110
114
|
priority,
|
|
111
115
|
icon,
|
|
116
|
+
apiKeyHint,
|
|
112
117
|
status: (): SupplierStatusNow => {
|
|
113
118
|
// 只报「现在状态」:凭证是否存在。冷却/禁用/错误累计由核心叠加。
|
|
114
119
|
const accounts = orderedKeys().map(({ uid, acct }) => ({
|
|
@@ -150,7 +155,7 @@ export default function factory(env: SupplierEnv): SupplierModule {
|
|
|
150
155
|
return true
|
|
151
156
|
},
|
|
152
157
|
/** 对单个 key 调一次上游。选号/冷却/换号是核心的活,这里只报结果。 */
|
|
153
|
-
async chatOnce(uid: string, req: ChatRequest): Promise<ChatOnceResult> {
|
|
158
|
+
async chatOnce(uid: string, lv: string, req: ChatRequest): Promise<ChatOnceResult> {
|
|
154
159
|
const base = stripAlias(req.model, currentAlias())
|
|
155
160
|
if (!freeIds.has(base) && !(await allModels(false)).some((m) => m.id === base)) {
|
|
156
161
|
const msg = `unknown free model ${JSON.stringify(req.model)}`
|
|
@@ -166,6 +171,11 @@ export default function factory(env: SupplierEnv): SupplierModule {
|
|
|
166
171
|
try {
|
|
167
172
|
const obj = JSON.parse(body) as Record<string, unknown>
|
|
168
173
|
obj.model = base
|
|
174
|
+
if (lv !== 'auto' && lv !== '' && lv !== 'none' && lv !== 'off') obj.reasoning_effort = lv
|
|
175
|
+
else if (lv === 'none' || lv === 'off') {
|
|
176
|
+
delete obj.reasoning_effort
|
|
177
|
+
delete obj.reasoning_summary
|
|
178
|
+
}
|
|
169
179
|
body = JSON.stringify(obj)
|
|
170
180
|
} catch {
|
|
171
181
|
// 保持原样
|