@dsh-plus/llm-pi 0.1.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/LICENSE +21 -0
- package/lib/client.js +1904 -0
- package/lib/index.d.ts +90 -0
- package/lib/index.js +1247 -0
- package/package.json +62 -0
- package/src/catalog/builtin.ts +75 -0
- package/src/catalog/models-dev.ts +248 -0
- package/src/client/api.ts +109 -0
- package/src/client/card.tsx +239 -0
- package/src/client/client.ts +63 -0
- package/src/client/constants.ts +97 -0
- package/src/client/draft.ts +293 -0
- package/src/client/fields.tsx +272 -0
- package/src/client/i18n.ts +184 -0
- package/src/client/styles.ts +92 -0
- package/src/client/views/compat.tsx +104 -0
- package/src/client/views/models.tsx +292 -0
- package/src/client/views/provider-fields.tsx +177 -0
- package/src/client/views/providers.tsx +176 -0
- package/src/compat.ts +146 -0
- package/src/config-api.ts +135 -0
- package/src/config.ts +210 -0
- package/src/discovery.ts +176 -0
- package/src/index.ts +33 -0
- package/src/inherit.ts +85 -0
- package/src/profiles.ts +340 -0
- package/src/resolve-dsh.ts +203 -0
- package/src/service.ts +238 -0
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 模型目录编辑:每行一个模型(id/extends/name/容量/模态/reasoningEfforts/compat),
|
|
3
|
+
* extends 输入框带 datalist 候选——候选来自 /catalog 端点:选定 source
|
|
4
|
+
* (builtin / models-dev)与 provider 后拉取其 models 列表。
|
|
5
|
+
* @module llm-pi/client/views/models
|
|
6
|
+
*/
|
|
7
|
+
import { useEffect, useState, type ReactElement } from 'react'
|
|
8
|
+
|
|
9
|
+
import { fetchCatalog, type WireModelsDevStatus } from '../api.ts'
|
|
10
|
+
import { MODALITIES, THINKING_LEVELS } from '../constants.ts'
|
|
11
|
+
import { emptyModelDraft, type ModelDraft, type ReasoningDraft } from '../draft.ts'
|
|
12
|
+
import { CheckRow, TextField } from '../fields.tsx'
|
|
13
|
+
import { CompatEditor } from './compat.tsx'
|
|
14
|
+
|
|
15
|
+
type CatalogSource = 'builtin' | 'models-dev'
|
|
16
|
+
|
|
17
|
+
function catalogNote(status: WireModelsDevStatus | undefined, t: (key: string) => string): string {
|
|
18
|
+
if (status === undefined) return ''
|
|
19
|
+
if (status.error !== null) return `${t('modelsDevError')}${status.error}`
|
|
20
|
+
return `${t('modelsDevStatusLine')}:${status.providers} 个 provider,快照 ${status.fetchedAt ?? '-'}`
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface ModelsTableProps {
|
|
24
|
+
route: string
|
|
25
|
+
api: string
|
|
26
|
+
/** 默认候选 provider(route 级 extends)。 */
|
|
27
|
+
defaultProvider: string
|
|
28
|
+
models: ModelDraft[]
|
|
29
|
+
epoch: number
|
|
30
|
+
disabled?: boolean
|
|
31
|
+
t(key: string): string
|
|
32
|
+
onModels(next: ModelDraft[]): void
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function ModelsTable(props: ModelsTableProps): ReactElement {
|
|
36
|
+
const { t } = props
|
|
37
|
+
const [source, setSource] = useState<CatalogSource>('builtin')
|
|
38
|
+
const [providerIds, setProviderIds] = useState<string[]>([])
|
|
39
|
+
const [provider, setProvider] = useState('')
|
|
40
|
+
const [candidateModels, setCandidateModels] = useState<string[]>([])
|
|
41
|
+
const [note, setNote] = useState('')
|
|
42
|
+
const listId = `lpc-models-${props.route.replace(/[^a-zA-Z0-9_-]/g, '_')}`
|
|
43
|
+
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
let alive = true
|
|
46
|
+
setNote(t('catalogLoading'))
|
|
47
|
+
void (async () => {
|
|
48
|
+
try {
|
|
49
|
+
const list = await fetchCatalog('', source)
|
|
50
|
+
if (!alive) return
|
|
51
|
+
setProviderIds(list.providers)
|
|
52
|
+
const preferred = list.providers.includes(props.defaultProvider)
|
|
53
|
+
? props.defaultProvider
|
|
54
|
+
: (list.providers[0] ?? '')
|
|
55
|
+
setProvider(preferred)
|
|
56
|
+
setNote(catalogNote(list.status, t))
|
|
57
|
+
if (preferred === '') return
|
|
58
|
+
const result = await fetchCatalog(preferred, source)
|
|
59
|
+
if (alive) setCandidateModels(result.models)
|
|
60
|
+
} catch {
|
|
61
|
+
if (alive) setNote(t('catalogFailed'))
|
|
62
|
+
}
|
|
63
|
+
})()
|
|
64
|
+
return () => {
|
|
65
|
+
alive = false
|
|
66
|
+
}
|
|
67
|
+
}, [source])
|
|
68
|
+
|
|
69
|
+
const onProviderChange = (value: string): void => {
|
|
70
|
+
setProvider(value)
|
|
71
|
+
if (value === '') {
|
|
72
|
+
setCandidateModels([])
|
|
73
|
+
return
|
|
74
|
+
}
|
|
75
|
+
void fetchCatalog(value, source)
|
|
76
|
+
.then((result) => setCandidateModels(result.models))
|
|
77
|
+
.catch(() => setNote(t('catalogFailed')))
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<div className="lpc-models">
|
|
82
|
+
<div className="lpc-modelHead">
|
|
83
|
+
<span className="lpc-modelTitle">{t('modelsGroup')}</span>
|
|
84
|
+
<button
|
|
85
|
+
type="button"
|
|
86
|
+
className="lpc-btn lpc-btnGhost lpc-btnSmall"
|
|
87
|
+
disabled={props.disabled === true}
|
|
88
|
+
onClick={() => props.onModels([...props.models, emptyModelDraft()])}
|
|
89
|
+
>
|
|
90
|
+
{t('addModel')}
|
|
91
|
+
</button>
|
|
92
|
+
</div>
|
|
93
|
+
<div className="lpc-catalogBar">
|
|
94
|
+
<label className="lpc-catalogLabel" htmlFor={`${listId}-source`}>{t('catalogSource')}</label>
|
|
95
|
+
<select
|
|
96
|
+
id={`${listId}-source`}
|
|
97
|
+
className="lpc-input lpc-select lpc-catalogSelect"
|
|
98
|
+
value={source}
|
|
99
|
+
disabled={props.disabled === true}
|
|
100
|
+
onChange={(event) => setSource(event.target.value as CatalogSource)}
|
|
101
|
+
>
|
|
102
|
+
<option value="builtin">builtin</option>
|
|
103
|
+
<option value="models-dev">models-dev</option>
|
|
104
|
+
</select>
|
|
105
|
+
<label className="lpc-catalogLabel" htmlFor={`${listId}-provider`}>{t('catalogProvider')}</label>
|
|
106
|
+
<select
|
|
107
|
+
id={`${listId}-provider`}
|
|
108
|
+
className="lpc-input lpc-select lpc-catalogSelect"
|
|
109
|
+
value={provider}
|
|
110
|
+
disabled={props.disabled === true || providerIds.length === 0}
|
|
111
|
+
onChange={(event) => onProviderChange(event.target.value)}
|
|
112
|
+
>
|
|
113
|
+
<option value="">-</option>
|
|
114
|
+
{providerIds.map((id) => (
|
|
115
|
+
<option key={id} value={id}>{id}</option>
|
|
116
|
+
))}
|
|
117
|
+
</select>
|
|
118
|
+
</div>
|
|
119
|
+
{note !== '' ? <p className="lpc-hint">{note}</p> : null}
|
|
120
|
+
<datalist id={listId}>
|
|
121
|
+
{candidateModels.map((id) => (
|
|
122
|
+
<option key={id} value={id} />
|
|
123
|
+
))}
|
|
124
|
+
</datalist>
|
|
125
|
+
{props.models.map((model, index) => (
|
|
126
|
+
<ModelRow
|
|
127
|
+
key={`${index}:${model.id}`}
|
|
128
|
+
index={index}
|
|
129
|
+
model={model}
|
|
130
|
+
api={props.api}
|
|
131
|
+
listId={listId}
|
|
132
|
+
epoch={props.epoch}
|
|
133
|
+
disabled={props.disabled === true}
|
|
134
|
+
t={t}
|
|
135
|
+
onPatch={(patch) =>
|
|
136
|
+
props.onModels(props.models.map((m, i) => (i === index ? { ...m, ...patch } : m)))
|
|
137
|
+
}
|
|
138
|
+
onRemove={() => props.onModels(props.models.filter((_, i) => i !== index))}
|
|
139
|
+
/>
|
|
140
|
+
))}
|
|
141
|
+
</div>
|
|
142
|
+
)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export interface ModelRowProps {
|
|
146
|
+
index: number
|
|
147
|
+
model: ModelDraft
|
|
148
|
+
api: string
|
|
149
|
+
listId: string
|
|
150
|
+
epoch: number
|
|
151
|
+
disabled?: boolean
|
|
152
|
+
t(key: string): string
|
|
153
|
+
onPatch(patch: Partial<ModelDraft>): void
|
|
154
|
+
onRemove(): void
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function ModelRow(props: ModelRowProps): ReactElement {
|
|
158
|
+
const { model, t } = props
|
|
159
|
+
const id = `${props.listId}-m${props.index}`
|
|
160
|
+
return (
|
|
161
|
+
<div className="lpc-modelRow">
|
|
162
|
+
<div className="lpc-modelHead">
|
|
163
|
+
<span className="lpc-modelTitle">{t('modelRow')} {props.index + 1}</span>
|
|
164
|
+
<button
|
|
165
|
+
type="button"
|
|
166
|
+
className="lpc-btn lpc-btnGhost lpc-btnSmall"
|
|
167
|
+
disabled={props.disabled === true}
|
|
168
|
+
onClick={props.onRemove}
|
|
169
|
+
>
|
|
170
|
+
{t('deleteModel')}
|
|
171
|
+
</button>
|
|
172
|
+
</div>
|
|
173
|
+
<div className="lpc-grid">
|
|
174
|
+
<TextField
|
|
175
|
+
id={`${id}-id`}
|
|
176
|
+
label={t('modelId')}
|
|
177
|
+
hint={t('modelIdHint')}
|
|
178
|
+
value={model.id}
|
|
179
|
+
disabled={props.disabled === true}
|
|
180
|
+
invalid={model.id.trim() === ''}
|
|
181
|
+
invalidLabel={t('modelIdRequired')}
|
|
182
|
+
onEdit={(value) => props.onPatch({ id: value })}
|
|
183
|
+
/>
|
|
184
|
+
<TextField
|
|
185
|
+
id={`${id}-extends`}
|
|
186
|
+
label={t('modelExtends')}
|
|
187
|
+
hint={t('modelExtendsHint')}
|
|
188
|
+
value={model.extends}
|
|
189
|
+
list={props.listId}
|
|
190
|
+
disabled={props.disabled === true}
|
|
191
|
+
onEdit={(value) => props.onPatch({ extends: value })}
|
|
192
|
+
/>
|
|
193
|
+
<TextField
|
|
194
|
+
id={`${id}-name`}
|
|
195
|
+
label={t('modelName')}
|
|
196
|
+
value={model.name}
|
|
197
|
+
disabled={props.disabled === true}
|
|
198
|
+
onEdit={(value) => props.onPatch({ name: value })}
|
|
199
|
+
/>
|
|
200
|
+
<TextField
|
|
201
|
+
id={`${id}-ctx`}
|
|
202
|
+
label={t('contextWindow')}
|
|
203
|
+
numeric
|
|
204
|
+
value={model.contextWindow}
|
|
205
|
+
disabled={props.disabled === true}
|
|
206
|
+
onEdit={(value) => props.onPatch({ contextWindow: value })}
|
|
207
|
+
/>
|
|
208
|
+
<TextField
|
|
209
|
+
id={`${id}-max`}
|
|
210
|
+
label={t('maxTokens')}
|
|
211
|
+
numeric
|
|
212
|
+
value={model.maxTokens}
|
|
213
|
+
disabled={props.disabled === true}
|
|
214
|
+
onEdit={(value) => props.onPatch({ maxTokens: value })}
|
|
215
|
+
/>
|
|
216
|
+
<div className="lpc-field">
|
|
217
|
+
<span className="lpc-label">{t('input')}</span>
|
|
218
|
+
{MODALITIES.map((modality) => (
|
|
219
|
+
<CheckRow
|
|
220
|
+
key={modality}
|
|
221
|
+
id={`${id}-input-${modality}`}
|
|
222
|
+
label={modality}
|
|
223
|
+
checked={model.input[modality]}
|
|
224
|
+
disabled={props.disabled === true}
|
|
225
|
+
onEdit={(checked) => props.onPatch({ input: { ...model.input, [modality]: checked } })}
|
|
226
|
+
/>
|
|
227
|
+
))}
|
|
228
|
+
</div>
|
|
229
|
+
</div>
|
|
230
|
+
<ReasoningEditor
|
|
231
|
+
idPrefix={`${id}-re`}
|
|
232
|
+
value={model.reasoningEfforts}
|
|
233
|
+
disabled={props.disabled === true}
|
|
234
|
+
t={t}
|
|
235
|
+
onEdit={(reasoningEfforts) => props.onPatch({ reasoningEfforts })}
|
|
236
|
+
/>
|
|
237
|
+
<CompatEditor
|
|
238
|
+
idPrefix={`${id}-compat`}
|
|
239
|
+
api={props.api}
|
|
240
|
+
compat={model.compat}
|
|
241
|
+
epoch={props.epoch}
|
|
242
|
+
disabled={props.disabled === true}
|
|
243
|
+
wide
|
|
244
|
+
t={t}
|
|
245
|
+
onEdit={(compat) => props.onPatch({ compat })}
|
|
246
|
+
/>
|
|
247
|
+
</div>
|
|
248
|
+
)
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export interface ReasoningEditorProps {
|
|
252
|
+
idPrefix: string
|
|
253
|
+
value: ReasoningDraft
|
|
254
|
+
disabled?: boolean
|
|
255
|
+
t(key: string): string
|
|
256
|
+
onEdit(next: ReasoningDraft): void
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export function ReasoningEditor(props: ReasoningEditorProps): ReactElement {
|
|
260
|
+
const { value } = props
|
|
261
|
+
return (
|
|
262
|
+
<div className="lpc-field lpc-wide">
|
|
263
|
+
<div className="lpc-head">
|
|
264
|
+
<span className="lpc-label">{props.t('reasoningEfforts')}</span>
|
|
265
|
+
</div>
|
|
266
|
+
<div className="lpc-checkRow">
|
|
267
|
+
<input
|
|
268
|
+
id={`${props.idPrefix}-nonreasoning`}
|
|
269
|
+
type="checkbox"
|
|
270
|
+
checked={value.nonReasoning}
|
|
271
|
+
disabled={props.disabled === true}
|
|
272
|
+
onChange={(event) => props.onEdit({ ...value, nonReasoning: event.target.checked })}
|
|
273
|
+
/>
|
|
274
|
+
<label htmlFor={`${props.idPrefix}-nonreasoning`}>{props.t('nonReasoning')}</label>
|
|
275
|
+
</div>
|
|
276
|
+
{value.nonReasoning ? null : (
|
|
277
|
+
<div className="lpc-grid">
|
|
278
|
+
{THINKING_LEVELS.map((level) => (
|
|
279
|
+
<TextField
|
|
280
|
+
key={level}
|
|
281
|
+
id={`${props.idPrefix}-${level}`}
|
|
282
|
+
label={level}
|
|
283
|
+
value={value.levels[level] ?? ''}
|
|
284
|
+
disabled={props.disabled === true}
|
|
285
|
+
onEdit={(text) => props.onEdit({ ...value, levels: { ...value.levels, [level]: text } })}
|
|
286
|
+
/>
|
|
287
|
+
))}
|
|
288
|
+
</div>
|
|
289
|
+
)}
|
|
290
|
+
</div>
|
|
291
|
+
)
|
|
292
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider route 的标量字段组:基础字段网格(继承/显示名/端点/凭据/默认容量/
|
|
3
|
+
* 超时/默认模态/预算)与协议/档位/缓存/传输下拉组。compat、headers、
|
|
4
|
+
* retryPolicy 与模型目录在 views/providers.tsx 的 route 小节内另行渲染。
|
|
5
|
+
* @module llm-pi/client/views/provider-fields
|
|
6
|
+
*/
|
|
7
|
+
import type { ReactElement } from 'react'
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
BUDGET_KEYS, CACHE_RETENTION_OPTIONS, MODALITIES, PROTOCOL_IDS, THINKING_LEVELS, TRANSPORT_OPTIONS,
|
|
11
|
+
} from '../constants.ts'
|
|
12
|
+
import type { ProviderDraft } from '../draft.ts'
|
|
13
|
+
import { CheckRow, SelectField, TextField } from '../fields.tsx'
|
|
14
|
+
import { pruneCompatForApi } from './compat.tsx'
|
|
15
|
+
|
|
16
|
+
export interface ProviderScalarFieldsProps {
|
|
17
|
+
id: string
|
|
18
|
+
draft: ProviderDraft
|
|
19
|
+
disabled?: boolean
|
|
20
|
+
t(key: string): string
|
|
21
|
+
onPatch(patch: Partial<ProviderDraft>): void
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function ProviderScalarFields(props: ProviderScalarFieldsProps): ReactElement {
|
|
25
|
+
const { draft, t } = props
|
|
26
|
+
return (
|
|
27
|
+
<div className="lpc-grid">
|
|
28
|
+
<TextField
|
|
29
|
+
id={`${props.id}-extends`}
|
|
30
|
+
label={t('extends')}
|
|
31
|
+
hint={t('extendsHint')}
|
|
32
|
+
value={draft.extends}
|
|
33
|
+
disabled={props.disabled === true}
|
|
34
|
+
onEdit={(value) => props.onPatch({ extends: value })}
|
|
35
|
+
/>
|
|
36
|
+
<TextField
|
|
37
|
+
id={`${props.id}-displayName`}
|
|
38
|
+
label={t('displayName')}
|
|
39
|
+
value={draft.displayName}
|
|
40
|
+
disabled={props.disabled === true}
|
|
41
|
+
onEdit={(value) => props.onPatch({ displayName: value })}
|
|
42
|
+
/>
|
|
43
|
+
<TextField
|
|
44
|
+
id={`${props.id}-baseURL`}
|
|
45
|
+
label={t('baseURL')}
|
|
46
|
+
hint={t('baseURLHint')}
|
|
47
|
+
value={draft.baseURL}
|
|
48
|
+
disabled={props.disabled === true}
|
|
49
|
+
onEdit={(value) => props.onPatch({ baseURL: value })}
|
|
50
|
+
/>
|
|
51
|
+
<TextField
|
|
52
|
+
id={`${props.id}-apiKeyEnv`}
|
|
53
|
+
label={t('apiKeyEnv')}
|
|
54
|
+
hint={t('apiKeyEnvHint')}
|
|
55
|
+
value={draft.apiKeyEnv}
|
|
56
|
+
disabled={props.disabled === true}
|
|
57
|
+
onEdit={(value) => props.onPatch({ apiKeyEnv: value })}
|
|
58
|
+
/>
|
|
59
|
+
<TextField
|
|
60
|
+
id={`${props.id}-defaultCtx`}
|
|
61
|
+
label={t('defaultContextWindow')}
|
|
62
|
+
numeric
|
|
63
|
+
value={draft.defaultContextWindow}
|
|
64
|
+
disabled={props.disabled === true}
|
|
65
|
+
onEdit={(value) => props.onPatch({ defaultContextWindow: value })}
|
|
66
|
+
/>
|
|
67
|
+
<TextField
|
|
68
|
+
id={`${props.id}-defaultMax`}
|
|
69
|
+
label={t('defaultMaxTokens')}
|
|
70
|
+
numeric
|
|
71
|
+
value={draft.defaultMaxTokens}
|
|
72
|
+
disabled={props.disabled === true}
|
|
73
|
+
onEdit={(value) => props.onPatch({ defaultMaxTokens: value })}
|
|
74
|
+
/>
|
|
75
|
+
<TextField
|
|
76
|
+
id={`${props.id}-timeout`}
|
|
77
|
+
label={t('timeoutMs')}
|
|
78
|
+
numeric
|
|
79
|
+
value={draft.timeoutMs}
|
|
80
|
+
disabled={props.disabled === true}
|
|
81
|
+
onEdit={(value) => props.onPatch({ timeoutMs: value })}
|
|
82
|
+
/>
|
|
83
|
+
<TextField
|
|
84
|
+
id={`${props.id}-wsTimeout`}
|
|
85
|
+
label={t('websocketConnectTimeoutMs')}
|
|
86
|
+
numeric
|
|
87
|
+
value={draft.websocketConnectTimeoutMs}
|
|
88
|
+
disabled={props.disabled === true}
|
|
89
|
+
onEdit={(value) => props.onPatch({ websocketConnectTimeoutMs: value })}
|
|
90
|
+
/>
|
|
91
|
+
<TextField
|
|
92
|
+
id={`${props.id}-streamIdle`}
|
|
93
|
+
label={t('streamIdleTimeoutMs')}
|
|
94
|
+
numeric
|
|
95
|
+
value={draft.streamIdleTimeoutMs}
|
|
96
|
+
disabled={props.disabled === true}
|
|
97
|
+
onEdit={(value) => props.onPatch({ streamIdleTimeoutMs: value })}
|
|
98
|
+
/>
|
|
99
|
+
<div className="lpc-field">
|
|
100
|
+
<span className="lpc-label">{t('defaultInput')}</span>
|
|
101
|
+
{MODALITIES.map((modality) => (
|
|
102
|
+
<CheckRow
|
|
103
|
+
key={modality}
|
|
104
|
+
id={`${props.id}-input-${modality}`}
|
|
105
|
+
label={modality}
|
|
106
|
+
checked={draft.input[modality]}
|
|
107
|
+
disabled={props.disabled === true}
|
|
108
|
+
onEdit={(checked) => props.onPatch({ input: { ...draft.input, [modality]: checked } })}
|
|
109
|
+
/>
|
|
110
|
+
))}
|
|
111
|
+
</div>
|
|
112
|
+
<div className="lpc-field">
|
|
113
|
+
<span className="lpc-label">{t('thinkingBudgets')}</span>
|
|
114
|
+
<div className="lpc-grid lpc-gridNested">
|
|
115
|
+
{BUDGET_KEYS.map((key) => (
|
|
116
|
+
<TextField
|
|
117
|
+
key={key}
|
|
118
|
+
id={`${props.id}-budget-${key}`}
|
|
119
|
+
label={key}
|
|
120
|
+
numeric
|
|
121
|
+
value={draft.thinkingBudgets[key]}
|
|
122
|
+
disabled={props.disabled === true}
|
|
123
|
+
onEdit={(value) =>
|
|
124
|
+
props.onPatch({ thinkingBudgets: { ...draft.thinkingBudgets, [key]: value } })
|
|
125
|
+
}
|
|
126
|
+
/>
|
|
127
|
+
))}
|
|
128
|
+
</div>
|
|
129
|
+
</div>
|
|
130
|
+
</div>
|
|
131
|
+
)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** 协议/档位/缓存/传输四个下拉组。 */
|
|
135
|
+
export function ProviderSelectFields(props: ProviderScalarFieldsProps): ReactElement {
|
|
136
|
+
const { draft, t } = props
|
|
137
|
+
return (
|
|
138
|
+
<div className="lpc-grid">
|
|
139
|
+
<SelectField
|
|
140
|
+
id={`${props.id}-api`}
|
|
141
|
+
label={t('api')}
|
|
142
|
+
value={draft.api}
|
|
143
|
+
options={PROTOCOL_IDS}
|
|
144
|
+
unsetLabel={t('compatUnset')}
|
|
145
|
+
disabled={props.disabled === true}
|
|
146
|
+
onEdit={(value) => props.onPatch({ api: value, compat: pruneCompatForApi(draft.compat, value) })}
|
|
147
|
+
/>
|
|
148
|
+
<SelectField
|
|
149
|
+
id={`${props.id}-reasoning`}
|
|
150
|
+
label={t('reasoning')}
|
|
151
|
+
value={draft.reasoning}
|
|
152
|
+
options={THINKING_LEVELS}
|
|
153
|
+
unsetLabel={t('compatUnset')}
|
|
154
|
+
disabled={props.disabled === true}
|
|
155
|
+
onEdit={(value) => props.onPatch({ reasoning: value })}
|
|
156
|
+
/>
|
|
157
|
+
<SelectField
|
|
158
|
+
id={`${props.id}-cache`}
|
|
159
|
+
label={t('cacheRetention')}
|
|
160
|
+
value={draft.cacheRetention}
|
|
161
|
+
options={CACHE_RETENTION_OPTIONS}
|
|
162
|
+
unsetLabel={t('compatUnset')}
|
|
163
|
+
disabled={props.disabled === true}
|
|
164
|
+
onEdit={(value) => props.onPatch({ cacheRetention: value })}
|
|
165
|
+
/>
|
|
166
|
+
<SelectField
|
|
167
|
+
id={`${props.id}-transport`}
|
|
168
|
+
label={t('transport')}
|
|
169
|
+
value={draft.transport}
|
|
170
|
+
options={TRANSPORT_OPTIONS}
|
|
171
|
+
unsetLabel={t('compatUnset')}
|
|
172
|
+
disabled={props.disabled === true}
|
|
173
|
+
onEdit={(value) => props.onPatch({ transport: value })}
|
|
174
|
+
/>
|
|
175
|
+
</div>
|
|
176
|
+
)
|
|
177
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider 路由编辑:每个 route 一张可折叠小节(键名 + 标量字段 +
|
|
3
|
+
* headers 键值对 + retryPolicy JSON + compat + 模型目录)。
|
|
4
|
+
* 支持新增 route(输入键名)与删除 route。
|
|
5
|
+
* @module llm-pi/client/views/providers
|
|
6
|
+
*/
|
|
7
|
+
import { useState, type ReactElement } from 'react'
|
|
8
|
+
|
|
9
|
+
import type { ProviderDraft } from '../draft.ts'
|
|
10
|
+
import { CollapseSection, JsonField, KeyValueEditor } from '../fields.tsx'
|
|
11
|
+
import { CompatEditor } from './compat.tsx'
|
|
12
|
+
import { ModelsTable } from './models.tsx'
|
|
13
|
+
import { ProviderScalarFields, ProviderSelectFields } from './provider-fields.tsx'
|
|
14
|
+
|
|
15
|
+
export interface ProvidersSectionProps {
|
|
16
|
+
providers: Record<string, ProviderDraft>
|
|
17
|
+
epoch: number
|
|
18
|
+
disabled?: boolean
|
|
19
|
+
t(key: string): string
|
|
20
|
+
onAddRoute(key: string): void
|
|
21
|
+
onRemoveRoute(route: string): void
|
|
22
|
+
onPatchProvider(route: string, patch: Partial<ProviderDraft>): void
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function ProvidersSection(props: ProvidersSectionProps): ReactElement {
|
|
26
|
+
const [newRoute, setNewRoute] = useState('')
|
|
27
|
+
const [routeError, setRouteError] = useState('')
|
|
28
|
+
const submitAdd = (): void => {
|
|
29
|
+
const key = newRoute.trim()
|
|
30
|
+
if (key === '') {
|
|
31
|
+
setRouteError(props.t('routeEmpty'))
|
|
32
|
+
return
|
|
33
|
+
}
|
|
34
|
+
if (props.providers[key] !== undefined) {
|
|
35
|
+
setRouteError(props.t('routeDuplicate'))
|
|
36
|
+
return
|
|
37
|
+
}
|
|
38
|
+
props.onAddRoute(key)
|
|
39
|
+
setNewRoute('')
|
|
40
|
+
setRouteError('')
|
|
41
|
+
}
|
|
42
|
+
return (
|
|
43
|
+
<div className="lpc-section">
|
|
44
|
+
<p className="lpc-groupLabel">{props.t('providersGroup')}</p>
|
|
45
|
+
<div className="lpc-addRoute">
|
|
46
|
+
<input
|
|
47
|
+
className={`lpc-input${routeError !== '' ? ' lpc-inputInvalid' : ''}`}
|
|
48
|
+
value={newRoute}
|
|
49
|
+
disabled={props.disabled === true}
|
|
50
|
+
placeholder={props.t('addRoutePlaceholder')}
|
|
51
|
+
onChange={(event) => {
|
|
52
|
+
setNewRoute(event.target.value)
|
|
53
|
+
setRouteError('')
|
|
54
|
+
}}
|
|
55
|
+
/>
|
|
56
|
+
<button
|
|
57
|
+
type="button"
|
|
58
|
+
className="lpc-btn lpc-btnGhost"
|
|
59
|
+
disabled={props.disabled === true}
|
|
60
|
+
onClick={submitAdd}
|
|
61
|
+
>
|
|
62
|
+
{props.t('addRoute')}
|
|
63
|
+
</button>
|
|
64
|
+
</div>
|
|
65
|
+
{routeError !== '' ? <p className="lpc-invalid">{routeError}</p> : null}
|
|
66
|
+
{Object.entries(props.providers).map(([route, draft]) => (
|
|
67
|
+
<ProviderSection
|
|
68
|
+
key={route}
|
|
69
|
+
route={route}
|
|
70
|
+
draft={draft}
|
|
71
|
+
epoch={props.epoch}
|
|
72
|
+
disabled={props.disabled === true}
|
|
73
|
+
t={props.t}
|
|
74
|
+
onRemove={() => props.onRemoveRoute(route)}
|
|
75
|
+
onPatch={(patch) => props.onPatchProvider(route, patch)}
|
|
76
|
+
/>
|
|
77
|
+
))}
|
|
78
|
+
</div>
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface ProviderSectionProps {
|
|
83
|
+
route: string
|
|
84
|
+
draft: ProviderDraft
|
|
85
|
+
epoch: number
|
|
86
|
+
disabled?: boolean
|
|
87
|
+
t(key: string): string
|
|
88
|
+
onRemove(): void
|
|
89
|
+
onPatch(patch: Partial<ProviderDraft>): void
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function ProviderSection(props: ProviderSectionProps): ReactElement {
|
|
93
|
+
const [open, setOpen] = useState(false)
|
|
94
|
+
const { route, draft, t } = props
|
|
95
|
+
const id = route.replace(/[^a-zA-Z0-9_-]/g, '_')
|
|
96
|
+
const summary = draft.api !== '' ? draft.api : draft.extends !== '' ? `extends ${draft.extends}` : ''
|
|
97
|
+
const fieldProps = { id, draft, disabled: props.disabled === true, t, onPatch: props.onPatch }
|
|
98
|
+
return (
|
|
99
|
+
<div className="lpc-route">
|
|
100
|
+
<div className="lpc-routeHead">
|
|
101
|
+
<button
|
|
102
|
+
type="button"
|
|
103
|
+
className="lpc-routeToggle"
|
|
104
|
+
aria-expanded={open}
|
|
105
|
+
onClick={() => setOpen(!open)}
|
|
106
|
+
>
|
|
107
|
+
<span className={`lpc-chevron${open ? ' lpc-chevronOpen' : ''}`}>▾</span>
|
|
108
|
+
<span className="lpc-routeKey">{route}</span>
|
|
109
|
+
{summary !== '' ? <span className="lpc-routeApi">{summary}</span> : null}
|
|
110
|
+
</button>
|
|
111
|
+
<button
|
|
112
|
+
type="button"
|
|
113
|
+
className="lpc-btn lpc-btnGhost lpc-btnSmall"
|
|
114
|
+
disabled={props.disabled === true}
|
|
115
|
+
onClick={props.onRemove}
|
|
116
|
+
>
|
|
117
|
+
{t('deleteRoute')}
|
|
118
|
+
</button>
|
|
119
|
+
</div>
|
|
120
|
+
{open ? (
|
|
121
|
+
<div className="lpc-routeBody">
|
|
122
|
+
<p className="lpc-groupLabel">{t('providerFields')}</p>
|
|
123
|
+
<ProviderScalarFields {...fieldProps} />
|
|
124
|
+
<ProviderSelectFields {...fieldProps} />
|
|
125
|
+
<KeyValueEditor
|
|
126
|
+
id={`${id}-headers`}
|
|
127
|
+
label={t('headers')}
|
|
128
|
+
hint={t('headersHint')}
|
|
129
|
+
pairs={draft.headers}
|
|
130
|
+
disabled={props.disabled === true}
|
|
131
|
+
keyPlaceholder={t('key')}
|
|
132
|
+
valuePlaceholder={t('value')}
|
|
133
|
+
addLabel={t('add')}
|
|
134
|
+
removeLabel={t('remove')}
|
|
135
|
+
onEdit={(headers) => props.onPatch({ headers })}
|
|
136
|
+
/>
|
|
137
|
+
<CollapseSection id={`${id}-advanced`} title={t('advancedGroup')} defaultOpen={false}>
|
|
138
|
+
<>
|
|
139
|
+
<JsonField
|
|
140
|
+
id={`${id}-retry`}
|
|
141
|
+
label={t('retryPolicy')}
|
|
142
|
+
hint={t('retryPolicyHint')}
|
|
143
|
+
invalidText={t('invalidJson')}
|
|
144
|
+
value={draft.retryPolicy}
|
|
145
|
+
epoch={props.epoch}
|
|
146
|
+
disabled={props.disabled === true}
|
|
147
|
+
wide
|
|
148
|
+
onEdit={(retryPolicy) => props.onPatch({ retryPolicy })}
|
|
149
|
+
/>
|
|
150
|
+
<CompatEditor
|
|
151
|
+
idPrefix={`${id}-compat`}
|
|
152
|
+
api={draft.api}
|
|
153
|
+
compat={draft.compat}
|
|
154
|
+
epoch={props.epoch}
|
|
155
|
+
disabled={props.disabled === true}
|
|
156
|
+
wide
|
|
157
|
+
t={t}
|
|
158
|
+
onEdit={(compat) => props.onPatch({ compat })}
|
|
159
|
+
/>
|
|
160
|
+
</>
|
|
161
|
+
</CollapseSection>
|
|
162
|
+
<ModelsTable
|
|
163
|
+
route={route}
|
|
164
|
+
api={draft.api}
|
|
165
|
+
defaultProvider={draft.extends}
|
|
166
|
+
models={draft.models}
|
|
167
|
+
epoch={props.epoch}
|
|
168
|
+
disabled={props.disabled === true}
|
|
169
|
+
t={t}
|
|
170
|
+
onModels={(models) => props.onPatch({ models })}
|
|
171
|
+
/>
|
|
172
|
+
</div>
|
|
173
|
+
) : null}
|
|
174
|
+
</div>
|
|
175
|
+
)
|
|
176
|
+
}
|