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.
Files changed (98) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +377 -0
  3. package/config/benchmarks.json +39 -0
  4. package/js/client/call.js +75 -0
  5. package/js/client/call_image.js +82 -0
  6. package/js/client/gate-binary.js +72 -0
  7. package/js/client/index.js +16 -0
  8. package/js/client/ndjson.js +29 -0
  9. package/js/client/transport.js +48 -0
  10. package/js/core/envelope.js +141 -0
  11. package/js/core/errors.js +75 -0
  12. package/js/core/events.js +96 -0
  13. package/js/core/image.js +58 -0
  14. package/js/core/index.js +10 -0
  15. package/js/core/status.js +48 -0
  16. package/js/factory/bridge.js +372 -0
  17. package/js/session/_cooldown.js +114 -0
  18. package/js/session/_logger.js +138 -0
  19. package/js/session/_rate_limiter.js +77 -0
  20. package/js/session/_tracing.js +58 -0
  21. package/js/session/adapters/_cancelled.js +44 -0
  22. package/js/session/adapters/_catalog.js +58 -0
  23. package/js/session/adapters/_chat_completions.js +439 -0
  24. package/js/session/adapters/_errors.js +85 -0
  25. package/js/session/adapters/_images.js +60 -0
  26. package/js/session/adapters/_lazy_json_cache.js +76 -0
  27. package/js/session/adapters/_pricing.js +67 -0
  28. package/js/session/adapters/_providers.js +60 -0
  29. package/js/session/adapters/_tools.js +185 -0
  30. package/js/session/adapters/_videos.js +283 -0
  31. package/js/session/adapters/anthropic.js +397 -0
  32. package/js/session/adapters/cerebras.js +28 -0
  33. package/js/session/adapters/deepseek.js +32 -0
  34. package/js/session/adapters/echo.js +51 -0
  35. package/js/session/adapters/fake.js +262 -0
  36. package/js/session/adapters/fireworks.js +46 -0
  37. package/js/session/adapters/gemini.js +381 -0
  38. package/js/session/adapters/groq.js +23 -0
  39. package/js/session/adapters/image/fake.js +55 -0
  40. package/js/session/adapters/image/index.js +40 -0
  41. package/js/session/adapters/image/novita.js +135 -0
  42. package/js/session/adapters/image/openai.js +50 -0
  43. package/js/session/adapters/index.js +53 -0
  44. package/js/session/adapters/mistral.js +31 -0
  45. package/js/session/adapters/novita.js +29 -0
  46. package/js/session/adapters/openai.js +381 -0
  47. package/js/session/adapters/openrouter.js +66 -0
  48. package/js/session/adapters/xai.js +27 -0
  49. package/js/session/bin.js +54 -0
  50. package/js/session/driver.js +160 -0
  51. package/js/session/index.js +18 -0
  52. package/js/session/run.js +393 -0
  53. package/js/session/run_image.js +61 -0
  54. package/package.json +107 -0
  55. package/src/cli/ask.js +160 -0
  56. package/src/cli/backup.js +107 -0
  57. package/src/cli/bench.js +262 -0
  58. package/src/cli/check.js +123 -0
  59. package/src/cli/colored-logger.js +67 -0
  60. package/src/cli/colors.js +13 -0
  61. package/src/cli/default.js +39 -0
  62. package/src/cli/index.js +150 -0
  63. package/src/cli/json-output.js +60 -0
  64. package/src/cli/model.js +571 -0
  65. package/src/cli/onboard.js +232 -0
  66. package/src/cli/rank.js +176 -0
  67. package/src/cli/ratelimit.js +160 -0
  68. package/src/cli/tag.js +105 -0
  69. package/src/lib/assets/alibaba.svg +1 -0
  70. package/src/lib/assets/anthropic.svg +5 -0
  71. package/src/lib/assets/deepseek.svg +1 -0
  72. package/src/lib/assets/gemini.svg +1 -0
  73. package/src/lib/assets/google.svg +2 -0
  74. package/src/lib/assets/kwaipilot.svg +1 -0
  75. package/src/lib/assets/meta.svg +1 -0
  76. package/src/lib/assets/minimax.svg +9 -0
  77. package/src/lib/assets/moonshotai.svg +4 -0
  78. package/src/lib/assets/openai.svg +5 -0
  79. package/src/lib/assets/xai.svg +1 -0
  80. package/src/lib/assets/xiaomi.svg +2 -0
  81. package/src/lib/assets/zai.svg +219 -0
  82. package/src/lib/benchmark-score.js +215 -0
  83. package/src/lib/benchmark-truth.js +68 -0
  84. package/src/lib/cache.js +76 -0
  85. package/src/lib/common.js +208 -0
  86. package/src/lib/cooldown.js +63 -0
  87. package/src/lib/creators.js +71 -0
  88. package/src/lib/curated-cache.js +146 -0
  89. package/src/lib/errors.js +126 -0
  90. package/src/lib/index.js +726 -0
  91. package/src/lib/logger.js +29 -0
  92. package/src/lib/providers.js +87 -0
  93. package/src/lib/rank.js +390 -0
  94. package/src/lib/rate-limiter.js +50 -0
  95. package/src/lib/schema.js +150 -0
  96. package/src/lib/select.js +474 -0
  97. package/src/lib/tracing.js +62 -0
  98. package/src/lib/utils.js +85 -0
@@ -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
+ }