mohdel 0.124.0 → 1.0.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/README.md +155 -30
- package/config/curated.schema.json +32 -10
- package/js/client/call.js +76 -19
- package/js/client/gate-binary.js +5 -0
- package/js/client/index.js +1 -0
- package/js/core/envelope.js +5 -1
- package/js/factory/bridge.js +2 -2
- package/js/session/adapters/_cancelled.js +0 -6
- package/js/session/adapters/_chat_completions.js +25 -0
- package/js/session/adapters/_output_cap.js +30 -0
- package/js/session/adapters/_registry.js +44 -0
- package/js/session/adapters/anthropic.js +8 -5
- package/js/session/adapters/gemini.js +4 -0
- package/js/session/adapters/openai.js +2 -1
- package/js/session/run.js +14 -4
- package/js/session/run_image.js +12 -3
- package/package.json +49 -19
- package/src/cli/aliases.js +20 -0
- package/src/cli/ask.js +58 -12
- package/src/cli/backup.js +2 -1
- package/src/cli/check.js +15 -86
- package/src/cli/complete.js +130 -0
- package/src/cli/default.js +34 -13
- package/src/cli/doctor.js +33 -14
- package/src/cli/entry.js +173 -0
- package/src/cli/index.js +77 -66
- package/src/cli/instructions.js +349 -0
- package/src/cli/local.js +14 -0
- package/src/cli/model.js +184 -37
- package/src/cli/onboard.js +186 -121
- package/src/cli/rank.js +2 -1
- package/src/cli/ratelimit.js +3 -3
- package/src/cli/tag.js +2 -0
- package/src/lib/assistants.js +93 -0
- package/src/lib/catalog/openrouter.js +6 -1
- package/src/lib/catalog-review.js +195 -0
- package/src/lib/common.js +14 -1
- package/src/lib/creators.js +35 -0
- package/src/lib/index.js +17 -3
- package/src/lib/local-conventions.js +120 -0
- package/src/lib/provider-info.js +98 -0
- package/src/lib/providers.js +69 -14
- package/src/lib/schema.js +15 -3
- package/src/lib/select.js +125 -67
- package/js/session/adapters/image/index.js +0 -40
package/src/lib/select.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as clack from '@clack/prompts'
|
|
2
2
|
import providers from './providers.js'
|
|
3
|
+
import creators, { creatorFromModelId } from './creators.js'
|
|
3
4
|
import {
|
|
4
5
|
getAPIKey,
|
|
5
6
|
getCuratedModels,
|
|
@@ -17,40 +18,21 @@ import { silent } from './logger.js'
|
|
|
17
18
|
loadEnvFile('.env')
|
|
18
19
|
loadDefaultEnv()
|
|
19
20
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
// Get API key using the common.js functionality
|
|
31
|
-
const apiKey = getAPIKey(config.apiKeyEnv)
|
|
32
|
-
|
|
33
|
-
if (!apiKey) {
|
|
34
|
-
console.warn(`Warning: No API key found for ${name} (env var: ${config.apiKeyEnv})`)
|
|
35
|
-
continue
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// Create configuration
|
|
39
|
-
const sdkConfig = config.createConfiguration(apiKey)
|
|
40
|
-
|
|
41
|
-
const sdkPath = `./catalog/${config.sdk}.js`
|
|
42
|
-
const { default: API } = await import(sdkPath)
|
|
43
|
-
|
|
44
|
-
api[name] = API({ ...sdkConfig, baseURL: config.baseURL }, {}, silent)
|
|
45
|
-
providersWithKeys.push(name)
|
|
46
|
-
} catch (err) {
|
|
47
|
-
console.error(`Error initializing provider ${name} api:`, err.message)
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
return { api, providersWithKeys }
|
|
21
|
+
// One provider's catalog client, for callers that don't need all of them.
|
|
22
|
+
export const providerApi = async (name) => {
|
|
23
|
+
const config = providers[name]
|
|
24
|
+
if (!config || config.catalog === false || !config.apiKeyEnv) return null
|
|
25
|
+
const apiKey = getAPIKey(config.apiKeyEnv)
|
|
26
|
+
if (!apiKey) return null
|
|
27
|
+
const { default: API } = await import(`./catalog/${config.sdk}.js`)
|
|
28
|
+
return API({ ...config.createConfiguration(apiKey), baseURL: config.baseURL }, {}, silent)
|
|
52
29
|
}
|
|
53
30
|
|
|
31
|
+
export const providersWithKeys = () =>
|
|
32
|
+
Object.entries(providers)
|
|
33
|
+
.filter(([, c]) => c.catalog !== false && c.apiKeyEnv && getAPIKey(c.apiKeyEnv))
|
|
34
|
+
.map(([name]) => name)
|
|
35
|
+
|
|
54
36
|
const getModelDetails = async (providerName, modelId, api) => {
|
|
55
37
|
try {
|
|
56
38
|
if (!api.getModelInfo) {
|
|
@@ -183,33 +165,45 @@ const isModelTrackedInCollection = (collection, providerName, modelId) => {
|
|
|
183
165
|
return false
|
|
184
166
|
}
|
|
185
167
|
|
|
168
|
+
// Which creators a provider serves is a fact of the catalog, not a property of
|
|
169
|
+
// the provider: a router's line-up changes week to week. Offer the ones this
|
|
170
|
+
// provider already serves first, then every creator mohdel has a label for.
|
|
171
|
+
const creatorOptions = async (providerName, guess) => {
|
|
172
|
+
const catalog = await getCuratedModels()
|
|
173
|
+
const served = new Set()
|
|
174
|
+
for (const [key, spec] of catalogEntries(catalog)) {
|
|
175
|
+
if (spec.deprecated || !spec.creator) continue
|
|
176
|
+
if (key.split('/')[0] === providerName) served.add(spec.creator)
|
|
177
|
+
}
|
|
178
|
+
served.delete(guess)
|
|
179
|
+
const rest = Object.keys(creators).filter(c => c !== guess && !served.has(c)).sort()
|
|
180
|
+
return [
|
|
181
|
+
...(guess ? [{ value: guess, label: guess, hint: 'matches the model id' }] : []),
|
|
182
|
+
...[...served].sort().map(c => ({ value: c, label: c, hint: `already served by ${providerName}` })),
|
|
183
|
+
...rest.map(c => ({ value: c, label: c })),
|
|
184
|
+
{ value: '__other', label: 'Other… (enter a name)' }
|
|
185
|
+
]
|
|
186
|
+
}
|
|
187
|
+
|
|
186
188
|
export const promptMissingFields = async (entry, curatedKey) => {
|
|
187
189
|
if (!entry.creator) {
|
|
188
|
-
const [providerName] = curatedKey.split('/')
|
|
189
|
-
const
|
|
190
|
-
const
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
]
|
|
190
|
+
const [providerName, ...bare] = curatedKey.split('/')
|
|
191
|
+
const guess = creatorFromModelId(bare.join('/'))
|
|
192
|
+
const creatorVal = await clack.select({
|
|
193
|
+
message: `Creator for ${curatedKey}:`,
|
|
194
|
+
initialValue: guess || undefined,
|
|
195
|
+
options: await creatorOptions(providerName, guess)
|
|
196
|
+
})
|
|
197
|
+
if (clack.isCancel(creatorVal)) return entry
|
|
198
|
+
if (creatorVal === '__other') {
|
|
199
|
+
const custom = await clack.text({
|
|
200
|
+
message: `New creator name for ${curatedKey}:`,
|
|
201
|
+
validate: (v) => v?.trim() ? undefined : 'Creator is required'
|
|
201
202
|
})
|
|
202
|
-
if (clack.isCancel(
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
validate: (v) => v?.trim() ? undefined : 'Creator is required'
|
|
207
|
-
})
|
|
208
|
-
if (clack.isCancel(custom)) return entry
|
|
209
|
-
entry.creator = custom.trim()
|
|
210
|
-
} else {
|
|
211
|
-
entry.creator = creatorVal
|
|
212
|
-
}
|
|
203
|
+
if (clack.isCancel(custom)) return entry
|
|
204
|
+
entry.creator = custom.trim()
|
|
205
|
+
} else {
|
|
206
|
+
entry.creator = creatorVal
|
|
213
207
|
}
|
|
214
208
|
}
|
|
215
209
|
|
|
@@ -268,6 +262,38 @@ const filterUncurated = (models, providerName, curated, excluded) => {
|
|
|
268
262
|
})
|
|
269
263
|
}
|
|
270
264
|
|
|
265
|
+
// A model list only carries prices when the provider publishes them
|
|
266
|
+
// (`pricesFromApi`). Where it does, the ones that cost nothing are the whole
|
|
267
|
+
// no-agent story, and a free-text search over hundreds of ids is no way to
|
|
268
|
+
// find them.
|
|
269
|
+
export const freeModels = (models) =>
|
|
270
|
+
models.filter(m => m.inputPrice === 0 && m.outputPrice === 0)
|
|
271
|
+
|
|
272
|
+
const buildEntry = async (providerName, model, providerInstance) => {
|
|
273
|
+
const info = await getModelDetails(providerName, model.id, providerInstance)
|
|
274
|
+
const entry = stripUnknown({
|
|
275
|
+
provider: providerName,
|
|
276
|
+
sdk: providers[providerName].sdk,
|
|
277
|
+
model: model.id,
|
|
278
|
+
label: model.label || model.id,
|
|
279
|
+
...(info || {})
|
|
280
|
+
})
|
|
281
|
+
// Upstream lists name the provider, never the creator. An OpenRouter id is
|
|
282
|
+
// `<vendor>/<model>`, so the vendor segment is the answer when the creator
|
|
283
|
+
// table does not recognize the name.
|
|
284
|
+
if (!entry.creator) entry.creator = creatorFromModelId(model.id) || model.id.split('/')[0]
|
|
285
|
+
return entry
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
export const addModels = async (providerName, providerInstance, models) => {
|
|
289
|
+
const curated = await getCuratedModels()
|
|
290
|
+
for (const model of models) {
|
|
291
|
+
curated[`${providerName}/${model.id}`] = await buildEntry(providerName, model, providerInstance)
|
|
292
|
+
}
|
|
293
|
+
await saveCuratedModels(curated)
|
|
294
|
+
return models.length
|
|
295
|
+
}
|
|
296
|
+
|
|
271
297
|
const searchModels = (models, query) => {
|
|
272
298
|
const q = query.toLowerCase()
|
|
273
299
|
const terms = q.split(/\s+/).filter(Boolean)
|
|
@@ -278,12 +304,32 @@ const searchModels = (models, query) => {
|
|
|
278
304
|
}
|
|
279
305
|
|
|
280
306
|
const processModelsSearchMode = async (providerName, providerInstance, allModels) => {
|
|
281
|
-
|
|
307
|
+
let curated = await getCuratedModels()
|
|
282
308
|
const excluded = await getExcludedModels()
|
|
283
|
-
|
|
309
|
+
let uncurated = filterUncurated(allModels, providerName, curated, excluded)
|
|
284
310
|
|
|
285
311
|
clack.log.info(`${providerName}: ${allModels.length} models upstream, ${uncurated.length} uncurated`)
|
|
286
312
|
|
|
313
|
+
const free = freeModels(uncurated)
|
|
314
|
+
if (free.length) {
|
|
315
|
+
const chosen = await clack.multiselect({
|
|
316
|
+
message: `${free.length} model${free.length > 1 ? 's' : ''} cost${free.length > 1 ? '' : 's'} nothing. Add them?`,
|
|
317
|
+
options: free.map(m => ({ value: m.id, label: m.id, hint: m.label !== m.id ? m.label : undefined })),
|
|
318
|
+
initialValues: free.map(m => m.id),
|
|
319
|
+
maxItems: 12,
|
|
320
|
+
required: false
|
|
321
|
+
})
|
|
322
|
+
if (!clack.isCancel(chosen) && chosen.length) {
|
|
323
|
+
const picked = free.filter(m => chosen.includes(m.id))
|
|
324
|
+
const s = clack.spinner()
|
|
325
|
+
s.start(`Adding ${picked.length} model${picked.length > 1 ? 's' : ''}...`)
|
|
326
|
+
await addModels(providerName, providerInstance, picked)
|
|
327
|
+
s.stop(`Added ${picked.length} model${picked.length > 1 ? 's' : ''}, priced at $0`)
|
|
328
|
+
curated = await getCuratedModels()
|
|
329
|
+
uncurated = filterUncurated(allModels, providerName, curated, excluded)
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
287
333
|
while (true) {
|
|
288
334
|
const query = await clack.text({
|
|
289
335
|
message: `Search ${providerName} models (or "done" to finish):`,
|
|
@@ -451,27 +497,35 @@ const processModelsSelectMode = async (providerName, providerInstance, allModels
|
|
|
451
497
|
export const processModels = async (providerName, providerInstance) => {
|
|
452
498
|
if (!providerInstance.listModels) {
|
|
453
499
|
console.log(`Provider ${providerName} does not support listModels`)
|
|
454
|
-
return
|
|
500
|
+
return false
|
|
455
501
|
}
|
|
456
502
|
|
|
457
|
-
try
|
|
458
|
-
|
|
459
|
-
|
|
503
|
+
// The spinner is started outside the try so a failed fetch can stop it. It
|
|
504
|
+
// used to be created inside, and any error left it spinning forever.
|
|
505
|
+
const s = clack.spinner()
|
|
506
|
+
let spinning = true
|
|
507
|
+
const stop = (message) => {
|
|
508
|
+
if (!spinning) return
|
|
509
|
+
spinning = false
|
|
510
|
+
s.stop(message)
|
|
511
|
+
}
|
|
512
|
+
s.start(`Fetching model list from ${providerName}...`)
|
|
460
513
|
|
|
514
|
+
try {
|
|
461
515
|
const models = await providerInstance.listModels()
|
|
462
516
|
if (!Array.isArray(models)) {
|
|
463
|
-
|
|
464
|
-
return
|
|
517
|
+
stop(`${providerName} returned an invalid model list`)
|
|
518
|
+
return false
|
|
465
519
|
}
|
|
466
520
|
|
|
467
521
|
const curated = await getCuratedModels()
|
|
468
522
|
const excluded = await getExcludedModels()
|
|
469
523
|
const uncurated = filterUncurated(models, providerName, curated, excluded)
|
|
470
|
-
|
|
524
|
+
stop(`${providerName}: ${models.length} models upstream, ${uncurated.length} uncurated`)
|
|
471
525
|
|
|
472
526
|
if (!uncurated.length) {
|
|
473
527
|
clack.log.info(`Nothing new to curate. To add a model not in the upstream catalog:\n mo model add ${providerName}/<model-id>`)
|
|
474
|
-
return
|
|
528
|
+
return true
|
|
475
529
|
}
|
|
476
530
|
|
|
477
531
|
if (uncurated.length > SEARCH_MODE_THRESHOLD) {
|
|
@@ -479,7 +533,11 @@ export const processModels = async (providerName, providerInstance) => {
|
|
|
479
533
|
} else {
|
|
480
534
|
await processModelsSelectMode(providerName, providerInstance, models)
|
|
481
535
|
}
|
|
536
|
+
return true
|
|
482
537
|
} catch (err) {
|
|
483
|
-
|
|
538
|
+
stop(`${providerName}: could not fetch the model list`)
|
|
539
|
+
console.error(err.message)
|
|
540
|
+
console.error(`Check the key with "mo doctor", or reset it with "mo provider setup ${providerName}".`)
|
|
541
|
+
return false
|
|
484
542
|
}
|
|
485
543
|
}
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Image-adapter registry. Mirrors session/adapters/index.js but
|
|
3
|
-
* scoped to image-generation providers.
|
|
4
|
-
*
|
|
5
|
-
* @module session/adapters/image
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { openaiImage } from './openai.js'
|
|
9
|
-
import { novitaImage } from './novita.js'
|
|
10
|
-
import { fakeImage } from './fake.js'
|
|
11
|
-
|
|
12
|
-
const IMAGE_ADAPTERS = {
|
|
13
|
-
openai: openaiImage,
|
|
14
|
-
novita: novitaImage,
|
|
15
|
-
fake: fakeImage
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* @param {string} provider
|
|
20
|
-
* @returns {(
|
|
21
|
-
* env: import('#core/image.js').ImageEnvelope,
|
|
22
|
-
* deps?: any
|
|
23
|
-
* ) => Promise<import('#core/image.js').ImageResult>}
|
|
24
|
-
*/
|
|
25
|
-
export function getImageAdapter (provider) {
|
|
26
|
-
const adapter = IMAGE_ADAPTERS[provider]
|
|
27
|
-
if (!adapter) throw new Error(`no image adapter for provider: ${provider}`)
|
|
28
|
-
return adapter
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Whether the provider has an image adapter registered. Used by
|
|
33
|
-
* `run.js` to distinguish "wrong call path" (image-only provider
|
|
34
|
-
* invoked via answer) from "truly unknown provider".
|
|
35
|
-
*
|
|
36
|
-
* @param {string} provider
|
|
37
|
-
*/
|
|
38
|
-
export function isImageProvider (provider) {
|
|
39
|
-
return Object.prototype.hasOwnProperty.call(IMAGE_ADAPTERS, provider)
|
|
40
|
-
}
|