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/client/CombosTab.tsx
CHANGED
|
@@ -49,17 +49,25 @@ interface ComboForm {
|
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
export function CombosTab({ combos, aliases, onRefresh }: CombosTabProps): JSX.Element {
|
|
52
|
-
/**
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
52
|
+
/**
|
|
53
|
+
* 存储值 → 展示全名 alias/model。
|
|
54
|
+
* 存储格式 = `supplierId,modelId`:精准,同名模型不会串台。
|
|
55
|
+
* 兼容旧数据的裸 modelId:查不到归属就显示裸名——不能兜底套第一个供应商的
|
|
56
|
+
* 前缀,那样前缀是假的。
|
|
57
|
+
*/
|
|
58
|
+
const displayName = (stored: string): string => {
|
|
59
|
+
const comma = stored.indexOf(',')
|
|
60
|
+
if (comma > 0) {
|
|
61
|
+
const sid = stored.slice(0, comma)
|
|
62
|
+
const mid = stored.slice(comma + 1)
|
|
63
|
+
const g = groups.find(x => x.supplier.id === sid)
|
|
64
|
+
const alias = g?.supplier.alias ?? aliases.find(a => a.id === sid)?.alias ?? ''
|
|
65
|
+
return alias === '' ? mid : `${alias}/${mid}`
|
|
58
66
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
return
|
|
67
|
+
// 旧数据裸 id:按分组找归属(优先已启用的),找不到显示裸名
|
|
68
|
+
const hit = groups.find(g => g.models.some(m => m.id === stored && m.enabled))
|
|
69
|
+
?? groups.find(g => g.models.some(m => m.id === stored))
|
|
70
|
+
return hit === undefined ? stored : `${hit.supplier.alias}/${stored}`
|
|
63
71
|
}
|
|
64
72
|
const [showForm, setShowForm] = useState(false)
|
|
65
73
|
const [editing, setEditing] = useState<RouterCombo | null>(null)
|
|
@@ -134,11 +142,13 @@ export function CombosTab({ combos, aliases, onRefresh }: CombosTabProps): JSX.E
|
|
|
134
142
|
setForm(prev => ({ ...prev, ...patch }))
|
|
135
143
|
}
|
|
136
144
|
|
|
137
|
-
|
|
145
|
+
/** 勾选:存 `supplierId,modelId`(精准归属,展示时才拼 alias)。 */
|
|
146
|
+
const toggleModel = (supplierId: string, id: string): void => {
|
|
147
|
+
const stored = `${supplierId},${id}`
|
|
138
148
|
patchForm({
|
|
139
|
-
models: form.models.includes(
|
|
140
|
-
? form.models.filter(m => m !==
|
|
141
|
-
: [...form.models,
|
|
149
|
+
models: form.models.includes(stored)
|
|
150
|
+
? form.models.filter(m => m !== stored)
|
|
151
|
+
: [...form.models, stored],
|
|
142
152
|
})
|
|
143
153
|
}
|
|
144
154
|
|
|
@@ -313,7 +323,7 @@ export function CombosTab({ combos, aliases, onRefresh }: CombosTabProps): JSX.E
|
|
|
313
323
|
{combo.models.slice(0, 4).map((model, i) => (
|
|
314
324
|
<span key={`${model}-${i}`} className="dshr-comboChip">
|
|
315
325
|
<span className="dshr-comboChipNum">{i + 1}</span>
|
|
316
|
-
<span className="dshr-comboChipSupplier">{
|
|
326
|
+
<span className="dshr-comboChipSupplier">{displayName(model)}</span>
|
|
317
327
|
</span>
|
|
318
328
|
))}
|
|
319
329
|
{combo.models.length > 4 && (
|
|
@@ -384,7 +394,7 @@ export function CombosTab({ combos, aliases, onRefresh }: CombosTabProps): JSX.E
|
|
|
384
394
|
>
|
|
385
395
|
<span className="dshr-comboGrip" title="拖拽排序"><Icon d={I.grip} size={14} /></span>
|
|
386
396
|
<span className="dshr-comboStepNum">{index + 1}</span>
|
|
387
|
-
<code className="dshr-comboSelectedModel">{
|
|
397
|
+
<code className="dshr-comboSelectedModel">{displayName(model)}</code>
|
|
388
398
|
<button type="button" className="dshr-iconBtn dshr-iconBtn-sm dshr-comboStepRemove" title="移除" onClick={() => removeModel(index)}>
|
|
389
399
|
<Icon d={I.delete} size={14} />
|
|
390
400
|
</button>
|
|
@@ -413,14 +423,14 @@ export function CombosTab({ combos, aliases, onRefresh }: CombosTabProps): JSX.E
|
|
|
413
423
|
? <p className="dshr-muted dshr-comboPickEmpty">没有可添加的模型</p>
|
|
414
424
|
: groups.map(g => {
|
|
415
425
|
const items = g.models.filter(m =>
|
|
416
|
-
!form.models.includes(m.id) &&
|
|
426
|
+
!form.models.includes(`${g.supplier.id},${m.id}`) &&
|
|
417
427
|
(modelQuery === '' || m.id.toLowerCase().includes(modelQuery.toLowerCase())))
|
|
418
428
|
if (items.length === 0) return null
|
|
419
429
|
return (
|
|
420
430
|
<div key={g.supplier.id} className="dshr-comboPickGroup">
|
|
421
431
|
<p className="dshr-comboPickGroupTitle">{g.supplier.name} · {g.supplier.alias}</p>
|
|
422
432
|
{items.map(m => (
|
|
423
|
-
<button key={m.id} type="button" className="dshr-comboPickItem" onClick={() => toggleModel(m.id)}>
|
|
433
|
+
<button key={m.id} type="button" className="dshr-comboPickItem" onClick={() => toggleModel(g.supplier.id, m.id)}>
|
|
424
434
|
<span className="dshr-comboPickName">{m.id}</span>
|
|
425
435
|
</button>
|
|
426
436
|
))}
|
|
@@ -1,17 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 路由系统 (Routing System) panel — a simplified 9router.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* dashboard: 供应商 (supplier cards → detail: accounts +
|
|
6
|
-
* 组合 (combo fallback chains),
|
|
4
|
+
* Four tabs fed by the host half (/router/api/*), mirroring 9router's
|
|
5
|
+
* dashboard: 概览 (usage), 供应商 (supplier cards → detail: accounts +
|
|
6
|
+
* add-account + models), 组合 (combo fallback chains),
|
|
7
|
+
* 端点与密钥 (endpoint URL + API keys + auth).
|
|
7
8
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
9
|
+
* **挂载在「设置」里**(`settings.section` 座位),所以布局规则跟中心栏劫持时期不同:
|
|
10
|
+
* - 没有 masthead / 返回会话按钮 —— 设置面板自带标题栏和关闭
|
|
11
|
+
* - **tab 条固定、只有内容区滚**:根节点限高 `height:100%`,内容区
|
|
12
|
+
* `flex:1 + min-height:0 + overflow-y:auto` 自己滚。
|
|
13
|
+
* 根节点**必须限高** —— 不限高内容会把外壳撑开,退化成整页滚、
|
|
14
|
+
* tab 条跟着滚上去
|
|
15
|
+
* - 根节点外面套了一层 `display: contents`(宿主 slot renderer 给的),
|
|
16
|
+
* 不过实测 `height:100%` 仍能穿过它解析到外壳高度
|
|
17
|
+
* - 内容宽度实测 564px(面板 612 − 左右 padding 24),卡片 grid 按这个宽度排
|
|
10
18
|
*/
|
|
11
19
|
import { useEffect, useRef, useState, type ReactNode } from 'react'
|
|
12
20
|
import {
|
|
13
21
|
ROUTER_API_BASE,
|
|
14
|
-
ROUTER_TITLE,
|
|
15
22
|
type RouterCombosResponse,
|
|
16
23
|
type RouterHealthResponse,
|
|
17
24
|
type RouterStatusResponse,
|
|
@@ -19,8 +26,9 @@ import {
|
|
|
19
26
|
import { SupplierDetail } from './SupplierDetail.tsx'
|
|
20
27
|
import { EndpointTab } from './EndpointTab.tsx'
|
|
21
28
|
import { CombosTab } from './CombosTab.tsx'
|
|
29
|
+
import { StatsTab } from './StatsTab.tsx'
|
|
22
30
|
|
|
23
|
-
type TabId = 'suppliers' | 'combos' | 'endpoint'
|
|
31
|
+
type TabId = 'overview' | 'suppliers' | 'combos' | 'endpoint'
|
|
24
32
|
|
|
25
33
|
interface Snapshot {
|
|
26
34
|
health: RouterHealthResponse | null
|
|
@@ -37,12 +45,15 @@ async function getJson<T>(path: string): Promise<T> {
|
|
|
37
45
|
}
|
|
38
46
|
|
|
39
47
|
interface RouterViewProps {
|
|
40
|
-
/**
|
|
41
|
-
|
|
48
|
+
/**
|
|
49
|
+
* 设置页不提供「返回会话」(关闭由设置面板自己负责),所以这里是可选的:
|
|
50
|
+
* 中心栏劫持那套老挂载方式还留着时会传。
|
|
51
|
+
*/
|
|
52
|
+
onBack?: () => void
|
|
42
53
|
}
|
|
43
54
|
|
|
44
55
|
export function RouterView({ onBack }: RouterViewProps): JSX.Element {
|
|
45
|
-
const [tab, setTab] = useState<TabId>('
|
|
56
|
+
const [tab, setTab] = useState<TabId>('overview')
|
|
46
57
|
const [detailSupplier, setDetailSupplier] = useState<{ id: string; name: string; icon?: string } | null>(null)
|
|
47
58
|
const [snapshot, setSnapshot] = useState<Snapshot>({ health: null, status: null, combos: null })
|
|
48
59
|
const [refreshing, setRefreshing] = useState(false)
|
|
@@ -111,32 +122,38 @@ export function RouterView({ onBack }: RouterViewProps): JSX.Element {
|
|
|
111
122
|
}
|
|
112
123
|
|
|
113
124
|
return (
|
|
114
|
-
<div className="dshr-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
</svg>
|
|
120
|
-
<span>返回会话</span>
|
|
125
|
+
<div className="dshr-settings">
|
|
126
|
+
{/* tab 切换:设置页已有页面标题,这里只做二级切换 */}
|
|
127
|
+
<nav className="dshr-tabs" role="tablist" aria-label="路由分区">
|
|
128
|
+
<button type="button" role="tab" aria-selected={tab === 'overview'} className={`dshr-tab${tab === 'overview' ? ' dshr-tab-on' : ''}`} onClick={() => setTab('overview')}>
|
|
129
|
+
概览
|
|
121
130
|
</button>
|
|
122
|
-
<
|
|
123
|
-
<div className="dshr-brandTitle">{ROUTER_TITLE}</div>
|
|
124
|
-
</div>
|
|
125
|
-
</header>
|
|
126
|
-
|
|
127
|
-
<nav className="dshr-nav">
|
|
128
|
-
<button type="button" aria-current={tab === 'suppliers' ? 'page' : undefined} onClick={() => setTab('suppliers')}>
|
|
131
|
+
<button type="button" role="tab" aria-selected={tab === 'suppliers'} className={`dshr-tab${tab === 'suppliers' ? ' dshr-tab-on' : ''}`} onClick={() => setTab('suppliers')}>
|
|
129
132
|
供应商 <span className="dshr-navCount">{suppliers.length}</span>
|
|
130
133
|
</button>
|
|
131
|
-
<button type="button" aria-
|
|
134
|
+
<button type="button" role="tab" aria-selected={tab === 'combos'} className={`dshr-tab${tab === 'combos' ? ' dshr-tab-on' : ''}`} onClick={() => setTab('combos')}>
|
|
132
135
|
组合 <span className="dshr-navCount">{combos.length}</span>
|
|
133
136
|
</button>
|
|
134
|
-
<button type="button" aria-
|
|
137
|
+
<button type="button" role="tab" aria-selected={tab === 'endpoint'} className={`dshr-tab${tab === 'endpoint' ? ' dshr-tab-on' : ''}`} onClick={() => setTab('endpoint')}>
|
|
135
138
|
端点与密钥
|
|
136
139
|
</button>
|
|
140
|
+
{/* 返回会话只在老的中心栏挂载方式下出现(设置页有关闭按钮) */}
|
|
141
|
+
{onBack !== undefined && (
|
|
142
|
+
<button type="button" className="dshr-tabBack" onClick={onBack}>返回会话</button>
|
|
143
|
+
)}
|
|
137
144
|
</nav>
|
|
138
145
|
|
|
139
|
-
<div className="dshr-
|
|
146
|
+
<div className="dshr-settingsBody">
|
|
147
|
+
{/* ---------------- 概览(用量看板) ---------------- */}
|
|
148
|
+
{tab === 'overview' && (
|
|
149
|
+
<StatsTab
|
|
150
|
+
active
|
|
151
|
+
refreshing={refreshing}
|
|
152
|
+
health={snapshot.health}
|
|
153
|
+
onRefresh={() => void refresh()}
|
|
154
|
+
/>
|
|
155
|
+
)}
|
|
156
|
+
|
|
140
157
|
{/* ---------------- 供应商 ---------------- */}
|
|
141
158
|
{tab === 'suppliers' && (
|
|
142
159
|
detailSupplier !== null
|