@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/README.md +21 -0
- package/docs/README.ru.md +21 -0
- package/lib/client.js +226 -124
- package/lib/file-workspace.js +94 -5
- package/lib/history.js +191 -0
- package/lib/index.js +221 -188
- package/lib/moa-runner.js +216 -87
- package/lib/pricing.js +194 -0
- package/package.json +2 -2
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
|
+
"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
|
+
}
|