dsh-router-core 0.2.0 → 0.3.1
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 +40 -11
- package/docs/screenshot.png +0 -0
- package/docs/suppliers.md +302 -0
- package/lib/client-registry.js +1119 -213
- package/lib/client.js +1119 -213
- package/lib/index.js +1066 -50
- package/lib/types/llm/adapter.d.ts +2 -0
- package/lib/types/router/index.d.ts +34 -5
- package/lib/types/router/usage-store.d.ts +93 -0
- package/lib/types/router/usage-tokens.d.ts +118 -0
- package/lib/types/shared.d.ts +86 -1
- package/lib/types/supplier-config.d.ts +15 -1
- package/package.json +21 -4
- package/src/client/CheckinCard.tsx +180 -0
- package/src/client/CombosTab.tsx +28 -18
- package/src/client/RouterView.tsx +44 -27
- package/src/client/StatsTab.tsx +531 -0
- package/src/client/index.tsx +61 -11
- package/src/client/router.css +688 -35
- package/src/client/settings-nav-icon.ts +55 -0
- package/src/client/settings-section.tsx +21 -0
- package/src/client/workspace-mount.tsx +2 -9
- package/src/index.ts +51 -3
- package/src/llm/adapter.test.ts +108 -0
- package/src/llm/adapter.ts +18 -6
- package/src/router/index.ts +461 -49
- package/src/router/models-cache.test.ts +269 -0
- package/src/router/precise-dispatch.test.ts +198 -0
- package/src/router/usage-store.ts +499 -0
- package/src/router/usage-tokens.ts +327 -0
- package/src/router/usage.test.ts +558 -0
- package/src/shared.ts +90 -1
- package/src/supplier-config.ts +30 -2
- package/src/suppliers/loader.ts +2 -1
- package/src/suppliers/nvidia/plugin.ts +2 -3
- package/src/suppliers/opencode/plugin.ts +2 -3
- package/src/suppliers/openrouter/plugin.ts +2 -3
- package/src/suppliers/registry.ts +7 -2
package/src/supplier-config.ts
CHANGED
|
@@ -29,12 +29,28 @@ interface ConfigFile {
|
|
|
29
29
|
export class SupplierConfigStore {
|
|
30
30
|
private fp = ''
|
|
31
31
|
private bySupplier = new Map<string, SupplierConfig>()
|
|
32
|
+
/** 全部供应商 id(加载时同步),用于别名唯一性校验。 */
|
|
33
|
+
private knownIds = new Set<string>()
|
|
32
34
|
|
|
33
35
|
constructor(stateFile: string) {
|
|
34
36
|
this.fp = stateFile ? join(dirname(stateFile), 'supplier-config.json') : ''
|
|
35
37
|
this.load()
|
|
36
38
|
}
|
|
37
39
|
|
|
40
|
+
/** 同步已知供应商 id 集合(加载供应商后调用)。 */
|
|
41
|
+
sync(supplierIds: string[]): void {
|
|
42
|
+
this.knownIds = new Set(supplierIds)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** 各供应商当前生效的别名(未设置别名的用供应商 id)。 */
|
|
46
|
+
effectiveAliases(): Map<string, string> {
|
|
47
|
+
const out = new Map<string, string>()
|
|
48
|
+
for (const id of this.knownIds) {
|
|
49
|
+
out.set(id, this.bySupplier.get(id)?.alias || id)
|
|
50
|
+
}
|
|
51
|
+
return out
|
|
52
|
+
}
|
|
53
|
+
|
|
38
54
|
/** 读取某供应商配置(不存在则返回默认并注册)。 */
|
|
39
55
|
get(supplierId: string): SupplierConfig {
|
|
40
56
|
let cfg = this.bySupplier.get(supplierId)
|
|
@@ -45,9 +61,21 @@ export class SupplierConfigStore {
|
|
|
45
61
|
return cfg
|
|
46
62
|
}
|
|
47
63
|
|
|
48
|
-
|
|
49
|
-
|
|
64
|
+
/**
|
|
65
|
+
* 设置别名。别名必须唯一——它是对外模型全名的前缀(alias/model),
|
|
66
|
+
* 请求时用别名反查供应商,重复就会指错人。空别名表示用供应商 id(默认值)。
|
|
67
|
+
* @returns 冲突时返回占用的供应商 id
|
|
68
|
+
*/
|
|
69
|
+
setAlias(supplierId: string, alias: string): { ok: boolean; conflictWith?: string } {
|
|
70
|
+
const clean = alias.trim()
|
|
71
|
+
// 空 = 用默认(供应商 id);此时要确认没有别人显式占用了本供应商的 id 作别名
|
|
72
|
+
const effective = clean === '' ? supplierId : clean
|
|
73
|
+
for (const [id, cur] of this.effectiveAliases()) {
|
|
74
|
+
if (id !== supplierId && cur === effective) return { ok: false, conflictWith: id }
|
|
75
|
+
}
|
|
76
|
+
this.get(supplierId).alias = clean
|
|
50
77
|
this.saveLocked()
|
|
78
|
+
return { ok: true }
|
|
51
79
|
}
|
|
52
80
|
|
|
53
81
|
setModelEnabled(supplierId: string, modelId: string, enabled: boolean): void {
|
package/src/suppliers/loader.ts
CHANGED
|
@@ -123,7 +123,8 @@ export function wrapModule(instance: SupplierModule, env: SupplierEnv, source: s
|
|
|
123
123
|
})),
|
|
124
124
|
)
|
|
125
125
|
},
|
|
126
|
-
|
|
126
|
+
// 别名留空即用供应商 id(默认值)——别名是用户可改的展示前缀,唯一性由核心校验
|
|
127
|
+
getAlias: (): string => env.store.get(instance.id).alias || instance.id,
|
|
127
128
|
customModelIds: (): string[] => [...env.store.get(instance.id).custom],
|
|
128
129
|
accounts: (): SupplierAccountNow[] => instance.status().accounts,
|
|
129
130
|
chatOnce: (uid: string, req: ChatRequest): Promise<ChatOnceResult> => instance.chatOnce(uid, req),
|
|
@@ -29,7 +29,6 @@ const BASE = 'https://integrate.api.nvidia.com/v1'
|
|
|
29
29
|
const MODELS_URL = `${BASE}/models`
|
|
30
30
|
const CHAT_URL = `${BASE}/chat/completions`
|
|
31
31
|
/** 默认前缀(用户可在面板改;loader 包装会优先用 store 里的值)。 */
|
|
32
|
-
const DEFAULT_ALIAS = 'nv'
|
|
33
32
|
/** 非聊天用途的模型(护栏/翻译/解析/检测/图像生成等),聊天路由用不上,拉列表时过滤掉。 */
|
|
34
33
|
const NON_CHAT_MODEL = /(guard|safety|moderation|translate|transcription|parse|detector|embed|rerank|diffusion|tts|asr|calibration)/i
|
|
35
34
|
|
|
@@ -68,7 +67,7 @@ export default function factory(env: SupplierEnv): SupplierModule {
|
|
|
68
67
|
|
|
69
68
|
/** 当前前缀(与 loader 包装一致:store 覆盖默认值)。 */
|
|
70
69
|
function currentAlias(): string {
|
|
71
|
-
return env.store.get(id).alias ||
|
|
70
|
+
return env.store.get(id).alias || id
|
|
72
71
|
}
|
|
73
72
|
|
|
74
73
|
// ---- 模型可用性探测(NIM 的 /v1/models 无状态字段,大量模型已下线/未授权) ----
|
|
@@ -218,7 +217,7 @@ export default function factory(env: SupplierEnv): SupplierModule {
|
|
|
218
217
|
return { id, name, accounts }
|
|
219
218
|
},
|
|
220
219
|
listModels: (force?: boolean): Promise<ModelInfo[]> => allModels(!!force),
|
|
221
|
-
getAlias: (): string =>
|
|
220
|
+
getAlias: (): string => id,
|
|
222
221
|
async addApiKey(input: { name: string; apiKey: string }): Promise<{ ok: boolean; error?: string; account?: { uid: string; nickname: string } }> {
|
|
223
222
|
const key = input.apiKey.trim()
|
|
224
223
|
if (key === '') return { ok: false, error: 'API key 不能为空' }
|
|
@@ -11,7 +11,6 @@
|
|
|
11
11
|
import type { ChatRequest, ModelInfo } from '../../router/types.ts'
|
|
12
12
|
import type { AccountState, ChatOnceResult, SupplierEnv, SupplierModule, SupplierStatusNow } from '../contract.ts'
|
|
13
13
|
|
|
14
|
-
const DEFAULT_ALIAS = 'oc'
|
|
15
14
|
|
|
16
15
|
export const id = 'opencode'
|
|
17
16
|
export const name = 'OpenCode Free'
|
|
@@ -61,7 +60,7 @@ export default function factory(env: SupplierEnv): SupplierModule {
|
|
|
61
60
|
|
|
62
61
|
/** 当前前缀(与 loader 包装一致:store 覆盖默认值)。 */
|
|
63
62
|
function currentAlias(): string {
|
|
64
|
-
return env.store.get(id).alias ||
|
|
63
|
+
return env.store.get(id).alias || id
|
|
65
64
|
}
|
|
66
65
|
|
|
67
66
|
/** 剥本供应商 alias 前缀(只剥自己的,模型 id 自带的斜杠保留)。 */
|
|
@@ -82,7 +81,7 @@ export default function factory(env: SupplierEnv): SupplierModule {
|
|
|
82
81
|
icon,
|
|
83
82
|
status: (): SupplierStatusNow => ({ id, name, accounts: [] }),
|
|
84
83
|
listModels: (force?: boolean): Promise<ModelInfo[]> => allModels(!!force),
|
|
85
|
-
getAlias: (): string =>
|
|
84
|
+
getAlias: (): string => id,
|
|
86
85
|
// testModel 由 dsh-router 核心统一走 chatOnce 路径(无账号,无需回退)
|
|
87
86
|
lastError: (): string | undefined => lastErr,
|
|
88
87
|
/** 无账号供应商:uid 恒为空,只认 free 模型。 */
|
|
@@ -23,7 +23,6 @@ const BASE = 'https://openrouter.ai/api/v1'
|
|
|
23
23
|
const MODELS_URL = `${BASE}/models`
|
|
24
24
|
const CHAT_URL = `${BASE}/chat/completions`
|
|
25
25
|
/** 默认前缀(用户可在面板改;loader 包装会优先用 store 里的值)。 */
|
|
26
|
-
const DEFAULT_ALIAS = 'or'
|
|
27
26
|
|
|
28
27
|
/** 免费模型过滤(同 9Router openrouter-free):pricing 全 0 且 context ≥ 200k。 */
|
|
29
28
|
function isFreeModelMeta(m: { pricing?: { prompt?: string; completion?: string }; context_length?: number }): boolean {
|
|
@@ -70,7 +69,7 @@ export default function factory(env: SupplierEnv): SupplierModule {
|
|
|
70
69
|
|
|
71
70
|
/** 当前前缀(与 loader 包装一致:store 覆盖默认值)。 */
|
|
72
71
|
function currentAlias(): string {
|
|
73
|
-
return env.store.get(id).alias ||
|
|
72
|
+
return env.store.get(id).alias || id
|
|
74
73
|
}
|
|
75
74
|
|
|
76
75
|
async function refreshModels(): Promise<ModelInfo[]> {
|
|
@@ -122,7 +121,7 @@ export default function factory(env: SupplierEnv): SupplierModule {
|
|
|
122
121
|
return { id, name, accounts }
|
|
123
122
|
},
|
|
124
123
|
listModels: (force?: boolean): Promise<ModelInfo[]> => allModels(!!force),
|
|
125
|
-
getAlias: (): string =>
|
|
124
|
+
getAlias: (): string => id,
|
|
126
125
|
async addApiKey(input: { name: string; apiKey: string }): Promise<{ ok: boolean; error?: string; account?: { uid: string; nickname: string } }> {
|
|
127
126
|
const key = input.apiKey.trim()
|
|
128
127
|
if (key === '') return { ok: false, error: 'API key 不能为空' }
|
|
@@ -121,11 +121,16 @@ export function supplierRoutes(base: string, loaded: LoadedSupplier, store: Supp
|
|
|
121
121
|
const body = JSON.parse(await readBody(req)) as { alias?: string }
|
|
122
122
|
const alias = body.alias ?? ''
|
|
123
123
|
const clean = alias.trim()
|
|
124
|
-
|
|
124
|
+
// 空 = 用供应商 id(默认值),合法;非空则校验字符集
|
|
125
|
+
if (clean !== '' && !/^[A-Za-z0-9_-]+$/.test(clean)) {
|
|
125
126
|
writeJson(res, 400, { ok: false, error: '前缀只能包含字母、数字、- 和 _' })
|
|
126
127
|
return
|
|
127
128
|
}
|
|
128
|
-
store.setAlias(s.id, clean)
|
|
129
|
+
const r = store.setAlias(s.id, clean)
|
|
130
|
+
if (!r.ok) {
|
|
131
|
+
writeJson(res, 400, { ok: false, error: `前缀已被供应商 ${JSON.stringify(r.conflictWith ?? '?')} 占用` })
|
|
132
|
+
return
|
|
133
|
+
}
|
|
129
134
|
writeJson(res, 200, { ok: true })
|
|
130
135
|
},
|
|
131
136
|
})
|