mohdel 0.90.0
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/LICENSE +21 -0
- package/README.md +377 -0
- package/config/benchmarks.json +39 -0
- package/js/client/call.js +75 -0
- package/js/client/call_image.js +82 -0
- package/js/client/gate-binary.js +72 -0
- package/js/client/index.js +16 -0
- package/js/client/ndjson.js +29 -0
- package/js/client/transport.js +48 -0
- package/js/core/envelope.js +141 -0
- package/js/core/errors.js +75 -0
- package/js/core/events.js +96 -0
- package/js/core/image.js +58 -0
- package/js/core/index.js +10 -0
- package/js/core/status.js +48 -0
- package/js/factory/bridge.js +372 -0
- package/js/session/_cooldown.js +114 -0
- package/js/session/_logger.js +138 -0
- package/js/session/_rate_limiter.js +77 -0
- package/js/session/_tracing.js +58 -0
- package/js/session/adapters/_cancelled.js +44 -0
- package/js/session/adapters/_catalog.js +58 -0
- package/js/session/adapters/_chat_completions.js +439 -0
- package/js/session/adapters/_errors.js +85 -0
- package/js/session/adapters/_images.js +60 -0
- package/js/session/adapters/_lazy_json_cache.js +76 -0
- package/js/session/adapters/_pricing.js +67 -0
- package/js/session/adapters/_providers.js +60 -0
- package/js/session/adapters/_tools.js +185 -0
- package/js/session/adapters/_videos.js +283 -0
- package/js/session/adapters/anthropic.js +397 -0
- package/js/session/adapters/cerebras.js +28 -0
- package/js/session/adapters/deepseek.js +32 -0
- package/js/session/adapters/echo.js +51 -0
- package/js/session/adapters/fake.js +262 -0
- package/js/session/adapters/fireworks.js +46 -0
- package/js/session/adapters/gemini.js +381 -0
- package/js/session/adapters/groq.js +23 -0
- package/js/session/adapters/image/fake.js +55 -0
- package/js/session/adapters/image/index.js +40 -0
- package/js/session/adapters/image/novita.js +135 -0
- package/js/session/adapters/image/openai.js +50 -0
- package/js/session/adapters/index.js +53 -0
- package/js/session/adapters/mistral.js +31 -0
- package/js/session/adapters/novita.js +29 -0
- package/js/session/adapters/openai.js +381 -0
- package/js/session/adapters/openrouter.js +66 -0
- package/js/session/adapters/xai.js +27 -0
- package/js/session/bin.js +54 -0
- package/js/session/driver.js +160 -0
- package/js/session/index.js +18 -0
- package/js/session/run.js +393 -0
- package/js/session/run_image.js +61 -0
- package/package.json +107 -0
- package/src/cli/ask.js +160 -0
- package/src/cli/backup.js +107 -0
- package/src/cli/bench.js +262 -0
- package/src/cli/check.js +123 -0
- package/src/cli/colored-logger.js +67 -0
- package/src/cli/colors.js +13 -0
- package/src/cli/default.js +39 -0
- package/src/cli/index.js +150 -0
- package/src/cli/json-output.js +60 -0
- package/src/cli/model.js +571 -0
- package/src/cli/onboard.js +232 -0
- package/src/cli/rank.js +176 -0
- package/src/cli/ratelimit.js +160 -0
- package/src/cli/tag.js +105 -0
- package/src/lib/assets/alibaba.svg +1 -0
- package/src/lib/assets/anthropic.svg +5 -0
- package/src/lib/assets/deepseek.svg +1 -0
- package/src/lib/assets/gemini.svg +1 -0
- package/src/lib/assets/google.svg +2 -0
- package/src/lib/assets/kwaipilot.svg +1 -0
- package/src/lib/assets/meta.svg +1 -0
- package/src/lib/assets/minimax.svg +9 -0
- package/src/lib/assets/moonshotai.svg +4 -0
- package/src/lib/assets/openai.svg +5 -0
- package/src/lib/assets/xai.svg +1 -0
- package/src/lib/assets/xiaomi.svg +2 -0
- package/src/lib/assets/zai.svg +219 -0
- package/src/lib/benchmark-score.js +215 -0
- package/src/lib/benchmark-truth.js +68 -0
- package/src/lib/cache.js +76 -0
- package/src/lib/common.js +208 -0
- package/src/lib/cooldown.js +63 -0
- package/src/lib/creators.js +71 -0
- package/src/lib/curated-cache.js +146 -0
- package/src/lib/errors.js +126 -0
- package/src/lib/index.js +726 -0
- package/src/lib/logger.js +29 -0
- package/src/lib/providers.js +87 -0
- package/src/lib/rank.js +390 -0
- package/src/lib/rate-limiter.js +50 -0
- package/src/lib/schema.js +150 -0
- package/src/lib/select.js +474 -0
- package/src/lib/tracing.js +62 -0
- package/src/lib/utils.js +85 -0
package/src/cli/model.js
ADDED
|
@@ -0,0 +1,571 @@
|
|
|
1
|
+
import mohdel, { silent } from '../lib/index.js'
|
|
2
|
+
import providerDefs from '../lib/providers.js'
|
|
3
|
+
import { loadDefaultEnv, getAPIKey, getCuratedModels, saveCuratedModels } from '../lib/common.js'
|
|
4
|
+
import { fieldDefs } from '../lib/schema.js'
|
|
5
|
+
import { parseJsonFlag, printAvailableFields, jsonOutput, jsonOutputOne } from './json-output.js'
|
|
6
|
+
import { id, label, tag, price, meta, err, ok } from './colors.js'
|
|
7
|
+
|
|
8
|
+
// Fields available for --json on model list/show
|
|
9
|
+
const MODEL_FIELDS = [
|
|
10
|
+
'model', 'provider', 'creator', 'label', 'type',
|
|
11
|
+
'contextTokenLimit', 'outputTokenLimit',
|
|
12
|
+
'inputPrice', 'outputPrice', 'thinkingPrice',
|
|
13
|
+
'inputFormat', 'tags', 'aliases',
|
|
14
|
+
'thinkingEffortLevels', 'defaultThinkingEffort',
|
|
15
|
+
'rpmLimit', 'tpmLimit', 'rateLimitScope',
|
|
16
|
+
'deprecated', 'suspended'
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
export async function runModel (args) {
|
|
20
|
+
const jsonFlag = parseJsonFlag(args)
|
|
21
|
+
const [action, arg1] = args
|
|
22
|
+
|
|
23
|
+
if (!action || action === '-h' || action === '--help') {
|
|
24
|
+
console.log(`mohdel model — browse models
|
|
25
|
+
|
|
26
|
+
Usage:
|
|
27
|
+
model list [--json [fields]] List all curated models
|
|
28
|
+
model list --sort price|context|name Sort model list
|
|
29
|
+
model search <term> Filter models by name/label
|
|
30
|
+
model stats Catalog summary
|
|
31
|
+
model show <model> [--json [fields]] Show model details
|
|
32
|
+
model get <model> <key> Get a field value
|
|
33
|
+
model set <model> <key> <value> Set a field (custom or reserved)
|
|
34
|
+
model rm <model> <key> Remove a field
|
|
35
|
+
model add <provider>/<model-id> Add a model manually (interactive)
|
|
36
|
+
model backup list|restore|diff Manage catalog backups (prev/daily/weekly)
|
|
37
|
+
model check [--local] [--json] Validate catalog (schema + upstream drift)
|
|
38
|
+
model rank [options] Rank models by benchmark performance
|
|
39
|
+
model bench <model> [options] Benchmark a model with live inference
|
|
40
|
+
model bench --tag <tag> [options] Benchmark all models with a tag
|
|
41
|
+
model curate [provider] Add upstream models to catalog (interactive)
|
|
42
|
+
|
|
43
|
+
Flags:
|
|
44
|
+
--json List available JSON fields
|
|
45
|
+
--json f1,f2,f3 Output only selected fields as JSON
|
|
46
|
+
|
|
47
|
+
Aliases:
|
|
48
|
+
mo ls model list
|
|
49
|
+
mo show <model> model show <model>`)
|
|
50
|
+
process.exit(0)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const mo = await mohdel({ logger: silent })
|
|
54
|
+
const all = mo.list()
|
|
55
|
+
|
|
56
|
+
if (action === 'list' || action === 'search') {
|
|
57
|
+
const query = action === 'search' ? arg1 : null
|
|
58
|
+
const sortIdx = args.indexOf('--sort')
|
|
59
|
+
const sortKey = sortIdx !== -1 ? args[sortIdx + 1] : null
|
|
60
|
+
|
|
61
|
+
let items = all.map(m => ({ id: m.value, label: m.label, info: mo.use(m.value).info() }))
|
|
62
|
+
|
|
63
|
+
if (query) {
|
|
64
|
+
const q = query.toLowerCase()
|
|
65
|
+
items = items.filter(m => m.id.toLowerCase().includes(q) || m.label.toLowerCase().includes(q))
|
|
66
|
+
if (!items.length) { console.log(meta(`No models matching "${query}"`)); return }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (sortKey) {
|
|
70
|
+
const rp = resolvePrice
|
|
71
|
+
const sorters = {
|
|
72
|
+
price: (a, b) => rp(a.info.inputPrice) - rp(b.info.inputPrice),
|
|
73
|
+
context: (a, b) => (b.info.contextTokenLimit || 0) - (a.info.contextTokenLimit || 0),
|
|
74
|
+
name: (a, b) => a.id.localeCompare(b.id)
|
|
75
|
+
}
|
|
76
|
+
if (sorters[sortKey]) items.sort(sorters[sortKey])
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (jsonFlag.json && !jsonFlag.fields) {
|
|
80
|
+
printAvailableFields(MODEL_FIELDS)
|
|
81
|
+
return
|
|
82
|
+
}
|
|
83
|
+
if (jsonFlag.json) {
|
|
84
|
+
jsonOutput(items.map(m => ({ id: m.id, ...m.info })), jsonFlag.fields)
|
|
85
|
+
return
|
|
86
|
+
}
|
|
87
|
+
for (const m of items) {
|
|
88
|
+
const tags = (m.info.tags || []).map(t => meta(t)).join(meta(', '))
|
|
89
|
+
const p = formatPrice(m.info)
|
|
90
|
+
console.log(`${id(m.id)} ${label(m.label)} ${p} ${tags}`)
|
|
91
|
+
}
|
|
92
|
+
return
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (action === 'stats') {
|
|
96
|
+
const providers = new Set()
|
|
97
|
+
const creators = new Set()
|
|
98
|
+
const allTags = new Set()
|
|
99
|
+
let withTools = 0
|
|
100
|
+
let withThinking = 0
|
|
101
|
+
for (const m of all) {
|
|
102
|
+
const info = mo.use(m.value).info()
|
|
103
|
+
if (info.provider) providers.add(info.provider)
|
|
104
|
+
if (info.creator) creators.add(info.creator)
|
|
105
|
+
for (const t of info.tags || []) allTags.add(t)
|
|
106
|
+
if (info.supportsTools) withTools++
|
|
107
|
+
if (info.thinkingEffortLevels) withThinking++
|
|
108
|
+
}
|
|
109
|
+
if (jsonFlag.json) {
|
|
110
|
+
jsonOutputOne({ models: all.length, providers: providers.size, creators: creators.size, tags: allTags.size, withTools, withThinking }, jsonFlag.fields)
|
|
111
|
+
return
|
|
112
|
+
}
|
|
113
|
+
console.log(`${label(all.length)} models across ${id(providers.size)} providers (${meta(creators.size)} creators)`)
|
|
114
|
+
console.log(`${meta('tools:')} ${withTools} ${meta('thinking:')} ${withThinking} ${meta('tags:')} ${allTags.size}`)
|
|
115
|
+
return
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (action === 'show') {
|
|
119
|
+
if (!arg1) { console.error('Usage: model show <model>'); process.exit(1) }
|
|
120
|
+
let model
|
|
121
|
+
try { model = mo.use(arg1) } catch (e) {
|
|
122
|
+
console.error(err(e.message))
|
|
123
|
+
process.exit(1)
|
|
124
|
+
}
|
|
125
|
+
const info = model.info()
|
|
126
|
+
if (jsonFlag.json && !jsonFlag.fields) {
|
|
127
|
+
printAvailableFields(MODEL_FIELDS)
|
|
128
|
+
return
|
|
129
|
+
}
|
|
130
|
+
if (jsonFlag.json) {
|
|
131
|
+
jsonOutputOne({ id: arg1, ...info }, jsonFlag.fields)
|
|
132
|
+
return
|
|
133
|
+
}
|
|
134
|
+
console.log(`${label(info.label)} ${meta(`(${arg1})`)}
|
|
135
|
+
${meta('provider:')} ${id(info.provider)}
|
|
136
|
+
${meta('creator:')} ${info.creator}
|
|
137
|
+
${meta('context:')} ${(info.contextTokenLimit || 0).toLocaleString()} tokens
|
|
138
|
+
${meta('output:')} ${(info.outputTokenLimit || 0).toLocaleString()} tokens
|
|
139
|
+
${meta('input price:')} ${price('$' + resolvePrice(info.inputPrice) + '/M')}
|
|
140
|
+
${meta('output price:')} ${price('$' + resolvePrice(info.outputPrice) + '/M')}
|
|
141
|
+
${meta('tags:')} ${(info.tags || []).map(t => tag(t)).join(', ') || meta('(none)')}`)
|
|
142
|
+
return
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (action === 'get') {
|
|
146
|
+
const modelId = arg1
|
|
147
|
+
const key = args[2]
|
|
148
|
+
if (!modelId || !key) { console.error('Usage: model get <model> <key>'); process.exit(1) }
|
|
149
|
+
let model
|
|
150
|
+
try { model = mo.use(modelId) } catch (e) {
|
|
151
|
+
console.error(err(e.message))
|
|
152
|
+
process.exit(1)
|
|
153
|
+
}
|
|
154
|
+
const info = model.info()
|
|
155
|
+
const value = info[key]
|
|
156
|
+
if (value === undefined) {
|
|
157
|
+
console.error(meta(`${modelId}: ${key} is not set`))
|
|
158
|
+
process.exit(1)
|
|
159
|
+
}
|
|
160
|
+
if (jsonFlag.json) {
|
|
161
|
+
console.log(JSON.stringify(value))
|
|
162
|
+
} else {
|
|
163
|
+
console.log(typeof value === 'object' ? JSON.stringify(value, null, 2) : value)
|
|
164
|
+
}
|
|
165
|
+
return
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (action === 'set') {
|
|
169
|
+
const modelId = arg1
|
|
170
|
+
const key = args[2]
|
|
171
|
+
const rawValue = args.includes('--json-value') ? args[args.indexOf('--json-value') + 1] : args[3]
|
|
172
|
+
if (!modelId || !key) { console.error('Usage: model set <model> <key> <value>'); process.exit(1) }
|
|
173
|
+
if (rawValue === undefined) { console.error('Usage: model set <model> <key> <value> [--json-value]'); process.exit(1) }
|
|
174
|
+
|
|
175
|
+
// Resolve alias → canonical ID; throws with suggestions if not found
|
|
176
|
+
let resolved
|
|
177
|
+
try { resolved = mo.use(modelId) } catch (e) {
|
|
178
|
+
console.error(err(e.message))
|
|
179
|
+
process.exit(1)
|
|
180
|
+
}
|
|
181
|
+
const resolvedId = resolved.id
|
|
182
|
+
const curated = await getCuratedModels()
|
|
183
|
+
if (!curated[resolvedId]) {
|
|
184
|
+
// Model resolved via fallback but not in curated
|
|
185
|
+
console.error(err(`Model '${modelId}' is not in the curated catalog. Use "mo model curate" to add it.`))
|
|
186
|
+
process.exit(1)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Parse value: --json-value for complex types, otherwise auto-detect
|
|
190
|
+
let value
|
|
191
|
+
if (args.includes('--json-value')) {
|
|
192
|
+
try { value = JSON.parse(rawValue) } catch (e) {
|
|
193
|
+
console.error(err(`Invalid JSON: ${e.message}`))
|
|
194
|
+
process.exit(1)
|
|
195
|
+
}
|
|
196
|
+
} else {
|
|
197
|
+
value = coerceValue(key, rawValue)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Validate type for reserved fields
|
|
201
|
+
const def = fieldDefs[key]
|
|
202
|
+
if (def) {
|
|
203
|
+
const expected = def.type
|
|
204
|
+
const actual = Array.isArray(value) ? 'array' : typeof value
|
|
205
|
+
if (actual !== expected && !(def.altType && actual === def.altType)) {
|
|
206
|
+
console.error(err(`Field "${key}" expects ${expected}, got ${actual}`))
|
|
207
|
+
process.exit(1)
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
curated[resolvedId][key] = value
|
|
212
|
+
await saveCuratedModels(curated)
|
|
213
|
+
console.log(`${id(resolvedId)}: ${key} = ${typeof value === 'object' ? JSON.stringify(value) : value}`)
|
|
214
|
+
return
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (action === 'rm' || action === 'rm-field') {
|
|
218
|
+
const modelId = arg1
|
|
219
|
+
const key = args[2]
|
|
220
|
+
if (!modelId || !key) { console.error('Usage: model rm <model> <key>'); process.exit(1) }
|
|
221
|
+
|
|
222
|
+
let resolved
|
|
223
|
+
try { resolved = mo.use(modelId) } catch (e) {
|
|
224
|
+
console.error(err(e.message))
|
|
225
|
+
process.exit(1)
|
|
226
|
+
}
|
|
227
|
+
const resolvedId = resolved.id
|
|
228
|
+
const curated = await getCuratedModels()
|
|
229
|
+
if (!curated[resolvedId]) {
|
|
230
|
+
console.error(err(`Model '${modelId}' is not in the curated catalog.`))
|
|
231
|
+
process.exit(1)
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (fieldDefs[key]?.required) {
|
|
235
|
+
console.error(err(`Cannot remove required field: ${key}`))
|
|
236
|
+
process.exit(1)
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
delete curated[resolvedId][key]
|
|
240
|
+
await saveCuratedModels(curated)
|
|
241
|
+
console.log(`${id(resolvedId)}: ${key} removed`)
|
|
242
|
+
return
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (action === 'backup') {
|
|
246
|
+
const { runBackup } = await import('./backup.js')
|
|
247
|
+
await runBackup(args.slice(1))
|
|
248
|
+
return
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (action === 'add') {
|
|
252
|
+
const modelId = arg1
|
|
253
|
+
if (!modelId || !modelId.includes('/')) {
|
|
254
|
+
console.error('Usage: model add <provider>/<model-id>')
|
|
255
|
+
console.error('Example: mo model add fireworks/deepseek-r1')
|
|
256
|
+
process.exit(1)
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const [providerName, ...modelParts] = modelId.split('/')
|
|
260
|
+
const modelName = modelParts.join('/')
|
|
261
|
+
const providerConfig = providerDefs[providerName]
|
|
262
|
+
if (!providerConfig) {
|
|
263
|
+
console.error(err(`Unknown provider: ${providerName}`))
|
|
264
|
+
process.exit(1)
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const curated = await getCuratedModels()
|
|
268
|
+
if (curated[modelId]) {
|
|
269
|
+
console.error(err(`${modelId} already exists in catalog`))
|
|
270
|
+
process.exit(1)
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Pre-fill from provider config
|
|
274
|
+
const entry = {
|
|
275
|
+
model: modelName,
|
|
276
|
+
provider: providerName,
|
|
277
|
+
sdk: providerConfig.sdk
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Try to fetch upstream info if provider has an API key
|
|
281
|
+
const apiKey = getAPIKey(providerConfig.apiKeyEnv)
|
|
282
|
+
if (apiKey && providerConfig.catalog !== false) {
|
|
283
|
+
try {
|
|
284
|
+
const sdkConfig = providerConfig.createConfiguration(apiKey)
|
|
285
|
+
const { default: API } = await import(`../lib/sdk/${providerConfig.sdk}.js`)
|
|
286
|
+
const noop = () => {}
|
|
287
|
+
const api = API(sdkConfig, {}, { trace: noop, debug: noop, info: noop, warn: noop, error: noop, fatal: noop })
|
|
288
|
+
if (api.getModelInfo) {
|
|
289
|
+
const info = await api.getModelInfo(modelName)
|
|
290
|
+
if (info) {
|
|
291
|
+
Object.assign(entry, info)
|
|
292
|
+
console.log(meta('Fetched model info from upstream'))
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
} catch {}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Interactive prompts for missing fields
|
|
299
|
+
const { promptMissingFields } = await import('../lib/select.js')
|
|
300
|
+
const completed = await promptMissingFields(entry, modelId)
|
|
301
|
+
|
|
302
|
+
curated[modelId] = completed
|
|
303
|
+
await saveCuratedModels(curated)
|
|
304
|
+
console.log(`${ok('+')} ${id(modelId)} added to catalog`)
|
|
305
|
+
return
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (action === 'check') {
|
|
309
|
+
const { runCheck } = await import('./check.js')
|
|
310
|
+
await runCheck(args.slice(1))
|
|
311
|
+
return
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (action === 'rank') {
|
|
315
|
+
const { runRank } = await import('./rank.js')
|
|
316
|
+
await runRank(args.slice(1))
|
|
317
|
+
return
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (action === 'bench') {
|
|
321
|
+
const { runBench } = await import('./bench.js')
|
|
322
|
+
await runBench(args.slice(1))
|
|
323
|
+
return
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (action === 'curate') {
|
|
327
|
+
const { initializeAPIs, processModels } = await import('../lib/select.js')
|
|
328
|
+
const { api, providersWithKeys } = await initializeAPIs()
|
|
329
|
+
|
|
330
|
+
if (!providersWithKeys.length) {
|
|
331
|
+
console.error(err('No providers with API keys configured. Run "mo" to set up.'))
|
|
332
|
+
process.exit(1)
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// mo model curate <provider> — curate specific provider
|
|
336
|
+
if (arg1) {
|
|
337
|
+
if (!api[arg1]) {
|
|
338
|
+
console.error(err(`Provider "${arg1}" not found or no API key. Available: ${providersWithKeys.join(', ')}`))
|
|
339
|
+
process.exit(1)
|
|
340
|
+
}
|
|
341
|
+
await processModels(arg1, api[arg1])
|
|
342
|
+
return
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// mo model curate — prompt for provider
|
|
346
|
+
const { select, isCancel } = await import('@clack/prompts')
|
|
347
|
+
const selected = await select({
|
|
348
|
+
message: 'Select a provider to curate:',
|
|
349
|
+
options: providersWithKeys.map(name => ({ value: name, label: name }))
|
|
350
|
+
})
|
|
351
|
+
if (isCancel(selected)) return
|
|
352
|
+
await processModels(selected, api[selected])
|
|
353
|
+
return
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
console.error(`Unknown action: ${action}. Run "model --help".`)
|
|
357
|
+
process.exit(1)
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
export async function runProvider (args) {
|
|
361
|
+
const jsonFlag = parseJsonFlag(args)
|
|
362
|
+
const [action, arg1] = args
|
|
363
|
+
|
|
364
|
+
const mo = await mohdel({ logger: silent })
|
|
365
|
+
const all = mo.list()
|
|
366
|
+
|
|
367
|
+
if ((!action || action === 'list') && !arg1) {
|
|
368
|
+
loadDefaultEnv()
|
|
369
|
+
const providerMap = new Map()
|
|
370
|
+
for (const m of all) {
|
|
371
|
+
const info = mo.use(m.value).info()
|
|
372
|
+
if (!info.provider) continue
|
|
373
|
+
if (!providerMap.has(info.provider)) providerMap.set(info.provider, 0)
|
|
374
|
+
providerMap.set(info.provider, providerMap.get(info.provider) + 1)
|
|
375
|
+
}
|
|
376
|
+
const rows = [...providerMap.entries()]
|
|
377
|
+
.sort((a, b) => a[0].localeCompare(b[0]))
|
|
378
|
+
.map(([provider, count]) => {
|
|
379
|
+
const def = providerDefs[provider]
|
|
380
|
+
const hasKey = def?.apiKeyEnv ? !!getAPIKey(def.apiKeyEnv) : null
|
|
381
|
+
const rl = mo.getProviderRateLimit(provider)
|
|
382
|
+
return { provider, count, hasKey, rpmLimit: rl?.rpmLimit || null, tpmLimit: rl?.tpmLimit || null }
|
|
383
|
+
})
|
|
384
|
+
|
|
385
|
+
if (jsonFlag.json && !jsonFlag.fields) {
|
|
386
|
+
printAvailableFields(['provider', 'count', 'hasKey', 'rpmLimit', 'tpmLimit'])
|
|
387
|
+
return
|
|
388
|
+
}
|
|
389
|
+
if (jsonFlag.json) {
|
|
390
|
+
jsonOutput(rows, jsonFlag.fields)
|
|
391
|
+
return
|
|
392
|
+
}
|
|
393
|
+
for (const row of rows) {
|
|
394
|
+
const dot = row.hasKey === null ? ' ' : row.hasKey ? ok('●') : meta('○')
|
|
395
|
+
const rl = []
|
|
396
|
+
if (row.rpmLimit) rl.push(`rpm=${row.rpmLimit}`)
|
|
397
|
+
if (row.tpmLimit) rl.push(`tpm=${row.tpmLimit}`)
|
|
398
|
+
const rlStr = rl.length ? meta(rl.join(' ')) : ''
|
|
399
|
+
console.log(` ${dot} ${id(row.provider)} ${meta(`(${row.count} models)`)} ${rlStr}`)
|
|
400
|
+
}
|
|
401
|
+
const hasUnconfigured = rows.some(r => r.hasKey === false)
|
|
402
|
+
console.log(`\n${meta('Next:')} mo provider show <name> ${meta('│')} mo curate <name>` +
|
|
403
|
+
(hasUnconfigured ? ` ${meta('│')} mo provider setup <name>` : ''))
|
|
404
|
+
return
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
if (action === 'show' || (action === 'list' && arg1)) {
|
|
408
|
+
if (!arg1) { console.error('Usage: provider list <provider>'); process.exit(1) }
|
|
409
|
+
const models = all.filter(m => {
|
|
410
|
+
const info = mo.use(m.value).info()
|
|
411
|
+
return info.provider === arg1
|
|
412
|
+
})
|
|
413
|
+
if (!models.length) { console.error(err(`No models for provider: ${arg1}`)); process.exit(1) }
|
|
414
|
+
if (jsonFlag.json && !jsonFlag.fields) {
|
|
415
|
+
printAvailableFields(MODEL_FIELDS)
|
|
416
|
+
return
|
|
417
|
+
}
|
|
418
|
+
if (jsonFlag.json) {
|
|
419
|
+
const items = models.map(m => ({ id: m.value, ...mo.use(m.value).info() }))
|
|
420
|
+
jsonOutput(items, jsonFlag.fields)
|
|
421
|
+
return
|
|
422
|
+
}
|
|
423
|
+
for (const m of models) {
|
|
424
|
+
const info = mo.use(m.value).info()
|
|
425
|
+
console.log(`${id(m.value)} ${label(m.label)} ${formatPrice(info)}`)
|
|
426
|
+
}
|
|
427
|
+
return
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
if (action === 'setup') {
|
|
431
|
+
if (!arg1) { console.error('Usage: provider setup <provider>'); process.exit(1) }
|
|
432
|
+
const providerConfig = providerDefs[arg1]
|
|
433
|
+
if (!providerConfig || !providerConfig.apiKeyEnv) {
|
|
434
|
+
console.error(err(`Unknown provider or no API key supported: ${arg1}`))
|
|
435
|
+
process.exit(1)
|
|
436
|
+
}
|
|
437
|
+
const { PROVIDER_INFO, appendToEnvFile } = await import('./onboard.js')
|
|
438
|
+
const { text, note, isCancel } = await import('@clack/prompts')
|
|
439
|
+
const info = PROVIDER_INFO[arg1]
|
|
440
|
+
|
|
441
|
+
loadDefaultEnv()
|
|
442
|
+
const existing = getAPIKey(providerConfig.apiKeyEnv)
|
|
443
|
+
if (existing) {
|
|
444
|
+
console.log(`${ok('●')} ${id(arg1)} already configured ${meta(`(${providerConfig.apiKeyEnv})`)}`)
|
|
445
|
+
const { confirm } = await import('@clack/prompts')
|
|
446
|
+
const replace = await confirm({ message: 'Replace existing key?' })
|
|
447
|
+
if (isCancel(replace) || !replace) return
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
if (info) {
|
|
451
|
+
note(`${info.hint}\n\n${id(info.url)}`, `${info.label} — API Key`)
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const apiKey = await text({
|
|
455
|
+
message: `Paste your ${arg1} API key:`,
|
|
456
|
+
placeholder: providerConfig.apiKeyEnv,
|
|
457
|
+
validate: (v) => { if (!v?.trim()) return 'API key cannot be empty' }
|
|
458
|
+
})
|
|
459
|
+
if (isCancel(apiKey)) return
|
|
460
|
+
|
|
461
|
+
await appendToEnvFile(providerConfig.apiKeyEnv, apiKey.trim())
|
|
462
|
+
console.log(`${ok('✓')} Saved ${providerConfig.apiKeyEnv}`)
|
|
463
|
+
return
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
if (action === 'rm' || action === 'remove') {
|
|
467
|
+
if (!arg1) { console.error('Usage: provider rm <provider>'); process.exit(1) }
|
|
468
|
+
const providerConfig = providerDefs[arg1]
|
|
469
|
+
if (!providerConfig || !providerConfig.apiKeyEnv) {
|
|
470
|
+
console.error(err(`Unknown provider: ${arg1}`))
|
|
471
|
+
process.exit(1)
|
|
472
|
+
}
|
|
473
|
+
const { appendToEnvFile } = await import('./onboard.js')
|
|
474
|
+
await appendToEnvFile(providerConfig.apiKeyEnv, '')
|
|
475
|
+
console.log(`${ok('✓')} Removed ${providerConfig.apiKeyEnv}`)
|
|
476
|
+
return
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
console.error(`Unknown action: ${action}. Run "mo provider --help".`)
|
|
480
|
+
process.exit(1)
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
export async function runCreator (args) {
|
|
484
|
+
const jsonFlag = parseJsonFlag(args)
|
|
485
|
+
const [action, arg1] = args
|
|
486
|
+
|
|
487
|
+
const mo = await mohdel({ logger: silent })
|
|
488
|
+
const all = mo.list()
|
|
489
|
+
|
|
490
|
+
if ((!action || action === 'list') && !arg1) {
|
|
491
|
+
const creators = new Map()
|
|
492
|
+
for (const m of all) {
|
|
493
|
+
const info = mo.use(m.value).info()
|
|
494
|
+
if (!info.creator) continue
|
|
495
|
+
if (!creators.has(info.creator)) creators.set(info.creator, 0)
|
|
496
|
+
creators.set(info.creator, creators.get(info.creator) + 1)
|
|
497
|
+
}
|
|
498
|
+
if (jsonFlag.json && !jsonFlag.fields) {
|
|
499
|
+
printAvailableFields(['creator', 'count'])
|
|
500
|
+
return
|
|
501
|
+
}
|
|
502
|
+
if (jsonFlag.json) {
|
|
503
|
+
const items = [...creators.entries()]
|
|
504
|
+
.sort((a, b) => a[0].localeCompare(b[0]))
|
|
505
|
+
.map(([creator, count]) => ({ creator, count }))
|
|
506
|
+
jsonOutput(items, jsonFlag.fields)
|
|
507
|
+
return
|
|
508
|
+
}
|
|
509
|
+
for (const [name, count] of [...creators.entries()].sort((a, b) => a[0].localeCompare(b[0]))) {
|
|
510
|
+
console.log(`${id(name)} ${meta(`(${count} models)`)}`)
|
|
511
|
+
}
|
|
512
|
+
return
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
if (action === 'show' || (action === 'list' && arg1)) {
|
|
516
|
+
if (!arg1) { console.error('Usage: creator list <creator>'); process.exit(1) }
|
|
517
|
+
const models = all.filter(m => {
|
|
518
|
+
const info = mo.use(m.value).info()
|
|
519
|
+
return info.creator === arg1
|
|
520
|
+
})
|
|
521
|
+
if (!models.length) { console.error(err(`No models for creator: ${arg1}`)); process.exit(1) }
|
|
522
|
+
if (jsonFlag.json && !jsonFlag.fields) {
|
|
523
|
+
printAvailableFields(MODEL_FIELDS)
|
|
524
|
+
return
|
|
525
|
+
}
|
|
526
|
+
if (jsonFlag.json) {
|
|
527
|
+
const items = models.map(m => ({ id: m.value, ...mo.use(m.value).info() }))
|
|
528
|
+
jsonOutput(items, jsonFlag.fields)
|
|
529
|
+
return
|
|
530
|
+
}
|
|
531
|
+
for (const m of models) {
|
|
532
|
+
const info = mo.use(m.value).info()
|
|
533
|
+
console.log(`${id(m.value)} ${label(m.label)} ${formatPrice(info)} ${meta('via ' + info.provider)}`)
|
|
534
|
+
}
|
|
535
|
+
return
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
console.error(`Unknown action: ${action}. Use "creator list" or "creator show <name>".`)
|
|
539
|
+
process.exit(1)
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// Auto-detect value type from string input
|
|
543
|
+
function coerceValue (key, raw) {
|
|
544
|
+
if (raw === 'true') return true
|
|
545
|
+
if (raw === 'false') return false
|
|
546
|
+
if (raw === 'null') return null
|
|
547
|
+
// Reserved fields: use schema type hint
|
|
548
|
+
const def = fieldDefs[key]
|
|
549
|
+
if (def?.type === 'number') {
|
|
550
|
+
const n = Number(raw)
|
|
551
|
+
return Number.isFinite(n) ? n : raw
|
|
552
|
+
}
|
|
553
|
+
// Unrecognized: try number, fall back to string
|
|
554
|
+
const n = Number(raw)
|
|
555
|
+
if (raw !== '' && Number.isFinite(n)) return n
|
|
556
|
+
return raw
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function resolvePrice (p) {
|
|
560
|
+
if (p == null) return 0
|
|
561
|
+
if (typeof p === 'number') return p
|
|
562
|
+
if (typeof p === 'object') return p.default || Object.values(p)[0] || 0
|
|
563
|
+
return 0
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
function formatPrice (info) {
|
|
567
|
+
const inp = resolvePrice(info.inputPrice)
|
|
568
|
+
const out = resolvePrice(info.outputPrice)
|
|
569
|
+
if (!inp && !out) return meta('free')
|
|
570
|
+
return price(`$${inp}`) + meta('/') + price(`$${out}`)
|
|
571
|
+
}
|