@goodandready/dsh-moa 0.2.3 → 0.2.5

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/pricing.js ADDED
@@ -0,0 +1,194 @@
1
+ // lib/pricing.js
2
+ // Dynamic pricing registry for @goodandready/dsh-moa.
3
+ //
4
+ // Fetches real-time model rates from the public OpenRouter catalog
5
+ // (https://openrouter.ai/api/v1/models, no auth required), caches rates in
6
+ // ~/.dsh/storages/dsh-moa-catalog.json, and supports direct vendor tariffs
7
+ // and user config overrides in settings.yaml.
8
+
9
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
10
+ import { homedir } from 'node:os'
11
+ import { dirname, join } from 'node:path'
12
+
13
+ export const CATALOG_URL = 'https://openrouter.ai/api/v1/models'
14
+ export const CACHE_FILE = join(homedir(), '.dsh', 'storages', 'dsh-moa-catalog.json')
15
+ export const REFRESH_INTERVAL_MS = 24 * 60 * 60 * 1000 // 24 hours
16
+
17
+ /**
18
+ * Direct vendor tariffs (USD per 1M tokens) for official direct routes.
19
+ */
20
+ export const DIRECT_VENDOR_RATES = {
21
+ 'deepseek-chat': { input: 0.14, output: 0.28, cacheHit: 0.014 },
22
+ 'deepseek-reasoner': { input: 0.55, output: 2.19, cacheHit: 0.14 },
23
+ 'deepseek-v4-flash': { input: 0.14, output: 0.28, cacheHit: 0.014 },
24
+ 'deepseek-v4-pro': { input: 0.55, output: 2.19, cacheHit: 0.044 },
25
+ }
26
+
27
+ /** Default fallback rate for uncataloged models ($0.50 prompt / $1.50 completion per 1M tokens) */
28
+ export const FALLBACK_RATES = { input: 0.50, output: 1.50, cacheHit: 0.15 }
29
+
30
+ let memoryCatalog = null
31
+ let lastFetchedAt = 0
32
+
33
+ /**
34
+ * Load cached catalog from disk into memory.
35
+ */
36
+ export function loadCachedCatalog(cachePath = CACHE_FILE) {
37
+ if (memoryCatalog) return memoryCatalog
38
+ try {
39
+ if (existsSync(cachePath)) {
40
+ const raw = readFileSync(cachePath, 'utf8')
41
+ const parsed = JSON.parse(raw)
42
+ if (parsed && typeof parsed.models === 'object') {
43
+ memoryCatalog = parsed.models
44
+ lastFetchedAt = parsed.updatedAt || 0
45
+ return memoryCatalog
46
+ }
47
+ }
48
+ } catch {
49
+ // Ignore read/parse errors
50
+ }
51
+ memoryCatalog = {}
52
+ return memoryCatalog
53
+ }
54
+
55
+ /**
56
+ * Fetch and update catalog from OpenRouter public API.
57
+ */
58
+ export async function fetchCatalog({
59
+ url = CATALOG_URL,
60
+ cachePath = CACHE_FILE,
61
+ signal = AbortSignal.timeout(5000),
62
+ } = {}) {
63
+ try {
64
+ const res = await fetch(url, { signal })
65
+ if (!res.ok) return loadCachedCatalog(cachePath)
66
+ const json = await res.json()
67
+ if (!json || !Array.isArray(json.data)) return loadCachedCatalog(cachePath)
68
+
69
+ const models = {}
70
+ for (const item of json.data) {
71
+ if (!item.id || !item.pricing) continue
72
+ const promptPer1M = Number(item.pricing.prompt || 0) * 1e6
73
+ const completionPer1M = Number(item.pricing.completion || 0) * 1e6
74
+ const cacheHitPer1M = Number(item.pricing.input_cache_hit || item.pricing.prompt || 0) * 1e6
75
+
76
+ models[item.id] = {
77
+ input: promptPer1M,
78
+ output: completionPer1M,
79
+ cacheHit: cacheHitPer1M,
80
+ contextLength: item.context_length || 0,
81
+ name: item.name || item.id,
82
+ }
83
+ }
84
+
85
+ memoryCatalog = models
86
+ lastFetchedAt = Date.now()
87
+
88
+ try {
89
+ mkdirSync(dirname(cachePath), { recursive: true })
90
+ writeFileSync(
91
+ cachePath,
92
+ JSON.stringify({ updatedAt: lastFetchedAt, count: Object.keys(models).length, models }, null, 2),
93
+ 'utf8',
94
+ )
95
+ } catch {
96
+ // Non-fatal if filesystem is read-only
97
+ }
98
+
99
+ return models
100
+ } catch {
101
+ return loadCachedCatalog(cachePath)
102
+ }
103
+ }
104
+
105
+ /**
106
+ * Background refresh catalog if stale (> 24 hours).
107
+ */
108
+ export function refreshCatalogInBackground(cachePath = CACHE_FILE) {
109
+ const catalog = loadCachedCatalog(cachePath)
110
+ const isStale = !lastFetchedAt || Date.now() - lastFetchedAt > REFRESH_INTERVAL_MS
111
+ if (isStale) {
112
+ fetchCatalog({ cachePath }).catch(() => {})
113
+ }
114
+ return catalog
115
+ }
116
+
117
+ /**
118
+ * Resolve price rates (USD per 1M tokens) for a given provider/model slot.
119
+ */
120
+ export function resolveModelRates(slot = {}, customPrices = {}, cachePath = CACHE_FILE) {
121
+ const provider = (slot?.provider || '').trim().toLowerCase()
122
+ const model = (slot?.model || '').trim().toLowerCase()
123
+ const fullKey = provider ? `${provider}/${model}` : model
124
+
125
+ // 1. Check custom user overrides in config
126
+ if (customPrices && typeof customPrices === 'object') {
127
+ if (customPrices[fullKey]) return normalizeRates(customPrices[fullKey])
128
+ if (customPrices[model]) return normalizeRates(customPrices[model])
129
+ if (provider && customPrices[`${provider}/*`]) return normalizeRates(customPrices[`${provider}/*`])
130
+ if (customPrices['*']) return normalizeRates(customPrices['*'])
131
+ }
132
+
133
+ // 2. Check direct vendor rates
134
+ if (DIRECT_VENDOR_RATES[model]) {
135
+ return { ...DIRECT_VENDOR_RATES[model] }
136
+ }
137
+
138
+ // 3. Check OpenRouter catalog
139
+ const catalog = loadCachedCatalog(cachePath) || {}
140
+
141
+ if (catalog[fullKey]) return { ...catalog[fullKey] }
142
+ if (catalog[model]) return { ...catalog[model] }
143
+
144
+ for (const [catId, catRate] of Object.entries(catalog)) {
145
+ const catLower = catId.toLowerCase()
146
+ if (catLower.endsWith(`/${model}`) || catLower === model) {
147
+ return { ...catRate }
148
+ }
149
+ }
150
+
151
+ for (const [catId, catRate] of Object.entries(catalog)) {
152
+ const catLower = catId.toLowerCase()
153
+ if (model && (catLower.includes(model) || model.includes(catLower))) {
154
+ return { ...catRate }
155
+ }
156
+ }
157
+
158
+ // 4. Fallback rate
159
+ return { ...FALLBACK_RATES }
160
+ }
161
+
162
+ function normalizeRates(rate) {
163
+ if (!rate || typeof rate !== 'object') return { ...FALLBACK_RATES }
164
+ return {
165
+ input: Number(rate.input ?? rate.prompt ?? FALLBACK_RATES.input),
166
+ output: Number(rate.output ?? rate.completion ?? FALLBACK_RATES.output),
167
+ cacheHit: Number(rate.cacheHit ?? rate.cache_hit ?? FALLBACK_RATES.cacheHit),
168
+ }
169
+ }
170
+
171
+ /**
172
+ * Estimate USD cost and token totals for a single model call.
173
+ */
174
+ export function estimateTokenCost(slot = {}, usage = {}, customPrices = {}, cachePath = CACHE_FILE) {
175
+ const promptTokens = usage.prompt_tokens ?? usage.inputTokens ?? usage.input ?? usage.promptTokens ?? 0
176
+ const completionTokens = usage.completion_tokens ?? usage.outputTokens ?? usage.output ?? usage.completionTokens ?? 0
177
+ const cacheHitTokens = usage.prompt_tokens_details?.cached_tokens ?? usage.cacheHitTokens ?? 0
178
+ const totalTokens = promptTokens + completionTokens
179
+
180
+ const rates = resolveModelRates(slot, customPrices, cachePath)
181
+
182
+ const nonCachedPrompt = Math.max(0, promptTokens - cacheHitTokens)
183
+ const inputCost = (nonCachedPrompt / 1e6) * rates.input + (cacheHitTokens / 1e6) * rates.cacheHit
184
+ const outputCost = (completionTokens / 1e6) * rates.output
185
+ const totalCost = inputCost + outputCost
186
+
187
+ return {
188
+ inputTokens: promptTokens,
189
+ outputTokens: completionTokens,
190
+ totalTokens,
191
+ costUsd: Number(totalCost.toFixed(5)),
192
+ rates,
193
+ }
194
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-moa",
3
- "version": "0.2.3",
3
+ "version": "0.2.5",
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",
@@ -58,4 +58,4 @@
58
58
  "@deepseek-ai/dsh-tools": "^0.1.0-rc.6",
59
59
  "@deepseek-ai/schemastery": "^3.18.1"
60
60
  }
61
- }
61
+ }