@goodandready/dsh-model-sync 0.5.6 → 0.5.8
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 +9 -14
- package/lib/alias-resolver.js +24 -10
- package/lib/client.js +11 -11
- package/lib/diff-notifier.js +1 -1
- package/lib/generic-adapter.js +59 -7
- package/lib/http.js +3 -4
- package/lib/models.js +2 -5
- package/lib/policy.js +18 -4
- package/lib/pricing-database.js +4 -11
- package/lib/reliability.js +0 -3
- package/lib/reporting.js +1 -1
- package/lib/scheduler.js +1 -2
- package/lib/synchronizer-prober.js +72 -15
- package/lib/synchronizer-transfer.js +3 -3
- package/lib/synchronizer.js +9 -1
- 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'])
|
|
@@ -214,17 +212,14 @@ async function fetchSpecific(profile, descriptor, options) {
|
|
|
214
212
|
}
|
|
215
213
|
const respEtag = response.headers?.get?.('etag') || undefined
|
|
216
214
|
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
|
|
215
|
+
let text
|
|
216
|
+
try {
|
|
217
|
+
text = await readBoundedText(response, MAX_RESPONSE_BYTES)
|
|
218
|
+
} catch (err) {
|
|
219
|
+
if (err.message === 'model catalog response is too large') {
|
|
220
|
+
err.adapterCode = 'schema'
|
|
221
|
+
}
|
|
222
|
+
throw err
|
|
228
223
|
}
|
|
229
224
|
try { payload = JSON.parse(text) } catch {
|
|
230
225
|
const error = new Error('model catalog response is not valid JSON')
|
package/lib/alias-resolver.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
export const VIRTUAL_STRATEGIES = Object.freeze(['fastest', 'cheapest', 'best-code'])
|
|
2
2
|
|
|
3
|
-
export
|
|
3
|
+
export const VIRTUAL_ALIASES = Object.freeze(new Set(['@fast', '@cheap', '@best-code']))
|
|
4
|
+
|
|
5
|
+
export function isVirtualAlias(name, strategy) {
|
|
6
|
+
if (strategy && VIRTUAL_STRATEGIES.includes(typeof strategy === 'string' ? strategy.trim() : '')) return true
|
|
4
7
|
if (typeof name !== 'string') return false
|
|
5
8
|
const clean = name.trim().toLowerCase()
|
|
6
|
-
return
|
|
9
|
+
return VIRTUAL_ALIASES.has(clean)
|
|
7
10
|
}
|
|
8
11
|
|
|
9
12
|
export function validateAliasName(name) {
|
|
@@ -12,7 +15,7 @@ export function validateAliasName(name) {
|
|
|
12
15
|
return /^@?[a-zA-Z0-9_\-.]{1,64}$/.test(clean)
|
|
13
16
|
}
|
|
14
17
|
|
|
15
|
-
function modelScoreForCode(
|
|
18
|
+
function modelScoreForCode(model) {
|
|
16
19
|
let score = 0
|
|
17
20
|
const id = String(model?.id ?? '').toLowerCase()
|
|
18
21
|
const name = String(model?.name ?? '').toLowerCase()
|
|
@@ -135,11 +138,22 @@ export function resolveAlias(aliasName, {
|
|
|
135
138
|
}
|
|
136
139
|
|
|
137
140
|
if (strategy === 'fastest') {
|
|
138
|
-
const scored = available.map((item) =>
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
141
|
+
const scored = available.map((item) => {
|
|
142
|
+
const probeKey = `${item.provider}:${item.model.id}`
|
|
143
|
+
const probe = modelProbes[probeKey]
|
|
144
|
+
const measured = probe?.latencyMs && Number.isFinite(probe.latencyMs) && probe.ok !== false
|
|
145
|
+
const latencyMs = measured ? Math.round(probe.latencyMs) : estimateLatency(item.provider, item.model, modelProbes)
|
|
146
|
+
return {
|
|
147
|
+
...item,
|
|
148
|
+
latencyMs,
|
|
149
|
+
measured: Boolean(measured),
|
|
150
|
+
}
|
|
151
|
+
})
|
|
152
|
+
scored.sort((a, b) => {
|
|
153
|
+
if (a.measured && !b.measured) return -1
|
|
154
|
+
if (!a.measured && b.measured) return 1
|
|
155
|
+
return a.latencyMs - b.latencyMs
|
|
156
|
+
})
|
|
143
157
|
const best = scored[0]
|
|
144
158
|
return {
|
|
145
159
|
alias: clean,
|
|
@@ -149,7 +163,7 @@ export function resolveAlias(aliasName, {
|
|
|
149
163
|
name: best.model.name || best.model.id,
|
|
150
164
|
latencyMs: best.latencyMs,
|
|
151
165
|
fallbackUsed: false,
|
|
152
|
-
reason: 'lowest_latency',
|
|
166
|
+
reason: best.measured ? 'lowest_latency' : 'estimated_lowest_latency',
|
|
153
167
|
}
|
|
154
168
|
}
|
|
155
169
|
|
|
@@ -194,7 +208,7 @@ export function resolveAlias(aliasName, {
|
|
|
194
208
|
if (strategy === 'best-code') {
|
|
195
209
|
const scored = available.map((item) => ({
|
|
196
210
|
...item,
|
|
197
|
-
score: modelScoreForCode(item.
|
|
211
|
+
score: modelScoreForCode(item.model),
|
|
198
212
|
}))
|
|
199
213
|
scored.sort((a, b) => b.score - a.score)
|
|
200
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/diff-notifier.js
CHANGED
|
@@ -74,7 +74,7 @@ export function extractDiffEvents(results, options = {}) {
|
|
|
74
74
|
const bp = pair?.before?.pricing
|
|
75
75
|
const ap = pair?.after?.pricing
|
|
76
76
|
if (bp || ap) {
|
|
77
|
-
const inputDiff = bp?.inputPerToken !== ap?.
|
|
77
|
+
const inputDiff = bp?.inputPerToken !== ap?.inputPerToken
|
|
78
78
|
const outputDiff = bp?.outputPerToken !== ap?.outputPerToken
|
|
79
79
|
if (inputDiff || outputDiff) {
|
|
80
80
|
priceChanged.push({
|
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,16 +10,68 @@ 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')
|
|
21
36
|
}
|
|
22
|
-
|
|
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
|
|
68
|
+
}
|
|
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) {
|
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/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
|
package/lib/policy.js
CHANGED
|
@@ -33,9 +33,21 @@ export function normalizePolicy(policy) {
|
|
|
33
33
|
return res
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
function isDangerousPattern(pattern) {
|
|
37
|
+
if (typeof pattern !== 'string') return false
|
|
38
|
+
// Check for nested quantifiers like (a+)+, (a*)*, (a+)*, (a{1,2})+
|
|
39
|
+
if (/(\([^)]*[+*]\)[+*?{])|(\([^)]*\{[0-9]+,[0-9]*\}\)[+*?{])/.test(pattern)) return true
|
|
40
|
+
// Check for quantified groups containing quantified inner constructs
|
|
41
|
+
if (/\([^\)]*([+*]|\{[0-9]+,[0-9]*\})[^\)]*\)([+*]|\{[0-9]+,[0-9]*\})/.test(pattern)) return true
|
|
42
|
+
// Check for multiple consecutive open-ended wildcards (e.g. .*.*)
|
|
43
|
+
if (/(\.\*|\.\+){2,}/.test(pattern)) return true
|
|
44
|
+
return false
|
|
45
|
+
}
|
|
46
|
+
|
|
36
47
|
function compilePatterns(patterns, field) {
|
|
37
48
|
return patterns.map((pattern) => {
|
|
38
49
|
if (pattern.length > MAX_PATTERN_LENGTH) throw Object.assign(new Error(`${field} pattern is too long`), { code: 'INVALID_POLICY' })
|
|
50
|
+
if (isDangerousPattern(pattern)) throw Object.assign(new Error(`${field} pattern has catastrophic backtracking risk: ${pattern}`), { code: 'INVALID_POLICY' })
|
|
39
51
|
try {
|
|
40
52
|
return new RegExp(pattern, 'i')
|
|
41
53
|
} catch {
|
|
@@ -53,10 +65,12 @@ export function validatePolicy(policy) {
|
|
|
53
65
|
|
|
54
66
|
export function hasPolicy(policy) {
|
|
55
67
|
const normalized = normalizePolicy(policy)
|
|
68
|
+
const hasRequire = Object.values(normalized.requireCapabilities).some((v) => v === true)
|
|
69
|
+
const hasDeny = Object.values(normalized.denyCapabilities).some((v) => v === true)
|
|
56
70
|
return normalized.include.length > 0
|
|
57
71
|
|| normalized.exclude.length > 0
|
|
58
|
-
||
|
|
59
|
-
||
|
|
72
|
+
|| hasRequire
|
|
73
|
+
|| hasDeny
|
|
60
74
|
|| (normalized.enableCostFilter && normalized.maxPricePerMillion !== undefined)
|
|
61
75
|
|| (normalized.enableContextFilter && normalized.minContextTokens !== undefined)
|
|
62
76
|
}
|
|
@@ -74,10 +88,10 @@ export function filterModels(models, policy) {
|
|
|
74
88
|
if (exclude.some((pattern) => pattern.test(haystack))) return false
|
|
75
89
|
const modelCapabilities = model?.capabilities ?? {}
|
|
76
90
|
for (const [key, expected] of Object.entries(normalized.requireCapabilities)) {
|
|
77
|
-
if (modelCapabilities[key] !==
|
|
91
|
+
if (expected === true && modelCapabilities[key] !== true) return false
|
|
78
92
|
}
|
|
79
93
|
for (const [key, denied] of Object.entries(normalized.denyCapabilities)) {
|
|
80
|
-
if (modelCapabilities[key] ===
|
|
94
|
+
if (denied === true && modelCapabilities[key] === true) return false
|
|
81
95
|
}
|
|
82
96
|
if (normalized.enableCostFilter && normalized.maxPricePerMillion !== undefined) {
|
|
83
97
|
const inputPrice = model?.pricing?.inputPerToken
|
package/lib/pricing-database.js
CHANGED
|
@@ -100,14 +100,13 @@ export function lookupReferencePricing(provider, modelId) {
|
|
|
100
100
|
const rawId = modelId.toLowerCase().trim()
|
|
101
101
|
const cleaned = cleanId(rawId)
|
|
102
102
|
|
|
103
|
-
// 1. Direct check in provider table
|
|
103
|
+
// 1. Direct check in provider table (exact match or date-cleaned match)
|
|
104
104
|
const table = REFERENCE_PRICING[provKey]
|
|
105
105
|
if (table) {
|
|
106
106
|
if (table[rawId]) return { ...table[rawId] }
|
|
107
107
|
if (table[cleaned]) return { ...table[cleaned] }
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
}
|
|
108
|
+
// If the provider has a dedicated pricing table, do not search other providers' tables
|
|
109
|
+
return undefined
|
|
111
110
|
}
|
|
112
111
|
|
|
113
112
|
// 2. Vendor prefix check (e.g. "openai/gpt-4o" or "anthropic/claude-3-5-sonnet")
|
|
@@ -120,18 +119,12 @@ export function lookupReferencePricing(provider, modelId) {
|
|
|
120
119
|
if (subTable) {
|
|
121
120
|
if (subTable[subId]) return { ...subTable[subId] }
|
|
122
121
|
if (subTable[subCleaned]) return { ...subTable[subCleaned] }
|
|
123
|
-
for (const [key, val] of Object.entries(subTable)) {
|
|
124
|
-
if (subId.startsWith(key) || subCleaned.startsWith(key)) return { ...val }
|
|
125
|
-
}
|
|
126
122
|
}
|
|
127
123
|
}
|
|
128
124
|
|
|
129
|
-
// 3. Fallback search across
|
|
125
|
+
// 3. Fallback search across tables ONLY for unknown/aggregator providers without a dedicated table
|
|
130
126
|
for (const [p, t] of Object.entries(REFERENCE_PRICING)) {
|
|
131
127
|
if (t[cleaned]) return { ...t[cleaned] }
|
|
132
|
-
for (const [key, val] of Object.entries(t)) {
|
|
133
|
-
if (cleaned.startsWith(key)) return { ...val }
|
|
134
|
-
}
|
|
135
128
|
}
|
|
136
129
|
|
|
137
130
|
return undefined
|
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) {
|
package/lib/reporting.js
CHANGED
|
@@ -127,7 +127,7 @@ export function appendNotification(existing, report, { limit = DEFAULT_NOTIFICAT
|
|
|
127
127
|
rows.push({
|
|
128
128
|
...current,
|
|
129
129
|
updatedAt: now,
|
|
130
|
-
occurrences: boundedInteger(current.occurrences, 1, 1, 1000000)
|
|
130
|
+
occurrences: boundedInteger(Number(current.occurrences || 0) + 1, 1, 1, 1000000),
|
|
131
131
|
})
|
|
132
132
|
} else {
|
|
133
133
|
rows.push(notification)
|
package/lib/scheduler.js
CHANGED
|
@@ -82,6 +82,7 @@ export function createSyncScheduler(sync, {
|
|
|
82
82
|
async function tick() {
|
|
83
83
|
const config = getConfig() ?? {}
|
|
84
84
|
if (config.enabled === false || config.scheduleEnabled === false || running) return
|
|
85
|
+
resetStates(config)
|
|
85
86
|
const rows = scheduleRows(config)
|
|
86
87
|
if (rows.length === 0) return
|
|
87
88
|
const now = nowFn()
|
|
@@ -131,8 +132,6 @@ export function createSyncScheduler(sync, {
|
|
|
131
132
|
}
|
|
132
133
|
|
|
133
134
|
function status() {
|
|
134
|
-
const config = getConfig() ?? {}
|
|
135
|
-
resetStates(config)
|
|
136
135
|
const values = [...state.values()]
|
|
137
136
|
const nextAt = (timer !== null && values.length > 0) ? Math.min(...values.map((row) => row.nextAt)) : null
|
|
138
137
|
const lastRunAt = values.filter((row) => row.lastRunAt).reduce((max, row) => Math.max(max, row.lastRunAt), 0) || null
|
|
@@ -1,40 +1,92 @@
|
|
|
1
|
-
export function createModelProber({ health }) {
|
|
1
|
+
export function createModelProber({ health, getProviderCatalog = () => [] } = {}) {
|
|
2
|
+
const modelProbes = new Map()
|
|
3
|
+
|
|
2
4
|
async function tryModel({ provider, model } = {}) {
|
|
3
5
|
if (typeof provider !== 'string' || !provider) throw new Error('provider is required')
|
|
4
6
|
if (typeof model !== 'string' || !model) throw new Error('model is required')
|
|
5
7
|
const start = Date.now()
|
|
6
8
|
const result = await health({ provider })
|
|
7
9
|
const entry = result?.results?.find((r) => r.provider === provider)
|
|
8
|
-
|
|
10
|
+
const providerOk = entry?.status === 'ok'
|
|
11
|
+
|
|
12
|
+
// Verify model in provider catalog if catalog is available
|
|
13
|
+
const catalog = typeof getProviderCatalog === 'function' ? getProviderCatalog(provider) : []
|
|
14
|
+
const hasCatalog = Array.isArray(catalog) && catalog.length > 0
|
|
15
|
+
const inCatalog = !hasCatalog || catalog.some((m) => (typeof m === 'string' ? m : m?.id) === model)
|
|
16
|
+
|
|
17
|
+
const ok = Boolean(providerOk && inCatalog)
|
|
18
|
+
const latencyMs = Date.now() - start
|
|
19
|
+
const status = !providerOk ? (entry?.status ?? 'error') : (!inCatalog ? 'model_not_found' : 'ok')
|
|
20
|
+
const errorMsg = !providerOk
|
|
21
|
+
? (entry?.message || entry?.status || 'probe failed')
|
|
22
|
+
: (!inCatalog ? `model "${model}" not found in provider catalog` : undefined)
|
|
23
|
+
|
|
24
|
+
const probeResult = {
|
|
9
25
|
provider,
|
|
10
26
|
model,
|
|
11
|
-
ok
|
|
12
|
-
latencyMs
|
|
13
|
-
status
|
|
14
|
-
...(
|
|
27
|
+
ok,
|
|
28
|
+
latencyMs,
|
|
29
|
+
status,
|
|
30
|
+
...(errorMsg ? { error: errorMsg } : {}),
|
|
15
31
|
}
|
|
32
|
+
|
|
33
|
+
modelProbes.set(`${provider}:${model}`, {
|
|
34
|
+
latencyMs,
|
|
35
|
+
ok,
|
|
36
|
+
status,
|
|
37
|
+
timestamp: Date.now(),
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
return probeResult
|
|
16
41
|
}
|
|
17
42
|
|
|
18
43
|
async function batchTryModels({ provider, models } = {}) {
|
|
19
44
|
if (typeof provider !== 'string' || !provider) throw new Error('provider is required')
|
|
20
45
|
if (!Array.isArray(models)) throw new Error('models must be an array')
|
|
21
46
|
const list = [...new Set(models.filter((m) => typeof m === 'string' && m.trim()).map((m) => m.trim()))].slice(0, 50)
|
|
47
|
+
|
|
48
|
+
// Probe provider health ONCE for the entire batch rather than per-model
|
|
49
|
+
const start = Date.now()
|
|
50
|
+
const healthResult = await health({ provider })
|
|
51
|
+
const entry = healthResult?.results?.find((r) => r.provider === provider)
|
|
52
|
+
const providerOk = entry?.status === 'ok'
|
|
53
|
+
const catalog = typeof getProviderCatalog === 'function' ? getProviderCatalog(provider) : []
|
|
54
|
+
const hasCatalog = Array.isArray(catalog) && catalog.length > 0
|
|
55
|
+
const baseLatency = Date.now() - start
|
|
56
|
+
|
|
22
57
|
const results = []
|
|
23
|
-
const concurrency =
|
|
58
|
+
const concurrency = 5
|
|
24
59
|
for (let i = 0; i < list.length; i += concurrency) {
|
|
25
60
|
const chunk = list.slice(i, i + concurrency)
|
|
26
|
-
const chunkResults =
|
|
27
|
-
|
|
61
|
+
const chunkResults = chunk.map((model) => {
|
|
62
|
+
const inCatalog = !hasCatalog || catalog.some((m) => (typeof m === 'string' ? m : m?.id) === model)
|
|
63
|
+
const ok = Boolean(providerOk && inCatalog)
|
|
64
|
+
const status = !providerOk ? (entry?.status ?? 'error') : (!inCatalog ? 'model_not_found' : 'ok')
|
|
65
|
+
const errorMsg = !providerOk
|
|
66
|
+
? (entry?.message || entry?.status || 'probe failed')
|
|
67
|
+
: (!inCatalog ? `model "${model}" not found in provider catalog` : undefined)
|
|
68
|
+
|
|
69
|
+
const itemResult = {
|
|
28
70
|
provider,
|
|
29
71
|
model,
|
|
30
|
-
ok
|
|
31
|
-
latencyMs:
|
|
32
|
-
status
|
|
33
|
-
|
|
34
|
-
}
|
|
35
|
-
|
|
72
|
+
ok,
|
|
73
|
+
latencyMs: baseLatency,
|
|
74
|
+
status,
|
|
75
|
+
...(errorMsg ? { error: errorMsg } : {}),
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
modelProbes.set(`${provider}:${model}`, {
|
|
79
|
+
latencyMs: baseLatency,
|
|
80
|
+
ok,
|
|
81
|
+
status,
|
|
82
|
+
timestamp: Date.now(),
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
return itemResult
|
|
86
|
+
})
|
|
36
87
|
results.push(...chunkResults)
|
|
37
88
|
}
|
|
89
|
+
|
|
38
90
|
const reachable = results.filter((r) => r.ok).length
|
|
39
91
|
return {
|
|
40
92
|
provider,
|
|
@@ -43,8 +95,13 @@ export function createModelProber({ health }) {
|
|
|
43
95
|
}
|
|
44
96
|
}
|
|
45
97
|
|
|
98
|
+
function getModelProbes() {
|
|
99
|
+
return Object.fromEntries(modelProbes)
|
|
100
|
+
}
|
|
101
|
+
|
|
46
102
|
return {
|
|
47
103
|
tryModel,
|
|
48
104
|
batchTryModels,
|
|
105
|
+
getModelProbes,
|
|
49
106
|
}
|
|
50
107
|
}
|
|
@@ -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
|
}
|
|
@@ -74,7 +74,7 @@ export function createConfigTransfer({
|
|
|
74
74
|
const strat = v.strategy.trim()
|
|
75
75
|
if (VIRTUAL_STRATEGIES.includes(strat)) {
|
|
76
76
|
entry.strategy = strat
|
|
77
|
-
entry.isVirtual = isVirtualAlias(k)
|
|
77
|
+
entry.isVirtual = isVirtualAlias(k, strat)
|
|
78
78
|
} else {
|
|
79
79
|
continue
|
|
80
80
|
}
|
|
@@ -125,7 +125,7 @@ export function createConfigTransfer({
|
|
|
125
125
|
}
|
|
126
126
|
currentAliases[trimmedAlias] = {
|
|
127
127
|
strategy: cleanStrategy,
|
|
128
|
-
isVirtual: isVirtualAlias(trimmedAlias),
|
|
128
|
+
isVirtual: isVirtualAlias(trimmedAlias, cleanStrategy),
|
|
129
129
|
updatedAt: Date.now(),
|
|
130
130
|
}
|
|
131
131
|
} else {
|
package/lib/synchronizer.js
CHANGED
|
@@ -452,7 +452,13 @@ export function createModelSynchronizer(ctx, { getConfig = () => ({}), saveConfi
|
|
|
452
452
|
return transientNotifications.find((row) => row.id === id)
|
|
453
453
|
}
|
|
454
454
|
|
|
455
|
-
const prober = createModelProber({
|
|
455
|
+
const prober = createModelProber({
|
|
456
|
+
health,
|
|
457
|
+
getProviderCatalog: (p) => {
|
|
458
|
+
const row = providers().find((item) => item.provider === p)
|
|
459
|
+
return row?.availableModels || row?.models || []
|
|
460
|
+
},
|
|
461
|
+
})
|
|
456
462
|
|
|
457
463
|
const rollbackHistory = (opts) => performHistoryRollback({ ctx, config: getConfig() ?? {}, providers: providers(), ...opts })
|
|
458
464
|
|
|
@@ -533,6 +539,7 @@ export function createModelSynchronizer(ctx, { getConfig = () => ({}), saveConfi
|
|
|
533
539
|
validatePolicy,
|
|
534
540
|
isCircuitOpen: (p) => Boolean(circuits.get(p)?.blockedUntil > Date.now()),
|
|
535
541
|
getHealthResults: () => lastHealth?.results ?? [],
|
|
542
|
+
getModelProbes: () => prober.getModelProbes(),
|
|
536
543
|
})
|
|
537
544
|
|
|
538
545
|
return Object.freeze({
|
|
@@ -547,6 +554,7 @@ export function createModelSynchronizer(ctx, { getConfig = () => ({}), saveConfi
|
|
|
547
554
|
listPolicies,
|
|
548
555
|
health,
|
|
549
556
|
...prober,
|
|
557
|
+
getModelProbes: prober.getModelProbes,
|
|
550
558
|
...transfer,
|
|
551
559
|
credentialDiagnostics,
|
|
552
560
|
history,
|
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.8",
|
|
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
|
}
|