@goodandready/dsh-model-sync 0.5.8 → 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.
@@ -97,14 +97,32 @@ function mappedRows(provider, rows, descriptor) {
97
97
  }
98
98
 
99
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
+ }
100
108
  const parserKind = descriptor.parse || descriptor.parser
101
109
  if (parserKind === 'clinebot-plan') {
102
110
  const plan = payload?.data?.plan || payload?.data || payload?.plan || payload
103
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
+ }
104
117
  return parseClinePassModels(included)
105
118
  }
106
119
  if (parserKind === 'google') {
107
- const rows = Array.isArray(payload?.models) ? payload.models : []
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
+ }
108
126
  return normalizeModels(provider, rows.map((row) => ({
109
127
  ...row,
110
128
  id: String(row.name ?? '').replace(/^models\//, ''),
@@ -113,8 +131,13 @@ function parseRows(provider, payload, descriptor) {
113
131
  maxTokens: row.outputTokenLimit,
114
132
  })))
115
133
  }
116
- const rows = pathValue(payload, descriptor.modelsPath) ?? payload?.data ?? payload?.models ?? payload?.availableModels
117
- return normalizeModels(provider, Array.isArray(rows) ? mappedRows(provider, rows, descriptor) : [])
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))
118
141
  }
119
142
 
120
143
  function privateHttpHost(hostname) {
@@ -75,8 +75,21 @@ async function readBody(response) {
75
75
  }
76
76
 
77
77
  function rowsFrom(payload) {
78
- const rows = payload?.data ?? payload?.models ?? payload?.availableModels
79
- return Array.isArray(rows) ? rows : []
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
80
93
  }
81
94
 
82
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/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
- let liveConfig = resolveConfig(baseConfig)
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 && typeof settings.update === 'function') {
150
- try {
151
- await settings.update(NS, plainPatch)
152
- } catch (err) {
153
- if (typeof ctx.logger?.warn === 'function') {
154
- ctx.logger.warn('[dsh-model-sync] settings.update failed:', err)
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
@@ -273,6 +273,7 @@ function normalizePricing(row) {
273
273
  }
274
274
  if (typeof source.currency === 'string' && source.currency.trim()) pricing.currency = source.currency.trim()
275
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()
276
277
  return pricing
277
278
  }
278
279
 
@@ -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 cached = normalizeModels(row.provider, config.modelCatalogs?.[row.provider] ?? row.models ?? [])
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 explicitRemoveIds = new Set(lifecycle.removedIds)
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.8",
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",