@goodandready/dsh-moa 0.2.23 → 0.2.25

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.
Files changed (2) hide show
  1. package/lib/pricing.js +93 -6
  2. package/package.json +1 -1
package/lib/pricing.js CHANGED
@@ -28,6 +28,7 @@ export const DIRECT_VENDOR_RATES = {
28
28
  export const FALLBACK_RATES = { input: 0.50, output: 1.50, cacheHit: 0.15 }
29
29
 
30
30
  let memoryCatalog = null
31
+ let memoryCachePath = null
31
32
  let lastFetchedAt = 0
32
33
 
33
34
  /**
@@ -35,6 +36,7 @@ let lastFetchedAt = 0
35
36
  */
36
37
  export function resetMemoryCatalog() {
37
38
  memoryCatalog = null
39
+ memoryCachePath = null
38
40
  lastFetchedAt = 0
39
41
  }
40
42
 
@@ -42,13 +44,14 @@ export function resetMemoryCatalog() {
42
44
  * Load cached catalog from disk into memory.
43
45
  */
44
46
  export function loadCachedCatalog(cachePath = CACHE_FILE) {
45
- if (memoryCatalog) return memoryCatalog
47
+ if (memoryCatalog && memoryCachePath === cachePath) return memoryCatalog
46
48
  try {
47
49
  if (existsSync(cachePath)) {
48
50
  const raw = readFileSync(cachePath, 'utf8')
49
51
  const parsed = JSON.parse(raw)
50
52
  if (parsed && typeof parsed.models === 'object') {
51
53
  memoryCatalog = parsed.models
54
+ memoryCachePath = cachePath
52
55
  lastFetchedAt = parsed.updatedAt || 0
53
56
  return memoryCatalog
54
57
  }
@@ -57,6 +60,7 @@ export function loadCachedCatalog(cachePath = CACHE_FILE) {
57
60
  // Ignore read/parse errors
58
61
  }
59
62
  memoryCatalog = {}
63
+ memoryCachePath = cachePath
60
64
  return memoryCatalog
61
65
  }
62
66
 
@@ -154,20 +158,103 @@ export function resolveModelRates(slot = {}, customPrices = {}, cachePath = CACH
154
158
  if (catalog[fullKey]) return { ...catalog[fullKey] }
155
159
  if (catalog[model]) return { ...catalog[model] }
156
160
 
157
- for (const [catId, catRate] of Object.entries(catalog)) {
161
+ if (!model) return { ...FALLBACK_RATES }
162
+
163
+ const entries = Object.entries(catalog)
164
+ if (entries.length === 0) return { ...FALLBACK_RATES }
165
+
166
+ // 3a. Exact case-insensitive fullKey match
167
+ for (const [catId, catRate] of entries) {
158
168
  const catLower = catId.toLowerCase()
159
- if (catLower.endsWith(`/${model}`) || catLower === model) {
160
- return { ...catRate }
169
+ if (catLower === fullKey) return { ...catRate }
170
+ }
171
+
172
+ // 3b. Exact model match with provider matching
173
+ if (provider) {
174
+ for (const [catId, catRate] of entries) {
175
+ const catLower = catId.toLowerCase()
176
+ const slash = catLower.indexOf('/')
177
+ const catProv = slash !== -1 ? catLower.slice(0, slash) : ''
178
+ const catMod = slash !== -1 ? catLower.slice(slash + 1) : catLower
179
+ if (catProv === provider && (catMod === model || catLower.endsWith(`/${model}`))) {
180
+ return { ...catRate }
181
+ }
161
182
  }
162
183
  }
163
184
 
164
- for (const [catId, catRate] of Object.entries(catalog)) {
185
+ // 3c. Exact model match across providers or suffix match
186
+ for (const [catId, catRate] of entries) {
165
187
  const catLower = catId.toLowerCase()
166
- if (model && (catLower.includes(model) || model.includes(catLower))) {
188
+ const slash = catLower.indexOf('/')
189
+ const catMod = slash !== -1 ? catLower.slice(slash + 1) : catLower
190
+ if (catMod === model || catLower.endsWith(`/${model}`)) {
167
191
  return { ...catRate }
168
192
  }
169
193
  }
170
194
 
195
+ // 3d. Candidate resolution: prioritize same provider, select longest matching ID,
196
+ // and avoid bare substring collisions across distinct models with different rates.
197
+ let pool = entries
198
+ if (provider) {
199
+ const provEntries = entries.filter(([catId]) => {
200
+ const catLower = catId.toLowerCase()
201
+ const slash = catLower.indexOf('/')
202
+ return slash !== -1 && catLower.slice(0, slash) === provider
203
+ })
204
+ if (provEntries.length > 0) {
205
+ pool = provEntries
206
+ }
207
+ }
208
+
209
+ let bestCandidates = []
210
+ let bestScore = 0
211
+
212
+ for (const [catId, catRate] of pool) {
213
+ const catLower = catId.toLowerCase()
214
+ const slash = catLower.indexOf('/')
215
+ const catMod = slash !== -1 ? catLower.slice(slash + 1) : catLower
216
+
217
+ let score = 0
218
+ if (model.startsWith(catMod + '-') || model.startsWith(catMod + ':') || model.startsWith(catMod + '.')) {
219
+ // Query model is a specific variant/revision of catalog model (e.g. model="claude-3-sonnet-20240229", catMod="claude-3-sonnet")
220
+ score = 10000 + catMod.length
221
+ } else if (catMod.startsWith(model + '-') || catMod.startsWith(model + ':') || catMod.startsWith(model + '.')) {
222
+ // Catalog model is a revision/variant of query model
223
+ score = 5000 + model.length
224
+ } else if (model.includes(catMod) && catMod.length >= 4) {
225
+ score = 2000 + catMod.length
226
+ } else if (catMod.includes(model) && model.length >= 4) {
227
+ score = 1000 + model.length
228
+ }
229
+
230
+ if (score > 0) {
231
+ if (score > bestScore) {
232
+ bestScore = score
233
+ bestCandidates = [{ catId, catRate, score, catMod }]
234
+ } else if (score === bestScore) {
235
+ bestCandidates.push({ catId, catRate, score, catMod })
236
+ }
237
+ }
238
+ }
239
+
240
+ if (bestCandidates.length === 1) {
241
+ return { ...bestCandidates[0].catRate }
242
+ }
243
+
244
+ if (bestCandidates.length > 1) {
245
+ const firstRate = bestCandidates[0].catRate
246
+ const allAgree = bestCandidates.every(c =>
247
+ c.catRate.input === firstRate.input &&
248
+ c.catRate.output === firstRate.output
249
+ )
250
+ if (allAgree) {
251
+ return { ...firstRate }
252
+ }
253
+ // Conflict between different models with different pricing sharing a substring:
254
+ // return safe fallback rate instead of assigning a wrong model's price.
255
+ return { ...FALLBACK_RATES }
256
+ }
257
+
171
258
  // 4. Fallback rate
172
259
  return { ...FALLBACK_RATES }
173
260
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-moa",
3
- "version": "0.2.23",
3
+ "version": "0.2.25",
4
4
  "description": "Mixture of Agents (MoA) plugin for DeepSeek Harness with /moa slash command",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",