mohdel 0.125.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 +154 -29
- package/config/curated.schema.json +32 -10
- 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/_chat_completions.js +3 -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/cli/doctor.js
CHANGED
|
@@ -3,7 +3,7 @@ import { label, meta, ok, warn, err, inactive } from './colors.js'
|
|
|
3
3
|
import providers from '../lib/providers.js'
|
|
4
4
|
import { validate, isValidTag } from '../lib/schema.js'
|
|
5
5
|
import {
|
|
6
|
-
CONFIG_DIR, CURATED_PATH, CONFIG_PATH, ENV_PATH,
|
|
6
|
+
CONFIG_DIR, CURATED_PATH, CONFIG_PATH, ENV_PATH, tildePath,
|
|
7
7
|
getCuratedModels, getConfig, loadDefaultEnv, getAPIKey, catalogEntries
|
|
8
8
|
} from '../lib/common.js'
|
|
9
9
|
|
|
@@ -11,11 +11,15 @@ const row = (status, name, detail = '') =>
|
|
|
11
11
|
` ${status} ${name.padEnd(20)} ${meta(detail)}`
|
|
12
12
|
|
|
13
13
|
export async function runDoctor (args) {
|
|
14
|
+
const all = args.includes('--all')
|
|
14
15
|
if (args.includes('-h') || args.includes('--help')) {
|
|
15
16
|
console.log(`mohdel doctor — check that your install is wired up
|
|
16
17
|
|
|
17
18
|
Usage:
|
|
18
|
-
mo doctor [--json]
|
|
19
|
+
mo doctor [--all] [--json]
|
|
20
|
+
|
|
21
|
+
Options:
|
|
22
|
+
--all List every provider's API key variable, set or not
|
|
19
23
|
|
|
20
24
|
What it checks:
|
|
21
25
|
- Config directory and environment file exist
|
|
@@ -45,9 +49,10 @@ Exit code:
|
|
|
45
49
|
|
|
46
50
|
// 1. Config dir + env file
|
|
47
51
|
report.configDir.ok = existsSync(CONFIG_DIR)
|
|
52
|
+
report.assistant = (await getConfig()).assistant || null
|
|
48
53
|
report.envFile.ok = existsSync(ENV_PATH)
|
|
49
54
|
if (!report.configDir.ok) report.warnings.push('config directory does not exist (will be created on first save)')
|
|
50
|
-
if (!report.envFile.ok) report.warnings.push(`no ${ENV_PATH} — set API keys there or via shell env`)
|
|
55
|
+
if (!report.envFile.ok) report.warnings.push(`no ${tildePath(ENV_PATH)} — set API keys there or via shell env`)
|
|
51
56
|
|
|
52
57
|
// 2. API keys per provider
|
|
53
58
|
for (const [name, def] of Object.entries(providers)) {
|
|
@@ -70,6 +75,9 @@ Exit code:
|
|
|
70
75
|
const entries = catalogEntries(curated)
|
|
71
76
|
report.curatedFile.active = entries.filter(([, s]) => !s.deprecated).length
|
|
72
77
|
report.curatedFile.deprecated = entries.length - report.curatedFile.active
|
|
78
|
+
if (report.curatedFile.active === 0) {
|
|
79
|
+
report.warnings.push('catalog is empty — "mo curate <provider>" adds a provider\'s models, then "mo model instructions <provider>" fills in the prices')
|
|
80
|
+
}
|
|
73
81
|
|
|
74
82
|
// Schema validation (same logic as 'mo check', condensed)
|
|
75
83
|
const knownProviders = new Set(Object.keys(providers))
|
|
@@ -126,13 +134,15 @@ Exit code:
|
|
|
126
134
|
}
|
|
127
135
|
|
|
128
136
|
// Pretty output
|
|
129
|
-
console.log(label('
|
|
137
|
+
console.log(label('mohdel doctor\n'))
|
|
130
138
|
|
|
131
139
|
console.log(label('Configuration'))
|
|
132
|
-
console.log(row(report.configDir.ok ? ok('✓') : warn('!'), 'Config dir', report.configDir.path))
|
|
133
|
-
console.log(row(
|
|
140
|
+
console.log(row(report.configDir.ok ? ok('✓') : warn('!'), 'Config dir', tildePath(report.configDir.path)))
|
|
141
|
+
if (report.assistant) console.log(row(ok('✓'), 'Coding agent', report.assistant))
|
|
142
|
+
console.log(row(report.envFile.ok ? ok('✓') : warn('!'), 'Env file', report.envFile.ok ? tildePath(report.envFile.path) : `${tildePath(report.envFile.path)} (missing)`))
|
|
134
143
|
if (report.curatedFile.ok) {
|
|
135
|
-
|
|
144
|
+
const mark = report.curatedFile.active === 0 ? warn('!') : ok('✓')
|
|
145
|
+
console.log(row(mark, 'curated.json', `${report.curatedFile.active} active, ${report.curatedFile.deprecated} deprecated`))
|
|
136
146
|
} else {
|
|
137
147
|
console.log(row(err('✗'), 'curated.json', 'failed to load'))
|
|
138
148
|
}
|
|
@@ -142,13 +152,22 @@ Exit code:
|
|
|
142
152
|
for (const k of report.keys.configured) {
|
|
143
153
|
console.log(row(ok('✓'), k.provider, k.envVar))
|
|
144
154
|
}
|
|
145
|
-
|
|
146
|
-
|
|
155
|
+
// A row per unset provider is a dozen lines of nothing on a fresh install,
|
|
156
|
+
// but the variable names are what you came for when a key isn't picked up.
|
|
157
|
+
if (all) {
|
|
158
|
+
for (const k of report.keys.missing) {
|
|
159
|
+
console.log(row(inactive('○'), k.provider, `${k.envVar} (unset)`))
|
|
160
|
+
}
|
|
161
|
+
} else if (report.keys.missing.length) {
|
|
162
|
+
console.log(row(inactive('○'), `${report.keys.missing.length} unset`,
|
|
163
|
+
meta('"mo providers" lists them, "mo doctor --all" their variables')))
|
|
147
164
|
}
|
|
148
165
|
|
|
149
166
|
console.log()
|
|
150
167
|
console.log(label('Catalog validation'))
|
|
151
|
-
if (report.
|
|
168
|
+
if (report.curatedFile.ok && report.curatedFile.active === 0 && report.curatedFile.deprecated === 0) {
|
|
169
|
+
console.log(` ${inactive('○')} nothing to validate`)
|
|
170
|
+
} else if (report.schema.errors.length) {
|
|
152
171
|
console.log(` ${err('✗')} ${report.schema.errors.length} error(s) ${meta('— run "mo check" for details')}`)
|
|
153
172
|
} else {
|
|
154
173
|
console.log(` ${ok('✓')} no errors`)
|
|
@@ -171,11 +190,11 @@ Exit code:
|
|
|
171
190
|
if (report.errors.length) {
|
|
172
191
|
console.log(`${err('✗')} ${report.errors.length} error(s):`)
|
|
173
192
|
for (const e of report.errors) console.log(` ${err('✗')} ${e}`)
|
|
174
|
-
|
|
175
|
-
|
|
193
|
+
}
|
|
194
|
+
if (report.warnings.length) {
|
|
176
195
|
console.log(`${warn('!')} ${report.warnings.length} warning(s) — install is usable but not fully configured`)
|
|
177
196
|
for (const w of report.warnings) console.log(` ${warn('!')} ${w}`)
|
|
178
|
-
} else {
|
|
179
|
-
console.log(`${ok('✓')} ready`)
|
|
180
197
|
}
|
|
198
|
+
if (report.errors.length) process.exit(1)
|
|
199
|
+
if (!report.warnings.length) console.log(`${ok('✓')} ready`)
|
|
181
200
|
}
|
package/src/cli/entry.js
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { readFile, rm } from 'node:fs/promises'
|
|
2
|
+
import { CURATED_PATH, getCuratedModels, saveCuratedModels, tildePath } from '../lib/common.js'
|
|
3
|
+
import { parseCandidates, reviewCandidates } from '../lib/catalog-review.js'
|
|
4
|
+
import { localConventionsOrExit } from './local.js'
|
|
5
|
+
import { id, label, meta, err, warn, ok } from './colors.js'
|
|
6
|
+
|
|
7
|
+
const readStdin = async () => {
|
|
8
|
+
const chunks = []
|
|
9
|
+
for await (const chunk of process.stdin) chunks.push(chunk)
|
|
10
|
+
return Buffer.concat(chunks).toString('utf8')
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const readCandidateFile = async (path) => {
|
|
14
|
+
if (path === '-') return readStdin()
|
|
15
|
+
return readFile(path, 'utf8')
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const show = (value) => JSON.stringify(value)
|
|
19
|
+
|
|
20
|
+
const renderChange = ({ field, from, to }) => {
|
|
21
|
+
const name = field.padEnd(22)
|
|
22
|
+
if (from === undefined) return ` ${ok('+')} ${name} ${show(to)}`
|
|
23
|
+
if (to === undefined) return ` ${err('-')} ${name} ${show(from)} ${meta('→ (removed)')}`
|
|
24
|
+
return ` ${warn('~')} ${name} ${show(from)} ${meta('→')} ${show(to)}`
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const printReview = (reviews, ignored) => {
|
|
28
|
+
for (const key of ignored) {
|
|
29
|
+
console.log(meta(`skipping meta key ${key}`))
|
|
30
|
+
}
|
|
31
|
+
for (const review of reviews) {
|
|
32
|
+
console.log(`\n${id(review.key)} ${label(review.status)}`)
|
|
33
|
+
for (const change of review.changes) console.log(renderChange(change))
|
|
34
|
+
for (const e of review.errors) console.log(` ${err('✗')} ${e}`)
|
|
35
|
+
for (const w of review.warnings) console.log(` ${warn('!')} ${w}`)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const summarize = (reviews) => ({
|
|
40
|
+
candidates: reviews.length,
|
|
41
|
+
new: reviews.filter(r => r.status === 'new').length,
|
|
42
|
+
changed: reviews.filter(r => r.status === 'changed').length,
|
|
43
|
+
unchanged: reviews.filter(r => r.status === 'unchanged').length,
|
|
44
|
+
errors: reviews.reduce((n, r) => n + r.errors.length, 0),
|
|
45
|
+
warnings: reviews.reduce((n, r) => n + r.warnings.length, 0)
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
const printSummary = (counts) => {
|
|
49
|
+
const parts = [`${counts.new} new`, `${counts.changed} changed`, `${counts.unchanged} unchanged`]
|
|
50
|
+
console.log(`\n${label(`${counts.candidates} candidate(s):`)} ${parts.join(', ')} ${meta('│')} ` +
|
|
51
|
+
`${counts.errors ? err(`${counts.errors} error(s)`) : ok('0 errors')}, ${counts.warnings} warning(s)`)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const loadReview = async (path) => {
|
|
55
|
+
const { entries, ignored } = parseCandidates(await readCandidateFile(path))
|
|
56
|
+
const curated = await getCuratedModels()
|
|
57
|
+
const local = await localConventionsOrExit()
|
|
58
|
+
return { curated, entries, ignored, reviews: reviewCandidates(curated, entries, { local }) }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export async function runCheckEntry (args) {
|
|
62
|
+
const path = args[args.indexOf('--entry') + 1]
|
|
63
|
+
if (!path) {
|
|
64
|
+
console.error('Usage: mo model check --entry <file|->')
|
|
65
|
+
process.exit(1)
|
|
66
|
+
}
|
|
67
|
+
const json = args.includes('--json')
|
|
68
|
+
|
|
69
|
+
let review
|
|
70
|
+
try {
|
|
71
|
+
review = await loadReview(path)
|
|
72
|
+
} catch (e) {
|
|
73
|
+
if (json) console.log(JSON.stringify({ error: e.message }, null, 2))
|
|
74
|
+
else console.error(err(e.message))
|
|
75
|
+
process.exit(1)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const counts = summarize(review.reviews)
|
|
79
|
+
if (json) {
|
|
80
|
+
console.log(JSON.stringify({ counts, ignored: review.ignored, candidates: review.reviews }, null, 2))
|
|
81
|
+
} else {
|
|
82
|
+
printReview(review.reviews, review.ignored)
|
|
83
|
+
printSummary(counts)
|
|
84
|
+
if (!counts.errors) {
|
|
85
|
+
console.log(`${meta('Next:')} mo model apply ${path === '-' ? '<file>' : path}`)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (counts.errors) process.exit(1)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export async function runApply (args) {
|
|
92
|
+
if (args.includes('-h') || args.includes('--help')) {
|
|
93
|
+
console.log(`mohdel model apply — write reviewed catalog entries
|
|
94
|
+
|
|
95
|
+
Usage:
|
|
96
|
+
model apply <file|-> [--yes] [--rm]
|
|
97
|
+
|
|
98
|
+
Reads entries in curated.json shape, validates them, prints the diff
|
|
99
|
+
against ${tildePath(CURATED_PATH)}, and writes after confirmation. Nothing is
|
|
100
|
+
written when validation reports an error.
|
|
101
|
+
|
|
102
|
+
The candidate file is left alone unless you say otherwise — at a terminal
|
|
103
|
+
it asks, and --rm removes it without asking. Undo uses the catalog's own
|
|
104
|
+
backups, not the candidate.
|
|
105
|
+
|
|
106
|
+
Options:
|
|
107
|
+
--yes Skip the confirmation prompt
|
|
108
|
+
--rm Delete the candidate file once it has been applied
|
|
109
|
+
|
|
110
|
+
Write an entry with your coding agent: mo model instructions <provider>`)
|
|
111
|
+
process.exit(0)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const path = args.find(a => !a.startsWith('--'))
|
|
115
|
+
if (!path) {
|
|
116
|
+
console.error('Usage: mo model apply <file|-> [--yes] [--rm]')
|
|
117
|
+
process.exit(1)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
let review
|
|
121
|
+
try {
|
|
122
|
+
review = await loadReview(path)
|
|
123
|
+
} catch (e) {
|
|
124
|
+
console.error(err(e.message))
|
|
125
|
+
process.exit(1)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const counts = summarize(review.reviews)
|
|
129
|
+
printReview(review.reviews, review.ignored)
|
|
130
|
+
printSummary(counts)
|
|
131
|
+
|
|
132
|
+
if (counts.errors) {
|
|
133
|
+
console.error(`\n${err('Not written')} — fix the errors above and run again.`)
|
|
134
|
+
process.exit(1)
|
|
135
|
+
}
|
|
136
|
+
const nothingToDo = !counts.new && !counts.changed
|
|
137
|
+
if (nothingToDo) console.log(`\n${ok('Nothing to write')} — the catalog already matches.`)
|
|
138
|
+
|
|
139
|
+
if (!nothingToDo && !args.includes('--yes')) {
|
|
140
|
+
if (!process.stdout.isTTY || path === '-') {
|
|
141
|
+
console.error(`\n${err('Not written')} — no terminal to confirm on. Re-run in a terminal, or pass --yes.`)
|
|
142
|
+
process.exit(1)
|
|
143
|
+
}
|
|
144
|
+
const { confirm, isCancel } = await import('@clack/prompts')
|
|
145
|
+
const go = await confirm({ message: `Write ${counts.new + counts.changed} entry(ies) to ${tildePath(CURATED_PATH)}?` })
|
|
146
|
+
if (isCancel(go) || !go) {
|
|
147
|
+
console.log(meta('Not written.'))
|
|
148
|
+
return
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (!nothingToDo) {
|
|
153
|
+
await saveCuratedModels({ ...review.curated, ...review.entries })
|
|
154
|
+
console.log(`${ok('✓')} ${counts.new + counts.changed} entry(ies) written to ${tildePath(CURATED_PATH)}`)
|
|
155
|
+
console.log(`${meta('Undo:')} mo model backup diff prev ${meta('│')} mo model backup restore prev`)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// The candidate was written by the agent, not by mohdel, so it is never
|
|
159
|
+
// removed unasked. It has served its purpose though, and the catalog's own
|
|
160
|
+
// backups are what an undo uses — not this file.
|
|
161
|
+
if (path === '-') return
|
|
162
|
+
if (args.includes('--rm')) {
|
|
163
|
+
await rm(path, { force: true })
|
|
164
|
+
console.log(`${ok('✓')} removed ${path}`)
|
|
165
|
+
return
|
|
166
|
+
}
|
|
167
|
+
if (!process.stdout.isTTY) return
|
|
168
|
+
const { confirm, isCancel } = await import('@clack/prompts')
|
|
169
|
+
const drop = await confirm({ message: `Delete ${path}?`, initialValue: false })
|
|
170
|
+
if (isCancel(drop) || !drop) return
|
|
171
|
+
await rm(path, { force: true })
|
|
172
|
+
console.log(`${ok('✓')} removed ${path}`)
|
|
173
|
+
}
|
package/src/cli/index.js
CHANGED
|
@@ -9,6 +9,10 @@
|
|
|
9
9
|
* Aliases: ls → model list, rl → ratelimit
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
+
import { detectAssistants, launchLines } from '../lib/assistants.js'
|
|
13
|
+
import { getConfig } from '../lib/common.js'
|
|
14
|
+
import { ALIASES } from './aliases.js'
|
|
15
|
+
|
|
12
16
|
const [command, ...args] = process.argv.slice(2)
|
|
13
17
|
|
|
14
18
|
if (!command) {
|
|
@@ -17,38 +21,65 @@ if (!command) {
|
|
|
17
21
|
process.exit(0)
|
|
18
22
|
}
|
|
19
23
|
|
|
24
|
+
// Before anything else: a tab press must not pay for the factory import.
|
|
25
|
+
if (command === '__complete') {
|
|
26
|
+
const { runComplete } = await import('./complete.js')
|
|
27
|
+
await runComplete(args)
|
|
28
|
+
process.exit(0)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (command === 'completion') {
|
|
32
|
+
const { runCompletionScript } = await import('./complete.js')
|
|
33
|
+
runCompletionScript(args)
|
|
34
|
+
process.exit(0)
|
|
35
|
+
}
|
|
36
|
+
|
|
20
37
|
if (command === '-h' || command === '--help') {
|
|
21
|
-
|
|
38
|
+
const assistant = (await getConfig()).assistant || null
|
|
39
|
+
const { default: PROVIDER_INFO } = await import('../lib/provider-info.js')
|
|
40
|
+
const { default: providerDefs } = await import('../lib/providers.js')
|
|
41
|
+
const keyRows = Object.keys(PROVIDER_INFO)
|
|
42
|
+
.map(name => [providerDefs[name].apiKeyEnv, `${PROVIDER_INFO[name].label} API key`])
|
|
43
|
+
.concat([['MOHDEL_LOCAL_API_SK', 'Bearer token for a local server, if it wants one']])
|
|
44
|
+
const keyWidth = Math.max(...keyRows.map(([k]) => k.length))
|
|
45
|
+
const keys = keyRows.map(([k, d]) => ` ${k.padEnd(keyWidth + 2)}${d}`).join('\n')
|
|
46
|
+
console.log(`mohdel — self-hosted LLM gateway and SDK for Node
|
|
47
|
+
|
|
48
|
+
Run a model, see what the call cost, and curate the catalog those prices
|
|
49
|
+
come from. Your keys, your infra, no SaaS in the path.
|
|
22
50
|
|
|
23
51
|
Commands:
|
|
24
|
-
model list [--sort price|context|name] List all curated models
|
|
25
|
-
model search <term> Filter models by name/label
|
|
26
|
-
model stats Catalog summary
|
|
27
|
-
model show <model> Show model details
|
|
52
|
+
model list [--sort price|context|name] List all curated models (mo ls, mo models)
|
|
53
|
+
model search <term> Filter models by name/label (mo search)
|
|
54
|
+
model stats Catalog summary (mo stats)
|
|
55
|
+
model show <model> Show model details (mo show)
|
|
28
56
|
model get <model> <key> Get a field value
|
|
29
57
|
model set <model> <key> <value> Set a field
|
|
30
58
|
model rm <model> <key> Remove a field
|
|
31
59
|
model add <provider>/<model-id> Add a model manually
|
|
32
|
-
model
|
|
33
|
-
model
|
|
34
|
-
model
|
|
35
|
-
model
|
|
36
|
-
|
|
37
|
-
provider
|
|
38
|
-
|
|
39
|
-
provider
|
|
60
|
+
model instructions [provider] Brief for your coding agent (mo instructions)
|
|
61
|
+
model check [--entry <file|->] Validate catalog, or candidate entries (mo check)
|
|
62
|
+
model apply <file|-> Write reviewed entries to the catalog (mo apply)
|
|
63
|
+
model rank [--use-case <name>] Rank models by benchmarks (mo rank)
|
|
64
|
+
model bench <model> Benchmark with live inference (mo bench)
|
|
65
|
+
model curate [provider] Add upstream models to catalog (mo curate)
|
|
66
|
+
|
|
67
|
+
provider list List all providers (mo providers)
|
|
68
|
+
provider list <provider> List curated models for a provider
|
|
69
|
+
provider models <provider> List models the key can reach upstream
|
|
70
|
+
provider setup <provider> Configure API key (interactive) (mo setup)
|
|
40
71
|
provider rm <provider> Remove API key
|
|
41
72
|
|
|
42
|
-
creator list List all creators
|
|
73
|
+
creator list List all creators (mo creators)
|
|
43
74
|
creator list <creator> List models by a creator
|
|
44
75
|
|
|
45
|
-
tag list List all unique tags
|
|
76
|
+
tag list List all unique tags (mo tags)
|
|
46
77
|
tag list <model> Show tags on a model
|
|
47
78
|
tag show <tag> List models with a tag
|
|
48
79
|
tag add <model> <tag> Add a tag
|
|
49
80
|
tag rm <model> <tag> Remove a tag
|
|
50
81
|
|
|
51
|
-
ratelimit show <model|provider> Show effective limits
|
|
82
|
+
ratelimit show <model|provider> Show effective limits (mo rl show)
|
|
52
83
|
ratelimit set <model> [rpm] [tpm] Set model-level limits
|
|
53
84
|
ratelimit rm <model> Remove model-level limits
|
|
54
85
|
ratelimit provider set <p> [rpm] [tpm] Set provider-level limits
|
|
@@ -57,24 +88,30 @@ Commands:
|
|
|
57
88
|
ask <provider/model> [prompt] One-shot inference (pipeable)
|
|
58
89
|
transcribe <provider/model> <file> Speech → text from an audio file
|
|
59
90
|
|
|
60
|
-
default
|
|
91
|
+
default [model] Set the model "mo ask" uses by default
|
|
61
92
|
doctor Check that your install is wired up
|
|
93
|
+
completion bash Shell completion — source <(mo completion bash)
|
|
94
|
+
|
|
95
|
+
Catalog work with a coding agent:
|
|
96
|
+
Prices, context limits and cache rates are in no provider API — they live on
|
|
97
|
+
docs pages. Put the brief in front of the coding agent you already use:
|
|
62
98
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
99
|
+
mo model instructions openai > mohdel-brief.md
|
|
100
|
+
|
|
101
|
+
then start it on <prompt>:
|
|
102
|
+
|
|
103
|
+
read mohdel-brief.md, then add gpt-5.6 to my mohdel catalog
|
|
104
|
+
|
|
105
|
+
${launchLines(detectAssistants(), assistant).join('\n')}
|
|
106
|
+
|
|
107
|
+
The agent needs to be able to fetch a web page — that is where the prices
|
|
108
|
+
are. It drafts mohdel-candidate.json and checks it; you apply it:
|
|
109
|
+
|
|
110
|
+
mo model check --entry mohdel-candidate.json
|
|
111
|
+
mo model apply mohdel-candidate.json ← prints the diff first
|
|
112
|
+
|
|
113
|
+
Aliases appear in brackets above. "mo rl" stands in for "mo ratelimit" on
|
|
114
|
+
every subcommand, not only its list form.
|
|
78
115
|
|
|
79
116
|
Global flags:
|
|
80
117
|
--json [fields] Output as JSON (omit fields to list available)
|
|
@@ -83,51 +120,25 @@ Environment:
|
|
|
83
120
|
API keys are loaded from ~/.config/mohdel/environment (KEY=value format).
|
|
84
121
|
Run "mo" with no arguments to configure interactively.
|
|
85
122
|
|
|
86
|
-
|
|
87
|
-
OPENAI_API_SK OpenAI API key
|
|
88
|
-
GEMINI_API_SK Google Gemini API key
|
|
89
|
-
GROQ_API_SK Groq API key
|
|
90
|
-
CEREBRAS_API_SK Cerebras API key
|
|
91
|
-
XAI_API_SK xAI API key
|
|
92
|
-
MISTRAL_API_SK Mistral API key
|
|
93
|
-
DEEPSEEK_API_SK DeepSeek API key
|
|
94
|
-
FIREWORKS_API_SK Fireworks API key
|
|
95
|
-
OPENROUTER_API_SK OpenRouter API key
|
|
96
|
-
NOVITA_API_SK Novita API key
|
|
123
|
+
${keys}
|
|
97
124
|
|
|
98
125
|
Configuration:
|
|
99
|
-
~/.config/mohdel/environment
|
|
100
|
-
~/.config/mohdel/curated.json
|
|
101
|
-
~/.config/mohdel/
|
|
102
|
-
~/.config/mohdel/
|
|
126
|
+
~/.config/mohdel/environment API keys (loaded automatically)
|
|
127
|
+
~/.config/mohdel/curated.json Model catalog
|
|
128
|
+
~/.config/mohdel/catalog.local.json This installation's own fields and tags
|
|
129
|
+
~/.config/mohdel/providers.json Provider-level rate limits
|
|
130
|
+
~/.config/mohdel/excluded.json Models to hide from list and curate
|
|
131
|
+
~/.config/mohdel/default.json Default model, and your coding agent`)
|
|
103
132
|
process.exit(0)
|
|
104
133
|
}
|
|
105
134
|
|
|
106
|
-
// Alias resolution: short commands → noun + verb
|
|
107
|
-
const ALIASES = {
|
|
108
|
-
models: { noun: 'model', inject: ['list'] },
|
|
109
|
-
providers: { noun: 'provider', inject: ['list'] },
|
|
110
|
-
creators: { noun: 'creator', inject: ['list'] },
|
|
111
|
-
tags: { noun: 'tag', inject: ['list'] },
|
|
112
|
-
ls: { noun: 'model', inject: ['list'] },
|
|
113
|
-
show: { noun: 'model', inject: ['show'] },
|
|
114
|
-
search: { noun: 'model', inject: ['search'] },
|
|
115
|
-
stats: { noun: 'model', inject: ['stats'] },
|
|
116
|
-
check: { noun: 'model', inject: ['check'] },
|
|
117
|
-
setup: { noun: 'provider', inject: ['setup'] },
|
|
118
|
-
rank: { noun: 'model', inject: ['rank'] },
|
|
119
|
-
bench: { noun: 'model', inject: ['bench'] },
|
|
120
|
-
curate: { noun: 'model', inject: ['curate'] },
|
|
121
|
-
rl: { noun: 'ratelimit', inject: [] }
|
|
122
|
-
}
|
|
123
|
-
|
|
124
135
|
const alias = ALIASES[command]
|
|
125
136
|
const resolved = alias ? alias.noun : command
|
|
126
137
|
const resolvedArgs = alias ? [...alias.inject, ...args] : args
|
|
127
138
|
|
|
128
139
|
if (resolved === 'default') {
|
|
129
140
|
const { runDefault } = await import('./default.js')
|
|
130
|
-
await runDefault()
|
|
141
|
+
await runDefault(resolvedArgs)
|
|
131
142
|
} else if (resolved === 'doctor') {
|
|
132
143
|
const { runDoctor } = await import('./doctor.js')
|
|
133
144
|
await runDoctor(resolvedArgs)
|