@goodandready/dsh-model-sync 0.5.6 → 0.5.7

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.
@@ -1,9 +1,12 @@
1
1
  export const VIRTUAL_STRATEGIES = Object.freeze(['fastest', 'cheapest', 'best-code'])
2
2
 
3
- export function isVirtualAlias(name) {
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 clean === '@fast' || clean === '@cheap' || clean === '@best-code' || clean.startsWith('@')
9
+ return VIRTUAL_ALIASES.has(clean)
7
10
  }
8
11
 
9
12
  export function validateAliasName(name) {
@@ -135,11 +138,22 @@ export function resolveAlias(aliasName, {
135
138
  }
136
139
 
137
140
  if (strategy === 'fastest') {
138
- const scored = available.map((item) => ({
139
- ...item,
140
- latencyMs: estimateLatency(item.provider, item.model, modelProbes),
141
- }))
142
- scored.sort((a, b) => a.latencyMs - b.latencyMs)
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
 
@@ -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?.outputPerToken && bp?.inputPerToken !== ap?.inputPerToken
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/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
- || Object.keys(normalized.requireCapabilities).length > 0
59
- || Object.keys(normalized.denyCapabilities).length > 0
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] !== expected) return false
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] === denied) return false
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
@@ -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
- for (const [key, val] of Object.entries(table)) {
109
- if (rawId.startsWith(key) || cleaned.startsWith(key)) return { ...val }
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 all tables if cleaned matches well-known key
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/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) + 1,
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
- return {
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: entry?.status === 'ok',
12
- latencyMs: Date.now() - start,
13
- status: entry?.status ?? 'unknown',
14
- ...(entry?.status !== 'ok' ? { error: entry?.message || entry?.status || 'probe failed' } : {}),
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 = 3
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 = await Promise.all(
27
- chunk.map((model) => tryModel({ provider, model }).catch((err) => ({
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: false,
31
- latencyMs: 0,
32
- status: 'error',
33
- error: err instanceof Error ? err.message : String(err)
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
  }
@@ -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 {
@@ -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({ health })
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.6",
3
+ "version": "0.5.7",
4
4
  "description": "Automatic model catalog synchronization for API-key DeepSeek Harness providers.",
5
5
  "keywords": [
6
6
  "deepseek-harness",