@goodandready/dsh-model-sync 0.5.7 → 0.5.9
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/adapter-registry.js +35 -17
- package/lib/alias-resolver.js +2 -2
- package/lib/client.js +11 -11
- package/lib/generic-adapter.js +74 -9
- package/lib/history.js +5 -0
- package/lib/http.js +3 -4
- package/lib/index.js +19 -16
- package/lib/models.js +3 -5
- package/lib/reliability.js +0 -3
- package/lib/synchronizer-transfer.js +1 -1
- package/lib/synchronizer.js +12 -3
- package/package.json +6 -3
package/lib/adapter-registry.js
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import { catalogRequestError } from './reliability.js'
|
|
2
|
-
import { canUseGenericAdapter, createGenericAdapter } from './generic-adapter.js'
|
|
2
|
+
import { canUseGenericAdapter, createGenericAdapter, readBoundedText, MAX_RESPONSE_BYTES } from './generic-adapter.js'
|
|
3
3
|
import { normalizeModels, parseClinePassModels } from './models.js'
|
|
4
|
-
|
|
5
|
-
const MAX_RESPONSE_BYTES = 4 * 1024 * 1024
|
|
6
4
|
const SAFE_AUTH = new Set(['bearer', 'x-api-key', 'query-key', 'none'])
|
|
7
5
|
const SAFE_PARSERS = new Set(['openai', 'google', 'clinebot-plan'])
|
|
8
6
|
const CAPABILITIES = new Set(['vision', 'tools', 'reasoning', 'embeddings', 'code'])
|
|
@@ -99,14 +97,32 @@ function mappedRows(provider, rows, descriptor) {
|
|
|
99
97
|
}
|
|
100
98
|
|
|
101
99
|
function parseRows(provider, payload, descriptor) {
|
|
100
|
+
if (payload && (payload.error || payload.errors)) {
|
|
101
|
+
const message = typeof payload.error === 'string'
|
|
102
|
+
? payload.error
|
|
103
|
+
: payload.error?.message || (Array.isArray(payload.errors) ? payload.errors.map(e => e?.message || e).join(', ') : 'endpoint returned error payload')
|
|
104
|
+
const err = new Error(`endpoint returned error payload: ${message}`)
|
|
105
|
+
err.adapterCode = 'schema'
|
|
106
|
+
throw err
|
|
107
|
+
}
|
|
102
108
|
const parserKind = descriptor.parse || descriptor.parser
|
|
103
109
|
if (parserKind === 'clinebot-plan') {
|
|
104
110
|
const plan = payload?.data?.plan || payload?.data || payload?.plan || payload
|
|
105
111
|
const included = plan?.features?.included || plan?.includedModels || []
|
|
112
|
+
if (!Array.isArray(included)) {
|
|
113
|
+
const err = new Error('model catalog response does not contain plan features')
|
|
114
|
+
err.adapterCode = 'schema'
|
|
115
|
+
throw err
|
|
116
|
+
}
|
|
106
117
|
return parseClinePassModels(included)
|
|
107
118
|
}
|
|
108
119
|
if (parserKind === 'google') {
|
|
109
|
-
const rows =
|
|
120
|
+
const rows = payload?.models
|
|
121
|
+
if (!Array.isArray(rows)) {
|
|
122
|
+
const err = new Error('model catalog response does not contain models array')
|
|
123
|
+
err.adapterCode = 'schema'
|
|
124
|
+
throw err
|
|
125
|
+
}
|
|
110
126
|
return normalizeModels(provider, rows.map((row) => ({
|
|
111
127
|
...row,
|
|
112
128
|
id: String(row.name ?? '').replace(/^models\//, ''),
|
|
@@ -115,8 +131,13 @@ function parseRows(provider, payload, descriptor) {
|
|
|
115
131
|
maxTokens: row.outputTokenLimit,
|
|
116
132
|
})))
|
|
117
133
|
}
|
|
118
|
-
const rows = pathValue(payload, descriptor.modelsPath) ?? payload?.data ?? payload?.models ?? payload?.availableModels
|
|
119
|
-
|
|
134
|
+
const rows = pathValue(payload, descriptor.modelsPath) ?? (Array.isArray(payload) ? payload : (payload?.data ?? payload?.models ?? payload?.availableModels))
|
|
135
|
+
if (!Array.isArray(rows)) {
|
|
136
|
+
const err = new Error('model catalog response does not contain models array')
|
|
137
|
+
err.adapterCode = 'schema'
|
|
138
|
+
throw err
|
|
139
|
+
}
|
|
140
|
+
return normalizeModels(provider, mappedRows(provider, rows, descriptor))
|
|
120
141
|
}
|
|
121
142
|
|
|
122
143
|
function privateHttpHost(hostname) {
|
|
@@ -214,17 +235,14 @@ async function fetchSpecific(profile, descriptor, options) {
|
|
|
214
235
|
}
|
|
215
236
|
const respEtag = response.headers?.get?.('etag') || undefined
|
|
216
237
|
const respLastMod = response.headers?.get?.('last-modified') || undefined
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
const error = new Error('model catalog response is too large')
|
|
226
|
-
error.adapterCode = 'schema'
|
|
227
|
-
throw error
|
|
238
|
+
let text
|
|
239
|
+
try {
|
|
240
|
+
text = await readBoundedText(response, MAX_RESPONSE_BYTES)
|
|
241
|
+
} catch (err) {
|
|
242
|
+
if (err.message === 'model catalog response is too large') {
|
|
243
|
+
err.adapterCode = 'schema'
|
|
244
|
+
}
|
|
245
|
+
throw err
|
|
228
246
|
}
|
|
229
247
|
try { payload = JSON.parse(text) } catch {
|
|
230
248
|
const error = new Error('model catalog response is not valid JSON')
|
package/lib/alias-resolver.js
CHANGED
|
@@ -15,7 +15,7 @@ export function validateAliasName(name) {
|
|
|
15
15
|
return /^@?[a-zA-Z0-9_\-.]{1,64}$/.test(clean)
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
function modelScoreForCode(
|
|
18
|
+
function modelScoreForCode(model) {
|
|
19
19
|
let score = 0
|
|
20
20
|
const id = String(model?.id ?? '').toLowerCase()
|
|
21
21
|
const name = String(model?.name ?? '').toLowerCase()
|
|
@@ -208,7 +208,7 @@ export function resolveAlias(aliasName, {
|
|
|
208
208
|
if (strategy === 'best-code') {
|
|
209
209
|
const scored = available.map((item) => ({
|
|
210
210
|
...item,
|
|
211
|
-
score: modelScoreForCode(item.
|
|
211
|
+
score: modelScoreForCode(item.model),
|
|
212
212
|
}))
|
|
213
213
|
scored.sort((a, b) => b.score - a.score)
|
|
214
214
|
const best = scored[0]
|
package/lib/client.js
CHANGED
|
@@ -25,23 +25,23 @@ window.__ModuleLoader__.load({
|
|
|
25
25
|
'.dms-card-body{border-top:1px solid var(--dsw-alias-border-l2);margin:0 16px;padding-bottom:8px;display:flex;flex-direction:column;gap:10px;box-sizing:border-box}' +
|
|
26
26
|
'.dms-wrap{display:flex;flex-direction:column;gap:14px;max-width:760px;box-sizing:border-box;color:var(--dsw-alias-label-primary,inherit)}' +
|
|
27
27
|
'.dms-wrap>p{margin:0;color:var(--dsw-alias-label-secondary);font-size:12px;line-height:1.55}' +
|
|
28
|
-
'.dms-toolbar,.dms-section{display:flex;flex-direction:row;align-items:center;flex-wrap:wrap;gap:10px;padding:13px 14px;border:1px solid var(--dsw-alias-border-l2
|
|
28
|
+
'.dms-toolbar,.dms-section{display:flex;flex-direction:row;align-items:center;flex-wrap:wrap;gap:10px;padding:13px 14px;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;background:var(--dsw-alias-bg-layer-1,transparent);box-sizing:border-box}' +
|
|
29
29
|
'.dms-toolbar>select{flex:1 1 190px;min-width:190px}' +
|
|
30
30
|
'.dms-toolbar>.dms-actions{flex-basis:100%}' +
|
|
31
31
|
'.dms-toolbar>label{display:inline-flex;align-items:flex-start;gap:7px;font-size:12px;line-height:1.35;color:var(--dsw-alias-label-secondary)}' +
|
|
32
32
|
'.dms-toolbar>select,.dms-editor input,.dms-editor textarea,.dms-input{box-sizing:border-box;min-width:0;background:var(--dsw-specific-input-major,var(--dsw-alias-bg-layer-2,transparent));border:1px solid var(--dsw-alias-border-l1,var(--dsw-alias-border-l2));border-radius:6px;color:var(--dsw-alias-label-primary);font:inherit;padding:7px 9px}' +
|
|
33
33
|
'.dms-toolbar>label input{accent-color:var(--dsw-alias-brand-primary,var(--dsw-alias-button-info-fill))}' +
|
|
34
34
|
'.dms-actions{display:flex;align-items:center;flex-wrap:wrap;gap:7px}' +
|
|
35
|
-
'.dms-wrap>.dms-actions{padding:10px 14px;border:1px solid var(--dsw-alias-border-l2
|
|
36
|
-
'.dms-btn{margin:0!important;border:1px solid var(--dsw-alias-border-l2
|
|
35
|
+
'.dms-wrap>.dms-actions{padding:10px 14px;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;background:var(--dsw-alias-bg-layer-1,transparent)}' +
|
|
36
|
+
'.dms-btn{margin:0!important;border:1px solid var(--dsw-alias-border-l2);border-radius:6px;background:transparent;color:var(--dsw-alias-label-primary);font:inherit;font-size:12px;line-height:1.35;padding:6px 10px;cursor:pointer;white-space:normal}' +
|
|
37
37
|
'.dms-btn:hover:not(:disabled){border-color:var(--dsw-alias-brand-primary);background:var(--dsw-alias-bg-layer-2)}' +
|
|
38
38
|
'.dms-btn:disabled{opacity:.45;cursor:default}' +
|
|
39
39
|
'.dms-section>strong{display:block;font-size:13px;font-weight:650}' +
|
|
40
40
|
'.dms-provider-wrap{margin:0 0 7px}' +
|
|
41
|
-
'.dms-provider{display:grid!important;grid-template-columns:minmax(0,1fr) auto auto;align-items:center;gap:10px;padding:10px 11px;margin:0;border:1px solid var(--dsw-alias-border-l2
|
|
41
|
+
'.dms-provider{display:grid!important;grid-template-columns:minmax(0,1fr) auto auto;align-items:center;gap:10px;padding:10px 11px;margin:0;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-layer-2,transparent)}' +
|
|
42
42
|
'.dms-provider>span{min-width:0;overflow-wrap:anywhere;line-height:1.4}' +
|
|
43
43
|
'.dms-section>div[style]{padding:8px 0;border-top:1px solid var(--dsw-alias-border-l2)}' +
|
|
44
|
-
'.dms-editor{display:flex;flex-direction:column;align-items:stretch;gap:10px;padding:13px 14px;border:1px solid var(--dsw-alias-border-l2
|
|
44
|
+
'.dms-editor{display:flex;flex-direction:column;align-items:stretch;gap:10px;padding:13px 14px;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;background:var(--dsw-alias-bg-layer-1,transparent);box-sizing:border-box;border-color:var(--dsw-alias-brand-primary,var(--dsw-alias-border-l2))}' +
|
|
45
45
|
'.dms-editor strong{font-size:13px}' +
|
|
46
46
|
'.dms-editor label{display:block;gap:5px;margin-top:8px;color:var(--dsw-alias-label-secondary);font-size:12px}' +
|
|
47
47
|
'.dms-editor label input,.dms-editor label textarea{width:100%}' +
|
|
@@ -53,10 +53,10 @@ window.__ModuleLoader__.load({
|
|
|
53
53
|
'.dms-grid-2{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:12px}' +
|
|
54
54
|
'.dms-badge{font-size:11px;padding:2px 8px;border-radius:999px;border:1px solid var(--dsw-alias-border-l2);display:inline-flex;align-items:center;gap:4px;font-weight:500}' +
|
|
55
55
|
'.dms-badge-ok{border-color:var(--dsw-alias-state-success-primary);color:var(--dsw-alias-state-success-primary);background:var(--dsw-alias-state-success-secondary,var(--dsw-alias-bg-layer-2))}' +
|
|
56
|
-
'.dms-badge-warn{border-color:var(--dsw-alias-state-
|
|
56
|
+
'.dms-badge-warn{border-color:var(--dsw-alias-state-warn-primary);color:var(--dsw-alias-state-warn-primary);background:var(--dsw-alias-state-warn-secondary,var(--dsw-alias-bg-layer-2))}' +
|
|
57
57
|
'.dms-badge-bad{border-color:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-state-error-primary);background:var(--dsw-alias-state-error-secondary,var(--dsw-alias-bg-layer-2))}' +
|
|
58
58
|
'.dms-input{height:34px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);border-radius:6px;padding:0 10px;font-size:13px;box-sizing:border-box}' +
|
|
59
|
-
'.dms-input:focus{outline:none;border-color:var(--dsw-alias-
|
|
59
|
+
'.dms-input:focus{outline:none;border-color:var(--dsw-alias-brand-primary)}' +
|
|
60
60
|
'.dms-btn-primary{background:var(--dsw-alias-label-primary)!important;color:var(--dsw-alias-bg-layer-3)!important;border-color:transparent!important;font-weight:600}' +
|
|
61
61
|
'.dms-btn-primary:hover:not(:disabled){opacity:0.88}' +
|
|
62
62
|
'.dms-btn-danger{color:var(--dsw-alias-state-error-primary)!important;border-color:var(--dsw-alias-state-error-primary)!important}' +
|
|
@@ -517,7 +517,7 @@ window.__ModuleLoader__.load({
|
|
|
517
517
|
}, updaterChecking ? t.checkingUpdates : (updaterStatus ? t.upToDate : t.checkForUpdates))
|
|
518
518
|
)
|
|
519
519
|
),
|
|
520
|
-
updaterNotice ? React.createElement('div', { style: { color: 'var(--dsw-alias-state-success)', marginTop: 8, fontSize: 12 } }, updaterNotice) : null,
|
|
520
|
+
updaterNotice ? React.createElement('div', { style: { color: 'var(--dsw-alias-state-success-primary)', marginTop: 8, fontSize: 12 } }, updaterNotice) : null,
|
|
521
521
|
updaterError ? React.createElement('div', { style: { color: 'var(--dsw-alias-state-error-primary)', marginTop: 8, fontSize: 12 } }, updaterError) : null
|
|
522
522
|
)
|
|
523
523
|
}
|
|
@@ -1117,7 +1117,7 @@ window.__ModuleLoader__.load({
|
|
|
1117
1117
|
return React.createElement('div', { key: p.name, style: { display: 'flex', flexDirection: 'column', gap: 4, padding: '8px 10px', border: '1px solid var(--dsw-alias-border-l2)', borderRadius: 6, background: 'var(--dsw-alias-bg-layer-2)' } },
|
|
1118
1118
|
React.createElement('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' } },
|
|
1119
1119
|
React.createElement('div', { style: { display: 'flex', gap: 8, alignItems: 'center' } },
|
|
1120
|
-
React.createElement('strong', { style: { fontFamily: 'monospace', fontSize: 13, color: 'var(--dsw-alias-
|
|
1120
|
+
React.createElement('strong', { style: { fontFamily: 'monospace', fontSize: 13, color: 'var(--dsw-alias-brand-primary)' } }, p.name),
|
|
1121
1121
|
React.createElement('span', { style: { fontSize: 12, color: 'var(--dsw-alias-label-secondary)' } }, p.label)
|
|
1122
1122
|
),
|
|
1123
1123
|
button(isResolving ? t.resolvingAlias : t.resolveAlias, () => resolveSmartAlias(p.name), 'secondary')
|
|
@@ -1318,8 +1318,8 @@ window.__ModuleLoader__.load({
|
|
|
1318
1318
|
React.createElement('input', { type: 'checkbox', checked: selectionDraft.includes(model.id), onChange: (e) => setSelectionDraft((current) => e.target.checked ? [...current, model.id] : current.filter((id) => id !== model.id)) }),
|
|
1319
1319
|
` ${model.name ?? model.id} (${model.id})`,
|
|
1320
1320
|
isNewModel(model, row) ? React.createElement('span', { style: { marginLeft: 6, fontSize: 11, padding: '1px 5px', borderRadius: 4, background: 'var(--dsw-alias-state-business-primary)', color: 'white' } }, 'NEW') : null,
|
|
1321
|
-
deprecationDays !== null ? React.createElement('span', { style: { marginLeft: 6, fontSize: 10, padding: '1px 5px', borderRadius: 4, background: 'var(--dsw-alias-state-
|
|
1322
|
-
(model.contextWindow && maxContext > 0 && model.contextWindow >= maxContext) ? React.createElement('span', { style: { marginLeft: 4, fontSize: 10, padding: '1px 4px', borderRadius: 3, background: 'var(--dsw-alias-state-
|
|
1321
|
+
deprecationDays !== null ? React.createElement('span', { style: { marginLeft: 6, fontSize: 10, padding: '1px 5px', borderRadius: 4, background: 'var(--dsw-alias-state-warn-primary)', color: 'white' } }, deprecationDays > 0 ? t.deprecatesIn.replace('%d', deprecationDays) : t.deprecated) : null,
|
|
1322
|
+
(model.contextWindow && maxContext > 0 && model.contextWindow >= maxContext) ? React.createElement('span', { style: { marginLeft: 4, fontSize: 10, padding: '1px 4px', borderRadius: 3, background: 'var(--dsw-alias-state-warn-primary)', color: 'white' } }, '★') : null,
|
|
1323
1323
|
(model.pricing && minPrice !== Infinity && model.pricing.inputPerToken === minPrice) ? React.createElement('span', { style: { marginLeft: 4, fontSize: 10, padding: '1px 4px', borderRadius: 3, background: 'var(--dsw-alias-state-success-primary)', color: 'white' } }, '$') : null,
|
|
1324
1324
|
model.tags && model.tags.length ? React.createElement('small', { style: { marginLeft: 6, color: 'var(--dsw-alias-label-secondary)' } }, model.tags.join(', ')) : null,
|
|
1325
1325
|
model.pricing ? React.createElement('small', { style: { marginLeft: 6, color: 'var(--dsw-alias-label-secondary)', fontSize: 11 } }, `in $${(model.pricing.inputPerToken * 1000000).toFixed(2)}/1M` + (model.pricing.cacheReadPerToken !== undefined ? ` / cache $${(model.pricing.cacheReadPerToken * 1000000).toFixed(2)}` : '') + (model.pricing.outputPerToken !== undefined ? ` / out $${(model.pricing.outputPerToken * 1000000).toFixed(2)}` : '')) : null,
|
package/lib/generic-adapter.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { catalogRequestError } from './reliability.js'
|
|
2
2
|
import { normalizeModels } from './models.js'
|
|
3
3
|
|
|
4
|
-
const MAX_RESPONSE_BYTES = 4 * 1024 * 1024
|
|
4
|
+
export const MAX_RESPONSE_BYTES = 4 * 1024 * 1024
|
|
5
5
|
const SUPPORTED_APIS = new Set(['openai-completions', 'openai-responses'])
|
|
6
6
|
|
|
7
7
|
function endpointFor(baseURL) {
|
|
@@ -10,21 +10,86 @@ function endpointFor(baseURL) {
|
|
|
10
10
|
return base.endsWith('/models') ? base : `${base}/models`
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
async function
|
|
13
|
+
export async function readBoundedText(response, maxBytes = MAX_RESPONSE_BYTES) {
|
|
14
14
|
const declared = Number(response.headers?.get?.('content-length') ?? NaN)
|
|
15
|
-
if (Number.isFinite(declared) && declared >
|
|
15
|
+
if (Number.isFinite(declared) && declared > maxBytes) {
|
|
16
16
|
throw new Error('model catalog response is too large')
|
|
17
17
|
}
|
|
18
|
-
|
|
19
|
-
if (
|
|
20
|
-
|
|
18
|
+
|
|
19
|
+
if (response.body && typeof response.body[Symbol.asyncIterator] === 'function') {
|
|
20
|
+
const chunks = []
|
|
21
|
+
let totalBytes = 0
|
|
22
|
+
for await (const chunk of response.body) {
|
|
23
|
+
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)
|
|
24
|
+
totalBytes += buf.byteLength
|
|
25
|
+
if (totalBytes > maxBytes) {
|
|
26
|
+
if (typeof response.body.cancel === 'function') {
|
|
27
|
+
try { await response.body.cancel('response too large') } catch (_err) { /* best-effort */ }
|
|
28
|
+
} else if (typeof response.body.destroy === 'function') {
|
|
29
|
+
try { response.body.destroy() } catch (_err) { /* best-effort */ }
|
|
30
|
+
}
|
|
31
|
+
throw new Error('model catalog response is too large')
|
|
32
|
+
}
|
|
33
|
+
chunks.push(buf)
|
|
34
|
+
}
|
|
35
|
+
return Buffer.concat(chunks).toString('utf8')
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (response.body && typeof response.body.getReader === 'function') {
|
|
39
|
+
const reader = response.body.getReader()
|
|
40
|
+
const chunks = []
|
|
41
|
+
let totalBytes = 0
|
|
42
|
+
try {
|
|
43
|
+
while (true) {
|
|
44
|
+
const { done, value } = await reader.read()
|
|
45
|
+
if (done) break
|
|
46
|
+
if (value) {
|
|
47
|
+
const buf = Buffer.isBuffer(value) ? value : Buffer.from(value)
|
|
48
|
+
totalBytes += buf.byteLength
|
|
49
|
+
if (totalBytes > maxBytes) {
|
|
50
|
+
try { await reader.cancel('response too large') } catch (_err) { /* best-effort */ }
|
|
51
|
+
throw new Error('model catalog response is too large')
|
|
52
|
+
}
|
|
53
|
+
chunks.push(buf)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
} finally {
|
|
57
|
+
try { reader.releaseLock?.() } catch (_err) { /* best-effort */ }
|
|
58
|
+
}
|
|
59
|
+
return Buffer.concat(chunks).toString('utf8')
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (typeof response.text === 'function') {
|
|
63
|
+
const text = await response.text()
|
|
64
|
+
if (Buffer.byteLength(text, 'utf8') > maxBytes) {
|
|
65
|
+
throw new Error('model catalog response is too large')
|
|
66
|
+
}
|
|
67
|
+
return text
|
|
21
68
|
}
|
|
22
|
-
|
|
69
|
+
|
|
70
|
+
return ''
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function readBody(response) {
|
|
74
|
+
return readBoundedText(response, MAX_RESPONSE_BYTES)
|
|
23
75
|
}
|
|
24
76
|
|
|
25
77
|
function rowsFrom(payload) {
|
|
26
|
-
|
|
27
|
-
|
|
78
|
+
if (payload && (payload.error || payload.errors)) {
|
|
79
|
+
const message = typeof payload.error === 'string'
|
|
80
|
+
? payload.error
|
|
81
|
+
: payload.error?.message || (Array.isArray(payload.errors) ? payload.errors.map(e => e?.message || e).join(', ') : 'endpoint returned error payload')
|
|
82
|
+
const err = new Error(`endpoint returned error payload: ${message}`)
|
|
83
|
+
err.adapterCode = 'schema'
|
|
84
|
+
throw err
|
|
85
|
+
}
|
|
86
|
+
const rows = Array.isArray(payload) ? payload : (payload?.data ?? payload?.models ?? payload?.availableModels)
|
|
87
|
+
if (!Array.isArray(rows)) {
|
|
88
|
+
const err = new Error('model catalog response does not contain models array')
|
|
89
|
+
err.adapterCode = 'schema'
|
|
90
|
+
throw err
|
|
91
|
+
}
|
|
92
|
+
return rows
|
|
28
93
|
}
|
|
29
94
|
|
|
30
95
|
export function canUseGenericAdapter(profile) {
|
package/lib/history.js
CHANGED
|
@@ -27,6 +27,11 @@ function cloneModel(provider, model) {
|
|
|
27
27
|
if (normalized.pricing) compact.pricing = normalized.pricing
|
|
28
28
|
if (normalized.contextWindow !== undefined) compact.contextWindow = normalized.contextWindow
|
|
29
29
|
if (normalized.maxTokens !== undefined) compact.maxTokens = normalized.maxTokens
|
|
30
|
+
if (normalized.input !== undefined) compact.input = normalized.input
|
|
31
|
+
if (normalized.compat !== undefined) compact.compat = normalized.compat
|
|
32
|
+
if (normalized.tags !== undefined) compact.tags = normalized.tags
|
|
33
|
+
if (normalized.aliases !== undefined && normalized.aliases.length > 0) compact.aliases = normalized.aliases
|
|
34
|
+
if (normalized.lifecycle !== undefined) compact.lifecycle = normalized.lifecycle
|
|
30
35
|
return compact
|
|
31
36
|
}
|
|
32
37
|
|
package/lib/http.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
import crypto from 'node:crypto'
|
|
1
2
|
import { validateAliasName } from './alias-resolver.js'
|
|
3
|
+
import { validatePolicy } from './policy.js'
|
|
4
|
+
|
|
2
5
|
function normalizeWebhookTestRequest(body) {
|
|
3
6
|
if (!body || typeof body !== "object") throw Object.assign(new Error("body is required"), { code: "INVALID_REQUEST" })
|
|
4
7
|
if (typeof body.url !== "string" || !body.url.trim()) throw Object.assign(new Error("url is required"), { code: "INVALID_REQUEST" })
|
|
@@ -30,8 +33,6 @@ function normalizeWebhookTestRequest(body) {
|
|
|
30
33
|
secret: typeof body.secret === "string" ? body.secret.trim() : "",
|
|
31
34
|
}
|
|
32
35
|
}
|
|
33
|
-
import crypto from 'node:crypto'
|
|
34
|
-
import { validatePolicy } from './policy.js'
|
|
35
36
|
|
|
36
37
|
const BASE = '/dsh-model-sync'
|
|
37
38
|
|
|
@@ -82,8 +83,6 @@ export function trusted(req, { requireLoopback = false } = {}) {
|
|
|
82
83
|
}
|
|
83
84
|
// If sec-fetch-site is same-origin or same-site, allow
|
|
84
85
|
if (fetchSite === 'same-origin' || fetchSite === 'same-site') return true
|
|
85
|
-
// If sec-fetch-mode is present but origin/referer missing on a cross-site request, reject
|
|
86
|
-
if (req.headers?.['sec-fetch-mode'] && !host) return false
|
|
87
86
|
// For requests requiring loopback or POST without any browser origin/referer headers, require explicit loopback
|
|
88
87
|
if ((requireLoopback || req?.method === 'POST') && !origin && !referer) {
|
|
89
88
|
if (!isLoopbackAddress(remoteAddress ?? (req?.headers?.host ? req.headers.host.split(':')[0] : ''))) return false
|
package/lib/index.js
CHANGED
|
@@ -44,7 +44,7 @@ export const Config = Schema.object({
|
|
|
44
44
|
title: Schema.string().required(),
|
|
45
45
|
message: Schema.string().required(),
|
|
46
46
|
providers: Schema.array(Schema.object({}).loose()).default([]),
|
|
47
|
-
}).loose()).default([]),
|
|
47
|
+
}).loose()).default([]).volatile(),
|
|
48
48
|
staleGraceRuns: Schema.number().default(2).volatile(),
|
|
49
49
|
lifecycleRetentionRuns: Schema.number().default(20).volatile(),
|
|
50
50
|
lifecycleRevision: Schema.number().default(0).volatile(),
|
|
@@ -54,14 +54,14 @@ export const Config = Schema.object({
|
|
|
54
54
|
firstMissingAt: Schema.number().default(0),
|
|
55
55
|
lastSeenAt: Schema.number().default(0),
|
|
56
56
|
lastObservedRun: Schema.number().default(0),
|
|
57
|
-
}).loose())).default({}),
|
|
57
|
+
}).loose())).default({}).volatile(),
|
|
58
58
|
history: Schema.array(Schema.object({
|
|
59
59
|
id: Schema.string().required(),
|
|
60
60
|
version: Schema.number().required(),
|
|
61
61
|
startedAt: Schema.number().required(),
|
|
62
62
|
finishedAt: Schema.number().required(),
|
|
63
63
|
providers: Schema.array(Schema.object({}).loose()).default([]),
|
|
64
|
-
}).loose()).default([]),
|
|
64
|
+
}).loose()).default([]).volatile(),
|
|
65
65
|
providers: Schema.array(Schema.object({
|
|
66
66
|
provider: Schema.string().required(),
|
|
67
67
|
enabled: Schema.boolean().default(true),
|
|
@@ -69,7 +69,7 @@ export const Config = Schema.object({
|
|
|
69
69
|
jitterMinutes: Schema.number().default(0),
|
|
70
70
|
ttlMinutes: Schema.number().default(0),
|
|
71
71
|
autoApply: Schema.boolean().default(false),
|
|
72
|
-
})).default([]),
|
|
72
|
+
})).default([]).volatile(),
|
|
73
73
|
aliases: Schema.dict(Schema.object({
|
|
74
74
|
provider: Schema.string(),
|
|
75
75
|
model: Schema.string(),
|
|
@@ -79,7 +79,7 @@ export const Config = Schema.object({
|
|
|
79
79
|
})),
|
|
80
80
|
strategy: Schema.string(),
|
|
81
81
|
updatedAt: Schema.number().default(0),
|
|
82
|
-
}).loose()).default({}),
|
|
82
|
+
}).loose()).default({}).volatile(),
|
|
83
83
|
modelSelections: Schema.dict(Schema.array(Schema.string())).default({}).volatile(),
|
|
84
84
|
adapterRegistry: Schema.dict(Schema.object({
|
|
85
85
|
enabled: Schema.boolean().default(true),
|
|
@@ -90,7 +90,7 @@ export const Config = Schema.object({
|
|
|
90
90
|
fields: Schema.any().default({}),
|
|
91
91
|
capabilityMap: Schema.any().default({}),
|
|
92
92
|
headers: Schema.dict(Schema.string()).default({}),
|
|
93
|
-
}).loose()).default({}),
|
|
93
|
+
}).loose()).default({}).volatile(),
|
|
94
94
|
modelPolicies: Schema.dict(Schema.object({
|
|
95
95
|
include: Schema.array(Schema.string()).default([]),
|
|
96
96
|
exclude: Schema.array(Schema.string()).default([]),
|
|
@@ -100,7 +100,7 @@ export const Config = Schema.object({
|
|
|
100
100
|
maxPricePerMillion: Schema.number(),
|
|
101
101
|
enableContextFilter: Schema.boolean().default(false),
|
|
102
102
|
minContextTokens: Schema.number(),
|
|
103
|
-
}).loose()).default({}),
|
|
103
|
+
}).loose()).default({}).volatile(),
|
|
104
104
|
// Full catalogs are cached separately from the allowlist applied to DSH.
|
|
105
105
|
// This keeps the model picker useful after a restart without widening the
|
|
106
106
|
// active llm-pi-ai catalog.
|
|
@@ -108,7 +108,7 @@ export const Config = Schema.object({
|
|
|
108
108
|
provider: Schema.string().required(),
|
|
109
109
|
id: Schema.string().required(),
|
|
110
110
|
name: Schema.string().required(),
|
|
111
|
-
}).loose())).default({}),
|
|
111
|
+
}).loose())).default({}).volatile(),
|
|
112
112
|
})
|
|
113
113
|
|
|
114
114
|
// DSH serves a namespace's settings form from the volatile fields of its profile
|
|
@@ -123,6 +123,7 @@ export function plainConfig(value) {
|
|
|
123
123
|
}
|
|
124
124
|
|
|
125
125
|
export function apply(ctx, config) {
|
|
126
|
+
let liveConfig = undefined
|
|
126
127
|
const baseConfig = structuredClone(plainConfig(config || {}))
|
|
127
128
|
const resolveConfig = (value) => {
|
|
128
129
|
try {
|
|
@@ -134,7 +135,7 @@ export function apply(ctx, config) {
|
|
|
134
135
|
return liveConfig ?? baseConfig
|
|
135
136
|
}
|
|
136
137
|
}
|
|
137
|
-
|
|
138
|
+
liveConfig = resolveConfig(baseConfig)
|
|
138
139
|
let reconfigure = () => {}
|
|
139
140
|
let saveConfigImpl = async (patch) => {
|
|
140
141
|
const plainPatch = plainConfig(patch)
|
|
@@ -146,14 +147,16 @@ export function apply(ctx, config) {
|
|
|
146
147
|
ctx.logger.debug('[dsh-model-sync] settings service read failed:', err)
|
|
147
148
|
}
|
|
148
149
|
}
|
|
149
|
-
if (settings
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
150
|
+
if (!settings || typeof settings.update !== 'function') {
|
|
151
|
+
throw Object.assign(new Error('dsh-model-sync settings are not writable'), { code: 'CONFIG_UNAVAILABLE' })
|
|
152
|
+
}
|
|
153
|
+
try {
|
|
154
|
+
await settings.update(NS, plainPatch)
|
|
155
|
+
} catch (err) {
|
|
156
|
+
if (typeof ctx.logger?.warn === 'function') {
|
|
157
|
+
ctx.logger.warn('[dsh-model-sync] settings.update failed:', err)
|
|
156
158
|
}
|
|
159
|
+
throw err
|
|
157
160
|
}
|
|
158
161
|
liveConfig = resolveConfig({ ...liveConfig, ...plainPatch })
|
|
159
162
|
reconfigure()
|
package/lib/models.js
CHANGED
|
@@ -146,11 +146,8 @@ function normalizeCapabilities(row) {
|
|
|
146
146
|
if (!result.code && (/(^|[_-])(coder|code|starcoder|codellama)($|[_-])/i.test(idStr) || idStr.includes('coder') || idStr.includes('code-'))) {
|
|
147
147
|
result.code = true
|
|
148
148
|
}
|
|
149
|
-
if (!result.vision && (/(^|[_-])(vision|vl
|
|
150
|
-
|
|
151
|
-
if (/(^|[_-])(vision|vl)($|[_-])/i.test(idStr) || idStr.includes('-vl') || idStr.includes('-vision')) {
|
|
152
|
-
result.vision = true
|
|
153
|
-
}
|
|
149
|
+
if (!result.vision && (/(^|[_-])(vision|vl)($|[_-])/i.test(idStr) || idStr.includes('-vl') || idStr.includes('-vision'))) {
|
|
150
|
+
result.vision = true
|
|
154
151
|
}
|
|
155
152
|
|
|
156
153
|
return Object.keys(result).length > 0 ? result : undefined
|
|
@@ -276,6 +273,7 @@ function normalizePricing(row) {
|
|
|
276
273
|
}
|
|
277
274
|
if (typeof source.currency === 'string' && source.currency.trim()) pricing.currency = source.currency.trim()
|
|
278
275
|
if (typeof source.unit === 'string' && source.unit.trim()) pricing.unit = source.unit.trim()
|
|
276
|
+
if (typeof source.source === 'string' && source.source.trim()) pricing.source = source.source.trim()
|
|
279
277
|
return pricing
|
|
280
278
|
}
|
|
281
279
|
|
package/lib/reliability.js
CHANGED
|
@@ -110,13 +110,11 @@ export async function retryWithBackoff(operation, {
|
|
|
110
110
|
onRetry,
|
|
111
111
|
} = {}) {
|
|
112
112
|
const total = Math.max(1, Math.floor(Number(attempts) || 1))
|
|
113
|
-
let lastError
|
|
114
113
|
for (let attempt = 1; attempt <= total; attempt += 1) {
|
|
115
114
|
if (signal?.aborted) throw signal.reason ?? abortError()
|
|
116
115
|
try {
|
|
117
116
|
return await operation({ attempt })
|
|
118
117
|
} catch (error) {
|
|
119
|
-
lastError = error
|
|
120
118
|
if (attempt >= total || !isRetryableError(error)) throw error
|
|
121
119
|
const exponential = Math.max(0, Number(baseDelayMs) || 0) * (2 ** (attempt - 1))
|
|
122
120
|
// Full jitter: randomized between [0.5, 1.0] of calculated exponential backoff
|
|
@@ -130,7 +128,6 @@ export async function retryWithBackoff(operation, {
|
|
|
130
128
|
await delay(delayMs, signal)
|
|
131
129
|
}
|
|
132
130
|
}
|
|
133
|
-
throw lastError
|
|
134
131
|
}
|
|
135
132
|
|
|
136
133
|
export async function mapWithConcurrency(items, worker, concurrency = 4) {
|
|
@@ -54,7 +54,7 @@ export function createConfigTransfer({
|
|
|
54
54
|
importedProviders.push(pname)
|
|
55
55
|
}
|
|
56
56
|
if (Array.isArray(pdata.selectedModels)) {
|
|
57
|
-
currentSelections[pname] = pdata.selectedModels.filter((m) => typeof m === 'string')
|
|
57
|
+
currentSelections[pname] = [...new Set(pdata.selectedModels.filter((m) => typeof m === 'string' && m.trim()).map((m) => m.trim()))].slice(0, 1000)
|
|
58
58
|
importedProviders.push(pname)
|
|
59
59
|
}
|
|
60
60
|
}
|
package/lib/synchronizer.js
CHANGED
|
@@ -159,12 +159,18 @@ export function createModelSynchronizer(ctx, { getConfig = () => ({}), saveConfi
|
|
|
159
159
|
recordCredentialOutcome(row.provider, { status: 'ok', retries })
|
|
160
160
|
if (result && result.notModified) {
|
|
161
161
|
const config = getConfig() ?? {}
|
|
162
|
-
const
|
|
162
|
+
const existing = providerHttpMeta.get(row.provider)
|
|
163
|
+
if (existing && (result.etag || result.lastModified)) {
|
|
164
|
+
if (result.etag) existing.etag = result.etag
|
|
165
|
+
if (result.lastModified) existing.lastModified = result.lastModified
|
|
166
|
+
}
|
|
167
|
+
const memoryModels = existing?.models
|
|
168
|
+
const cached = normalizeModels(row.provider, memoryModels ?? config.modelCatalogs?.[row.provider] ?? row.models ?? [])
|
|
163
169
|
return { provider: row.provider, status: 'ok', models: cached, retries, notModified: true }
|
|
164
170
|
}
|
|
165
171
|
const models = Array.isArray(result) ? result : []
|
|
166
172
|
if (result?.__httpMeta) {
|
|
167
|
-
providerHttpMeta.set(row.provider, result.__httpMeta)
|
|
173
|
+
providerHttpMeta.set(row.provider, { ...result.__httpMeta, models })
|
|
168
174
|
}
|
|
169
175
|
return { provider: row.provider, status: 'ok', models, retries }
|
|
170
176
|
} catch (error) {
|
|
@@ -332,7 +338,10 @@ export function createModelSynchronizer(ctx, { getConfig = () => ({}), saveConfi
|
|
|
332
338
|
const advertised = selectedModels.length
|
|
333
339
|
? policyModels.filter((model) => selectedModels.includes(model.id))
|
|
334
340
|
: policyModels
|
|
335
|
-
const
|
|
341
|
+
const policyExcludedIds = current
|
|
342
|
+
.filter((model) => !filterModels([model], policy).length || (selectedModels.length && !selectedModels.includes(model.id)))
|
|
343
|
+
.map((model) => model.id)
|
|
344
|
+
const explicitRemoveIds = new Set([...lifecycle.removedIds, ...policyExcludedIds])
|
|
336
345
|
const reconciliation = reconcileModels(row.provider, current, advertised, { removeMissing: false, removeIds: explicitRemoveIds })
|
|
337
346
|
results.push({
|
|
338
347
|
...discovered,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-model-sync",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.9",
|
|
4
4
|
"description": "Automatic model catalog synchronization for API-key DeepSeek Harness providers.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"deepseek-harness",
|
|
@@ -53,7 +53,10 @@
|
|
|
53
53
|
"license": "MIT",
|
|
54
54
|
"peerDependencies": {
|
|
55
55
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
56
|
-
"@deepseek-ai/
|
|
57
|
-
"@deepseek-ai/dsh-
|
|
56
|
+
"@deepseek-ai/dsh-credentials": "^0.1.7-rc.2 || ^0.2.0-rc.1",
|
|
57
|
+
"@deepseek-ai/dsh-host-webserver": "^0.1.7-rc.2 || ^0.2.0-rc.1",
|
|
58
|
+
"@deepseek-ai/dsh-llm": "^0.1.7-rc.2 || ^0.2.0-rc.1",
|
|
59
|
+
"@deepseek-ai/dsh-settings": "^0.1.7-rc.2 || ^0.2.0-rc.1",
|
|
60
|
+
"@deepseek-ai/schemastery": "^3.18.4"
|
|
58
61
|
}
|
|
59
62
|
}
|