@dsh-plus/llm-pi 0.1.4 → 0.1.6
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/lib/client.js +6 -6
- package/lib/index.d.ts +1 -1
- package/lib/index.js +11 -9
- package/package.json +16 -5
- package/src/catalog/builtin.ts +17 -4
- package/src/catalog/models-dev.ts +13 -6
- package/src/client/api.ts +17 -4
- package/src/client/card.tsx +57 -20
- package/src/client/client.ts +20 -9
- package/src/client/constants.ts +5 -1
- package/src/client/draft.ts +20 -6
- package/src/client/fields.tsx +3 -1
- package/src/client/i18n.ts +4 -2
- package/src/client/scope.ts +22 -3
- package/src/client/views/compat.tsx +50 -44
- package/src/client/views/models.tsx +26 -7
- package/src/client/views/provider-fields.tsx +15 -3
- package/src/client/views/providers.tsx +31 -26
- package/src/compat.ts +18 -4
- package/src/config.ts +42 -11
- package/src/discovery.ts +30 -9
- package/src/index.ts +6 -3
- package/src/inherit.ts +11 -3
- package/src/profiles.ts +66 -21
- package/src/resolve-dsh.ts +15 -8
- package/src/service.ts +25 -12
|
@@ -11,7 +11,10 @@ import { COMPAT_FALLBACK_API, compatFieldSpec, compatFieldsOf } from '../constan
|
|
|
11
11
|
import { CollapseSection, JsonField, SelectField } from '../fields.tsx'
|
|
12
12
|
|
|
13
13
|
/** api 变更后裁剪 compat:只保留新渲染组的字段,避免保存时被后端拒绝。 */
|
|
14
|
-
export function pruneCompatForApi(
|
|
14
|
+
export function pruneCompatForApi(
|
|
15
|
+
compat: Record<string, unknown>,
|
|
16
|
+
api: string,
|
|
17
|
+
): Record<string, unknown> {
|
|
15
18
|
const group = api !== '' && compatFieldsOf(api).length > 0 ? api : COMPAT_FALLBACK_API
|
|
16
19
|
const fields = new Set(compatFieldsOf(group))
|
|
17
20
|
const next: Record<string, unknown> = {}
|
|
@@ -33,7 +36,8 @@ export interface CompatEditorProps {
|
|
|
33
36
|
}
|
|
34
37
|
|
|
35
38
|
export function CompatEditor(props: CompatEditorProps): ReactElement {
|
|
36
|
-
const effective =
|
|
39
|
+
const effective =
|
|
40
|
+
props.api !== '' && compatFieldsOf(props.api).length > 0 ? props.api : COMPAT_FALLBACK_API
|
|
37
41
|
const fields = compatFieldsOf(effective)
|
|
38
42
|
const setField = (field: string, value: unknown): void => {
|
|
39
43
|
const next = { ...props.compat }
|
|
@@ -43,61 +47,63 @@ export function CompatEditor(props: CompatEditorProps): ReactElement {
|
|
|
43
47
|
}
|
|
44
48
|
return (
|
|
45
49
|
<div className={`lpc-field lpc-wide`}>
|
|
46
|
-
<CollapseSection
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
id={`${props.idPrefix}-${field}`}
|
|
57
|
-
label={field}
|
|
58
|
-
value={props.compat[field] === undefined ? '' : String(props.compat[field])}
|
|
59
|
-
options={['true', 'false']}
|
|
60
|
-
unsetLabel={props.t('compatUnset')}
|
|
61
|
-
disabled={props.disabled === true}
|
|
62
|
-
onEdit={(value) => {
|
|
63
|
-
if (value === '') setField(field, undefined)
|
|
64
|
-
else setField(field, value === 'true')
|
|
65
|
-
}}
|
|
66
|
-
/>
|
|
67
|
-
)
|
|
68
|
-
}
|
|
69
|
-
if (spec === 'object') {
|
|
70
|
-
return (
|
|
71
|
-
<JsonField
|
|
72
|
-
key={field}
|
|
73
|
-
id={`${props.idPrefix}-${field}`}
|
|
74
|
-
label={field}
|
|
75
|
-
value={props.compat[field]}
|
|
76
|
-
epoch={props.epoch}
|
|
77
|
-
disabled={props.disabled === true}
|
|
78
|
-
invalidText={props.t('invalidJson')}
|
|
79
|
-
onEdit={(value) => setField(field, value)}
|
|
80
|
-
/>
|
|
81
|
-
)
|
|
82
|
-
}
|
|
50
|
+
<CollapseSection
|
|
51
|
+
id={`${props.idPrefix}-collapse`}
|
|
52
|
+
title={props.t('compatGroup')}
|
|
53
|
+
defaultOpen={false}
|
|
54
|
+
>
|
|
55
|
+
{props.api === '' ? <p className="lpc-hint">{props.t('compatApiHint')}</p> : null}
|
|
56
|
+
<div className="lpc-grid">
|
|
57
|
+
{fields.map((field) => {
|
|
58
|
+
const spec = compatFieldSpec(effective, field)
|
|
59
|
+
if (spec === 'boolean') {
|
|
83
60
|
return (
|
|
84
61
|
<SelectField
|
|
85
62
|
key={field}
|
|
86
63
|
id={`${props.idPrefix}-${field}`}
|
|
87
64
|
label={field}
|
|
88
65
|
value={props.compat[field] === undefined ? '' : String(props.compat[field])}
|
|
89
|
-
options={
|
|
66
|
+
options={['true', 'false']}
|
|
90
67
|
unsetLabel={props.t('compatUnset')}
|
|
91
68
|
disabled={props.disabled === true}
|
|
92
69
|
onEdit={(value) => {
|
|
93
70
|
if (value === '') setField(field, undefined)
|
|
94
|
-
else setField(field, value)
|
|
71
|
+
else setField(field, value === 'true')
|
|
95
72
|
}}
|
|
96
73
|
/>
|
|
97
74
|
)
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
|
|
75
|
+
}
|
|
76
|
+
if (spec === 'object') {
|
|
77
|
+
return (
|
|
78
|
+
<JsonField
|
|
79
|
+
key={field}
|
|
80
|
+
id={`${props.idPrefix}-${field}`}
|
|
81
|
+
label={field}
|
|
82
|
+
value={props.compat[field]}
|
|
83
|
+
epoch={props.epoch}
|
|
84
|
+
disabled={props.disabled === true}
|
|
85
|
+
invalidText={props.t('invalidJson')}
|
|
86
|
+
onEdit={(value) => setField(field, value)}
|
|
87
|
+
/>
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
return (
|
|
91
|
+
<SelectField
|
|
92
|
+
key={field}
|
|
93
|
+
id={`${props.idPrefix}-${field}`}
|
|
94
|
+
label={field}
|
|
95
|
+
value={props.compat[field] === undefined ? '' : String(props.compat[field])}
|
|
96
|
+
options={spec}
|
|
97
|
+
unsetLabel={props.t('compatUnset')}
|
|
98
|
+
disabled={props.disabled === true}
|
|
99
|
+
onEdit={(value) => {
|
|
100
|
+
if (value === '') setField(field, undefined)
|
|
101
|
+
else setField(field, value)
|
|
102
|
+
}}
|
|
103
|
+
/>
|
|
104
|
+
)
|
|
105
|
+
})}
|
|
106
|
+
</div>
|
|
101
107
|
</CollapseSection>
|
|
102
108
|
</div>
|
|
103
109
|
)
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* (builtin / models-dev)与 provider 后拉取其 models 列表。
|
|
5
5
|
* @module llm-pi/client/views/models
|
|
6
6
|
*/
|
|
7
|
-
import { useEffect, useState
|
|
7
|
+
import { type ReactElement, useEffect, useState } from 'react'
|
|
8
8
|
|
|
9
9
|
import { fetchCatalog, type WireModelsDevStatus } from '../api.ts'
|
|
10
10
|
import { MODALITIES, THINKING_LEVELS } from '../constants.ts'
|
|
@@ -41,6 +41,7 @@ export function ModelsTable(props: ModelsTableProps): ReactElement {
|
|
|
41
41
|
const [note, setNote] = useState('')
|
|
42
42
|
const listId = `lpc-models-${props.route.replace(/[^a-zA-Z0-9_-]/g, '_')}`
|
|
43
43
|
|
|
44
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: 目录拉取仅随 source 切换重跑;t/defaultProvider 取首帧值即可
|
|
44
45
|
useEffect(() => {
|
|
45
46
|
let alive = true
|
|
46
47
|
setNote(t('catalogLoading'))
|
|
@@ -91,7 +92,9 @@ export function ModelsTable(props: ModelsTableProps): ReactElement {
|
|
|
91
92
|
</button>
|
|
92
93
|
</div>
|
|
93
94
|
<div className="lpc-catalogBar">
|
|
94
|
-
<label className="lpc-catalogLabel" htmlFor={`${listId}-source`}>
|
|
95
|
+
<label className="lpc-catalogLabel" htmlFor={`${listId}-source`}>
|
|
96
|
+
{t('catalogSource')}
|
|
97
|
+
</label>
|
|
95
98
|
<select
|
|
96
99
|
id={`${listId}-source`}
|
|
97
100
|
className="lpc-input lpc-select lpc-catalogSelect"
|
|
@@ -102,7 +105,9 @@ export function ModelsTable(props: ModelsTableProps): ReactElement {
|
|
|
102
105
|
<option value="builtin">builtin</option>
|
|
103
106
|
<option value="models-dev">models-dev</option>
|
|
104
107
|
</select>
|
|
105
|
-
<label className="lpc-catalogLabel" htmlFor={`${listId}-provider`}>
|
|
108
|
+
<label className="lpc-catalogLabel" htmlFor={`${listId}-provider`}>
|
|
109
|
+
{t('catalogProvider')}
|
|
110
|
+
</label>
|
|
106
111
|
<select
|
|
107
112
|
id={`${listId}-provider`}
|
|
108
113
|
className="lpc-input lpc-select lpc-catalogSelect"
|
|
@@ -112,7 +117,9 @@ export function ModelsTable(props: ModelsTableProps): ReactElement {
|
|
|
112
117
|
>
|
|
113
118
|
<option value="">-</option>
|
|
114
119
|
{providerIds.map((id) => (
|
|
115
|
-
<option key={id} value={id}>
|
|
120
|
+
<option key={id} value={id}>
|
|
121
|
+
{id}
|
|
122
|
+
</option>
|
|
116
123
|
))}
|
|
117
124
|
</select>
|
|
118
125
|
</div>
|
|
@@ -124,6 +131,7 @@ export function ModelsTable(props: ModelsTableProps): ReactElement {
|
|
|
124
131
|
</datalist>
|
|
125
132
|
{props.models.map((model, index) => (
|
|
126
133
|
<ModelRow
|
|
134
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: model.id 可重复(手填),index 前缀保证 key 唯一且随行序稳定
|
|
127
135
|
key={`${index}:${model.id}`}
|
|
128
136
|
index={index}
|
|
129
137
|
model={model}
|
|
@@ -160,7 +168,9 @@ export function ModelRow(props: ModelRowProps): ReactElement {
|
|
|
160
168
|
return (
|
|
161
169
|
<div className="lpc-modelRow">
|
|
162
170
|
<div className="lpc-modelHead">
|
|
163
|
-
<span className="lpc-modelTitle">
|
|
171
|
+
<span className="lpc-modelTitle">
|
|
172
|
+
{t('modelRow')} {props.index + 1}
|
|
173
|
+
</span>
|
|
164
174
|
<button
|
|
165
175
|
type="button"
|
|
166
176
|
className="lpc-btn lpc-btnGhost lpc-btnSmall"
|
|
@@ -222,7 +232,11 @@ export function ModelRow(props: ModelRowProps): ReactElement {
|
|
|
222
232
|
label={modality}
|
|
223
233
|
checked={model.input[modality]}
|
|
224
234
|
disabled={props.disabled === true}
|
|
225
|
-
onEdit={(checked) =>
|
|
235
|
+
onEdit={(checked) =>
|
|
236
|
+
props.onPatch({
|
|
237
|
+
input: { ...model.input, [modality]: checked },
|
|
238
|
+
})
|
|
239
|
+
}
|
|
226
240
|
/>
|
|
227
241
|
))}
|
|
228
242
|
</div>
|
|
@@ -282,7 +296,12 @@ export function ReasoningEditor(props: ReasoningEditorProps): ReactElement {
|
|
|
282
296
|
label={level}
|
|
283
297
|
value={value.levels[level] ?? ''}
|
|
284
298
|
disabled={props.disabled === true}
|
|
285
|
-
onEdit={(text) =>
|
|
299
|
+
onEdit={(text) =>
|
|
300
|
+
props.onEdit({
|
|
301
|
+
...value,
|
|
302
|
+
levels: { ...value.levels, [level]: text },
|
|
303
|
+
})
|
|
304
|
+
}
|
|
286
305
|
/>
|
|
287
306
|
))}
|
|
288
307
|
</div>
|
|
@@ -7,7 +7,12 @@
|
|
|
7
7
|
import type { ReactElement } from 'react'
|
|
8
8
|
|
|
9
9
|
import {
|
|
10
|
-
BUDGET_KEYS,
|
|
10
|
+
BUDGET_KEYS,
|
|
11
|
+
CACHE_RETENTION_OPTIONS,
|
|
12
|
+
MODALITIES,
|
|
13
|
+
PROTOCOL_IDS,
|
|
14
|
+
THINKING_LEVELS,
|
|
15
|
+
TRANSPORT_OPTIONS,
|
|
11
16
|
} from '../constants.ts'
|
|
12
17
|
import type { ProviderDraft } from '../draft.ts'
|
|
13
18
|
import { CheckRow, SelectField, TextField } from '../fields.tsx'
|
|
@@ -129,7 +134,9 @@ export function ProviderScalarFields(props: ProviderScalarFieldsProps): ReactEle
|
|
|
129
134
|
value={draft.thinkingBudgets[key]}
|
|
130
135
|
disabled={props.disabled === true}
|
|
131
136
|
onEdit={(value) =>
|
|
132
|
-
props.onPatch({
|
|
137
|
+
props.onPatch({
|
|
138
|
+
thinkingBudgets: { ...draft.thinkingBudgets, [key]: value },
|
|
139
|
+
})
|
|
133
140
|
}
|
|
134
141
|
/>
|
|
135
142
|
))}
|
|
@@ -151,7 +158,12 @@ export function ProviderSelectFields(props: ProviderScalarFieldsProps): ReactEle
|
|
|
151
158
|
options={PROTOCOL_IDS}
|
|
152
159
|
unsetLabel={t('compatUnset')}
|
|
153
160
|
disabled={props.disabled === true}
|
|
154
|
-
onEdit={(value) =>
|
|
161
|
+
onEdit={(value) =>
|
|
162
|
+
props.onPatch({
|
|
163
|
+
api: value,
|
|
164
|
+
compat: pruneCompatForApi(draft.compat, value),
|
|
165
|
+
})
|
|
166
|
+
}
|
|
155
167
|
/>
|
|
156
168
|
<SelectField
|
|
157
169
|
id={`${props.id}-reasoning`}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* 支持新增 route(输入键名)与删除 route。
|
|
5
5
|
* @module llm-pi/client/views/providers
|
|
6
6
|
*/
|
|
7
|
-
import {
|
|
7
|
+
import { type ReactElement, useState } from 'react'
|
|
8
8
|
|
|
9
9
|
import type { ProviderDraft } from '../draft.ts'
|
|
10
10
|
import { CollapseSection, JsonField, KeyValueEditor } from '../fields.tsx'
|
|
@@ -93,8 +93,15 @@ export function ProviderSection(props: ProviderSectionProps): ReactElement {
|
|
|
93
93
|
const [open, setOpen] = useState(false)
|
|
94
94
|
const { route, draft, t } = props
|
|
95
95
|
const id = route.replace(/[^a-zA-Z0-9_-]/g, '_')
|
|
96
|
-
const summary =
|
|
97
|
-
|
|
96
|
+
const summary =
|
|
97
|
+
draft.api !== '' ? draft.api : draft.extends !== '' ? `extends ${draft.extends}` : ''
|
|
98
|
+
const fieldProps = {
|
|
99
|
+
id,
|
|
100
|
+
draft,
|
|
101
|
+
disabled: props.disabled === true,
|
|
102
|
+
t,
|
|
103
|
+
onPatch: props.onPatch,
|
|
104
|
+
}
|
|
98
105
|
return (
|
|
99
106
|
<div className="lpc-route">
|
|
100
107
|
<div className="lpc-routeHead">
|
|
@@ -135,29 +142,27 @@ export function ProviderSection(props: ProviderSectionProps): ReactElement {
|
|
|
135
142
|
onEdit={(headers) => props.onPatch({ headers })}
|
|
136
143
|
/>
|
|
137
144
|
<CollapseSection id={`${id}-advanced`} title={t('advancedGroup')} defaultOpen={false}>
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
/>
|
|
160
|
-
</>
|
|
145
|
+
<JsonField
|
|
146
|
+
id={`${id}-retry`}
|
|
147
|
+
label={t('retryPolicy')}
|
|
148
|
+
hint={t('retryPolicyHint')}
|
|
149
|
+
invalidText={t('invalidJson')}
|
|
150
|
+
value={draft.retryPolicy}
|
|
151
|
+
epoch={props.epoch}
|
|
152
|
+
disabled={props.disabled === true}
|
|
153
|
+
wide
|
|
154
|
+
onEdit={(retryPolicy) => props.onPatch({ retryPolicy })}
|
|
155
|
+
/>
|
|
156
|
+
<CompatEditor
|
|
157
|
+
idPrefix={`${id}-compat`}
|
|
158
|
+
api={draft.api}
|
|
159
|
+
compat={draft.compat}
|
|
160
|
+
epoch={props.epoch}
|
|
161
|
+
disabled={props.disabled === true}
|
|
162
|
+
wide
|
|
163
|
+
t={t}
|
|
164
|
+
onEdit={(compat) => props.onPatch({ compat })}
|
|
165
|
+
/>
|
|
161
166
|
</CollapseSection>
|
|
162
167
|
<ModelsTable
|
|
163
168
|
route={route}
|
package/src/compat.ts
CHANGED
|
@@ -90,7 +90,13 @@ export function compatFieldSpec(api: ProtocolId, field: string): CompatValue | u
|
|
|
90
90
|
return FIELDS_BY_PROTOCOL[api][field]
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
function checkValue(
|
|
93
|
+
function checkValue(
|
|
94
|
+
_api: ProtocolId,
|
|
95
|
+
field: string,
|
|
96
|
+
spec: CompatValue,
|
|
97
|
+
value: unknown,
|
|
98
|
+
where: string,
|
|
99
|
+
): void {
|
|
94
100
|
if (spec === 'boolean') {
|
|
95
101
|
if (typeof value !== 'boolean') throw new Error(`${where}: compat.${field} 必须是布尔值`)
|
|
96
102
|
return
|
|
@@ -102,7 +108,9 @@ function checkValue(api: ProtocolId, field: string, spec: CompatValue, value: un
|
|
|
102
108
|
return
|
|
103
109
|
}
|
|
104
110
|
if (typeof value !== 'string' || !spec.includes(value)) {
|
|
105
|
-
throw new Error(
|
|
111
|
+
throw new Error(
|
|
112
|
+
`${where}: compat.${field} 必须是 ${spec.map((v) => JSON.stringify(v)).join(' | ')} 之一`,
|
|
113
|
+
)
|
|
106
114
|
}
|
|
107
115
|
}
|
|
108
116
|
|
|
@@ -110,11 +118,17 @@ function checkValue(api: ProtocolId, field: string, spec: CompatValue, value: un
|
|
|
110
118
|
* 校验一份 compat 字典对指定协议合法:未知协议/未知键拒绝(对比官方的静默丢弃),
|
|
111
119
|
* 已知键校验值类型/枚举。undefined 值视为未设置,跳过(语义同 pi-ai 的 ??)。
|
|
112
120
|
*/
|
|
113
|
-
export function validateCompat(
|
|
121
|
+
export function validateCompat(
|
|
122
|
+
api: string,
|
|
123
|
+
compat: Record<string, unknown> | undefined,
|
|
124
|
+
where: string,
|
|
125
|
+
): void {
|
|
114
126
|
if (compat === undefined) return
|
|
115
127
|
const fields = FIELDS_BY_PROTOCOL[api as ProtocolId]
|
|
116
128
|
if (fields === undefined) {
|
|
117
|
-
throw new Error(
|
|
129
|
+
throw new Error(
|
|
130
|
+
`${where}: 协议 ${JSON.stringify(api)} 无 compat 字段表(支持:${Object.keys(FIELDS_BY_PROTOCOL).join(', ')})`,
|
|
131
|
+
)
|
|
118
132
|
}
|
|
119
133
|
for (const [key, value] of Object.entries(compat)) {
|
|
120
134
|
const spec = fields[key]
|
package/src/config.ts
CHANGED
|
@@ -8,8 +8,9 @@
|
|
|
8
8
|
* - provider/model 均支持 extends 继承(见 inherit.ts)。
|
|
9
9
|
* @module llm-pi/config
|
|
10
10
|
*/
|
|
11
|
-
|
|
11
|
+
|
|
12
12
|
import { settingsNamespace } from '@deepseek-ai/dsh-settings'
|
|
13
|
+
import z from '@deepseek-ai/schemastery'
|
|
13
14
|
|
|
14
15
|
import { SETTINGS_NS as NS_LITERAL } from './ns.ts'
|
|
15
16
|
|
|
@@ -17,7 +18,11 @@ import { SETTINGS_NS as NS_LITERAL } from './ns.ts'
|
|
|
17
18
|
export const SETTINGS_NS = settingsNamespace(NS_LITERAL)
|
|
18
19
|
|
|
19
20
|
/** 本插件可为手写 route 提供的协议实现(与官方 PROTOCOLS 表一致)。 */
|
|
20
|
-
export const PROTOCOL_IDS = [
|
|
21
|
+
export const PROTOCOL_IDS = [
|
|
22
|
+
'openai-completions',
|
|
23
|
+
'openai-responses',
|
|
24
|
+
'anthropic-messages',
|
|
25
|
+
] as const
|
|
21
26
|
export type ProtocolId = (typeof PROTOCOL_IDS)[number]
|
|
22
27
|
|
|
23
28
|
/** pi-ai 思考档位,升级序。 */
|
|
@@ -66,7 +71,12 @@ export interface ProviderProfileConfig {
|
|
|
66
71
|
defaultMaxTokens?: number
|
|
67
72
|
defaultInput?: Modality[]
|
|
68
73
|
reasoning?: ThinkingLevel
|
|
69
|
-
thinkingBudgets?: {
|
|
74
|
+
thinkingBudgets?: {
|
|
75
|
+
minimal: number
|
|
76
|
+
low: number
|
|
77
|
+
medium: number
|
|
78
|
+
high: number
|
|
79
|
+
}
|
|
70
80
|
cacheRetention?: 'none' | 'short' | 'long'
|
|
71
81
|
transport?: 'sse' | 'websocket' | 'websocket-cached' | 'auto'
|
|
72
82
|
timeoutMs?: number
|
|
@@ -104,11 +114,19 @@ const modelEntry = z.object({
|
|
|
104
114
|
id: z.string().required().description('模型 id(发送给 provider 的标识)'),
|
|
105
115
|
extends: z
|
|
106
116
|
.string()
|
|
107
|
-
.description(
|
|
117
|
+
.description(
|
|
118
|
+
'继承源:"provider/model" 或裸 model id(随 provider 级 extends 源);缺省先查内置目录同名模型',
|
|
119
|
+
),
|
|
108
120
|
name: z.string().description('选择器显示名;缺省继承内置目录名,再退化为 id'),
|
|
109
121
|
contextWindow: z.number().step(1).min(1).description('上下文容量(覆盖继承值)'),
|
|
110
|
-
maxTokens: z
|
|
111
|
-
|
|
122
|
+
maxTokens: z
|
|
123
|
+
.number()
|
|
124
|
+
.step(1)
|
|
125
|
+
.min(1)
|
|
126
|
+
.description('输出能力上限;显式配置同时成为无 cap 请求的默认 cap'),
|
|
127
|
+
input: z
|
|
128
|
+
.array(z.union(MODALITIES))
|
|
129
|
+
.description('请求模态;缺省继承内置目录,再退化 route defaultInput'),
|
|
112
130
|
reasoningEfforts: z
|
|
113
131
|
.union([z.const(false), reasoningEfforts])
|
|
114
132
|
.description('可选 reasoning 档位:false=非推理模型;dict=档位→线值映射;缺省继承内置目录能力'),
|
|
@@ -118,14 +136,22 @@ const modelEntry = z.object({
|
|
|
118
136
|
const providerProfile = z.object({
|
|
119
137
|
extends: z
|
|
120
138
|
.string()
|
|
121
|
-
.description(
|
|
139
|
+
.description(
|
|
140
|
+
'provider 级继承:内置 provider id,提供 api/baseURL 默认值与模型 extends 的缺省查找源',
|
|
141
|
+
),
|
|
122
142
|
displayName: z.string().description('选择器显示名;缺省为 route 键'),
|
|
123
|
-
api: z
|
|
143
|
+
api: z
|
|
144
|
+
.union(PROTOCOL_IDS)
|
|
145
|
+
.description('线协议;缺省逐模型取继承值的 api,全部一致时作为 route 协议'),
|
|
124
146
|
baseURL: z.string().description('端点;缺省继承 extends 源 provider 的端点'),
|
|
125
147
|
apiKeyEnv: z.string().role('credential-ref').description('凭据引用名(凭据服务/环境变量)'),
|
|
126
148
|
headers: z.dict(z.string()).description('provider 请求头(Harness 署名头保留名优先)'),
|
|
127
149
|
compat: compatDict.description('route 级 compat 默认(逐模型按字段生效)'),
|
|
128
|
-
defaultContextWindow: z
|
|
150
|
+
defaultContextWindow: z
|
|
151
|
+
.number()
|
|
152
|
+
.step(1)
|
|
153
|
+
.min(1)
|
|
154
|
+
.description('模型与继承源都未标注时的上下文容量兜底'),
|
|
129
155
|
defaultMaxTokens: z.number().step(1).min(1).description('模型与继承源都未标注时的输出能力兜底'),
|
|
130
156
|
defaultInput: z
|
|
131
157
|
.array(z.union(MODALITIES))
|
|
@@ -146,12 +172,17 @@ const providerProfile = z.object({
|
|
|
146
172
|
.natural()
|
|
147
173
|
.description('单请求 base64 图片载荷上限字节;缺省 20MiB(rc8 起生效,旧配置免改)'),
|
|
148
174
|
retryPolicy: z.any().description('provider 重试策略(dsh-llm RetryPolicy 形状,构建期校验)'),
|
|
149
|
-
models: z
|
|
175
|
+
models: z
|
|
176
|
+
.array(modelEntry)
|
|
177
|
+
.description('本 route 的模型目录;缺省且 provider 有 extends 时继承该源全部模型'),
|
|
150
178
|
})
|
|
151
179
|
|
|
152
180
|
export const Config: z<LlmPiConfig> = z.object({
|
|
153
181
|
enabled: z.boolean().description('总开关(关闭则不注册任何 route)').default(true),
|
|
154
|
-
catalogUrl: z
|
|
182
|
+
catalogUrl: z
|
|
183
|
+
.string()
|
|
184
|
+
.description('models.dev 目录数据端点')
|
|
185
|
+
.default('https://models.dev/api.json'),
|
|
155
186
|
catalogRefreshHours: z
|
|
156
187
|
.number()
|
|
157
188
|
.description('models.dev 自动拉取间隔小时数;0 = 不自动拉取(可手动拉取或读已有缓存)')
|
package/src/discovery.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* GET {baseURL}/models、4MB 上限、署名头)与官方一致。结果不落盘。
|
|
7
7
|
* @module llm-pi/discovery
|
|
8
8
|
*/
|
|
9
|
-
import {
|
|
9
|
+
import { builtinModelIds, hasBuiltinProvider } from './catalog/builtin.ts'
|
|
10
10
|
import type { ProviderProfileConfig } from './config.ts'
|
|
11
11
|
import type { DshKit } from './resolve-dsh.ts'
|
|
12
12
|
|
|
@@ -37,7 +37,8 @@ export interface DiscoveryDeps {
|
|
|
37
37
|
|
|
38
38
|
/** 读取有界响应体:声明超长或累计超长都拒绝(对齐官方 readBounded)。 */
|
|
39
39
|
async function readBounded(kit: DshKit, response: Response, url: string): Promise<string> {
|
|
40
|
-
const oversized = () =>
|
|
40
|
+
const oversized = () =>
|
|
41
|
+
new kit.LlmError(`${url} 响应超过 ${MAX_RESPONSE_BYTES} 字节`, 'DISCOVERY_FAILED')
|
|
41
42
|
const declared = Number(response.headers.get('content-length') ?? NaN)
|
|
42
43
|
if (Number.isFinite(declared) && declared > MAX_RESPONSE_BYTES) {
|
|
43
44
|
await response.body?.cancel()
|
|
@@ -87,7 +88,12 @@ function readListing(kit: DshKit, body: unknown): DiscoveryEntry[] {
|
|
|
87
88
|
['max_tokens', 'maxTokens'],
|
|
88
89
|
] as const) {
|
|
89
90
|
const value = entry[key]
|
|
90
|
-
if (
|
|
91
|
+
if (
|
|
92
|
+
typeof value === 'number' &&
|
|
93
|
+
Number.isInteger(value) &&
|
|
94
|
+
value > 0 &&
|
|
95
|
+
out[field] === undefined
|
|
96
|
+
) {
|
|
91
97
|
out[field] = value
|
|
92
98
|
}
|
|
93
99
|
}
|
|
@@ -114,7 +120,10 @@ function catalogAnswer(kit: DshKit, source: string): DiscoveryEntry[] {
|
|
|
114
120
|
* 回答"该 provider 可服务哪些模型":extends 内置源零网络直答;
|
|
115
121
|
* 否则仅 openai 系协议走 GET {baseURL}/models;其余协议明确不支持。
|
|
116
122
|
*/
|
|
117
|
-
export async function discoverModels(
|
|
123
|
+
export async function discoverModels(
|
|
124
|
+
request: DiscoveryRequest,
|
|
125
|
+
deps: DiscoveryDeps,
|
|
126
|
+
): Promise<DiscoveryEntry[]> {
|
|
118
127
|
const { kit } = deps
|
|
119
128
|
const route: ProviderProfileConfig | undefined =
|
|
120
129
|
request.provider === undefined ? undefined : deps.configProviders()[request.provider]
|
|
@@ -130,7 +139,10 @@ export async function discoverModels(request: DiscoveryRequest, deps: DiscoveryD
|
|
|
130
139
|
}
|
|
131
140
|
const api = request.api ?? route?.api ?? 'openai-completions'
|
|
132
141
|
if (!LISTABLE_PROTOCOLS.has(api)) {
|
|
133
|
-
throw new kit.LlmError(
|
|
142
|
+
throw new kit.LlmError(
|
|
143
|
+
`协议 "${api}" 无可读取的模型清单端点;请手工录入模型`,
|
|
144
|
+
'DISCOVERY_UNSUPPORTED',
|
|
145
|
+
)
|
|
134
146
|
}
|
|
135
147
|
const url = `${baseURL.replace(/\/+$/, '')}/models`
|
|
136
148
|
const supplied = request.apiKey ?? (await deps.storedApiKey(request.provider))
|
|
@@ -139,7 +151,9 @@ export async function discoverModels(request: DiscoveryRequest, deps: DiscoveryD
|
|
|
139
151
|
const checked = kit.normalizeApiKey(supplied)
|
|
140
152
|
if (!checked.ok) {
|
|
141
153
|
throw new kit.LlmError(
|
|
142
|
-
checked.reason === 'empty'
|
|
154
|
+
checked.reason === 'empty'
|
|
155
|
+
? 'API key 为空;请在 Models 页配置或留空以匿名探测'
|
|
156
|
+
: 'API key 含有 HTTP 头无法携带的字符',
|
|
143
157
|
kit.INVALID_CREDENTIAL_CODE,
|
|
144
158
|
)
|
|
145
159
|
}
|
|
@@ -157,8 +171,13 @@ export async function discoverModels(request: DiscoveryRequest, deps: DiscoveryD
|
|
|
157
171
|
...(request.signal === undefined ? {} : { signal: request.signal }),
|
|
158
172
|
})
|
|
159
173
|
} catch (error) {
|
|
160
|
-
if (request.signal?.aborted)
|
|
161
|
-
|
|
174
|
+
if (request.signal?.aborted)
|
|
175
|
+
throw new kit.LlmError('模型发现被调用方中止', 'ABORTED', {
|
|
176
|
+
cause: error,
|
|
177
|
+
})
|
|
178
|
+
throw new kit.LlmError(`无法连接 ${url}`, 'DISCOVERY_FAILED', {
|
|
179
|
+
cause: error,
|
|
180
|
+
})
|
|
162
181
|
}
|
|
163
182
|
if (!response.ok) {
|
|
164
183
|
throw new kit.LlmError(
|
|
@@ -171,6 +190,8 @@ export async function discoverModels(request: DiscoveryRequest, deps: DiscoveryD
|
|
|
171
190
|
return readListing(kit, JSON.parse(text))
|
|
172
191
|
} catch (error) {
|
|
173
192
|
if (error instanceof kit.LlmError) throw error
|
|
174
|
-
throw new kit.LlmError(`${url} 未返回 JSON`, 'DISCOVERY_FAILED', {
|
|
193
|
+
throw new kit.LlmError(`${url} 未返回 JSON`, 'DISCOVERY_FAILED', {
|
|
194
|
+
cause: error,
|
|
195
|
+
})
|
|
175
196
|
}
|
|
176
197
|
}
|
package/src/index.ts
CHANGED
|
@@ -20,10 +20,13 @@ export const name = 'dsh-plus-llm-pi'
|
|
|
20
20
|
|
|
21
21
|
export const inject = ['llm'] as const
|
|
22
22
|
|
|
23
|
-
export {
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
export type {
|
|
24
|
+
LlmPiConfig,
|
|
25
|
+
ModelEntryConfig,
|
|
26
|
+
ProviderProfileConfig,
|
|
27
|
+
} from './config.ts'
|
|
26
28
|
export { SETTINGS_NS } from './config.ts'
|
|
29
|
+
export { Config }
|
|
27
30
|
|
|
28
31
|
export async function apply(ctx: Context, config: LlmPiConfig): Promise<void> {
|
|
29
32
|
const runtime = await startRuntime(ctx, config)
|
package/src/inherit.ts
CHANGED
|
@@ -26,13 +26,18 @@ export interface BaseResolution {
|
|
|
26
26
|
export class ExtendsError extends Error {}
|
|
27
27
|
|
|
28
28
|
/** 解析 "provider/model" 或裸 "model" 引用。 */
|
|
29
|
-
export function parseExtendsRef(raw: string): {
|
|
29
|
+
export function parseExtendsRef(raw: string): {
|
|
30
|
+
provider?: string
|
|
31
|
+
model: string
|
|
32
|
+
} {
|
|
30
33
|
const slash = raw.indexOf('/')
|
|
31
34
|
if (slash < 0) return { model: raw }
|
|
32
35
|
const provider = raw.slice(0, slash)
|
|
33
36
|
const model = raw.slice(slash + 1)
|
|
34
37
|
if (provider.length === 0 || model.length === 0 || model.includes('/')) {
|
|
35
|
-
throw new ExtendsError(
|
|
38
|
+
throw new ExtendsError(
|
|
39
|
+
`extends 引用 ${JSON.stringify(raw)} 非法:应为 "provider/model" 或 "model"`,
|
|
40
|
+
)
|
|
36
41
|
}
|
|
37
42
|
return { provider, model }
|
|
38
43
|
}
|
|
@@ -65,7 +70,10 @@ export function resolveModelBase(
|
|
|
65
70
|
if (entry.extends === undefined) {
|
|
66
71
|
if (profile.extends === undefined) return { base: {}, source: 'none' }
|
|
67
72
|
return (
|
|
68
|
-
lookup(kit, modelsDev, profile.extends, entry.id) ?? {
|
|
73
|
+
lookup(kit, modelsDev, profile.extends, entry.id) ?? {
|
|
74
|
+
base: {},
|
|
75
|
+
source: 'none',
|
|
76
|
+
}
|
|
69
77
|
)
|
|
70
78
|
}
|
|
71
79
|
const ref = parseExtendsRef(entry.extends)
|