mohdel 3.1.0 → 3.3.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 +27 -11
- package/config/curated.example.json +8 -8
- package/config/curated.schema.json +1 -1
- package/js/session/adapters/_registry.js +1 -0
- package/js/session/adapters/index.js +2 -0
- package/js/session/adapters/meta.js +29 -0
- package/js/session/adapters/openai.js +3 -0
- package/js/session/run.js +39 -18
- package/package.json +6 -6
- package/src/cli/ask.js +1 -1
- package/src/cli/bench.js +1 -1
- package/src/cli/complete.js +2 -1
- package/src/cli/entry.js +1 -0
- package/src/cli/export.js +114 -0
- package/src/cli/model.js +7 -0
- package/src/lib/creators.js +1 -1
- package/src/lib/index.js +1 -1
- package/src/lib/provider-info.js +7 -0
- package/src/lib/providers.js +12 -0
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Mohdel
|
|
2
2
|
|
|
3
|
-
Self-hosted LLM gateway and SDK for Node — think LiteLLM, for the JS world. One `answer()` call for
|
|
3
|
+
Self-hosted LLM gateway and SDK for Node — think LiteLLM, for the JS world. One `answer()` call for 14 providers or local inference; swap models by changing one string; get real per-call USD cost back on every result, with OpenTelemetry built in and process isolation when you need it. Your keys, your infra, no SaaS proxy in the path.
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
6
|
npm install -g mohdel
|
|
@@ -20,14 +20,14 @@ curate novita` writes complete, priced entries on its own, and setup counts the
|
|
|
20
20
|
models that cost nothing and offers to add all of them in one keystroke. A
|
|
21
21
|
working catalog without a pricing page or a brief.
|
|
22
22
|
|
|
23
|
-
Providers: Anthropic, OpenAI, Gemini, Mistral, Groq, xAI, Cerebras, Fireworks, DeepSeek, Qwen Cloud, Xiaomi, OpenRouter, Novita. Node 22+, ES modules.
|
|
23
|
+
Providers: Anthropic, OpenAI, Gemini, Mistral, Groq, xAI, Cerebras, Fireworks, DeepSeek, Qwen Cloud, Xiaomi, Meta Model API, OpenRouter, Novita. Node 22+, ES modules.
|
|
24
24
|
|
|
25
25
|
Mohdel runs the inference layer of production stacks, among them [docAnalyzer](https://docanalyzer.ai), a document analysis and chat platform serving hundreds of thousands of users.
|
|
26
26
|
|
|
27
27
|
## Why mohdel
|
|
28
28
|
|
|
29
29
|
- **Real numbers on every call.** Token counts and per-call USD cost computed from your own pricing catalog (`curated.json`) — not estimates, not provider-specific shapes. Bill tenants, alert on spend, reconcile invoices. Your own catalog means your negotiated rates and your own tags, and it is not a spreadsheet you maintain: `mo model instructions` hands the provider's docs page to your coding agent, which drafts the entries for you to review. See [docs/CATALOG.md](docs/CATALOG.md).
|
|
30
|
-
- **One interface across providers.** Same `answer()` call, same event stream, same `{ status, output, inputTokens, outputTokens, cost }` result. Switching from `anthropic/claude-sonnet-
|
|
30
|
+
- **One interface across providers.** Same `answer()` call, same event stream, same `{ status, output, inputTokens, outputTokens, cost }` result. Switching from `anthropic/claude-sonnet-5-5` to `openai/gpt-5.4-mini` is one string change — adapter differences stay inside mohdel.
|
|
31
31
|
- **Self-hosted, no vendor in the path.** API keys live in `~/.config/mohdel/`. Mohdel calls provider APIs directly; nothing routes through a third party, nothing marks up your tokens, no extra hop of availability risk.
|
|
32
32
|
- **Nothing to compromise.** No network listener, no credential store, no tool execution. Mohdel runs a model call and returns the result; it cannot read a file, run a command, or hand back a key. See [Attack surface](#attack-surface).
|
|
33
33
|
- **Observability without instrumentation.** OpenTelemetry spans, trace-linked logs, and OTLP metrics over one endpoint. Set `OTEL_EXPORTER_OTLP_ENDPOINT`; everything else is wired.
|
|
@@ -100,7 +100,7 @@ Model IDs always use the `<provider>/<model>` format:
|
|
|
100
100
|
|
|
101
101
|
```
|
|
102
102
|
openai/gpt-5.6-luna
|
|
103
|
-
anthropic/claude-sonnet-
|
|
103
|
+
anthropic/claude-sonnet-5-5
|
|
104
104
|
openai/gpt-5.4-mini
|
|
105
105
|
groq/llama-4-scout-17b-16e-instruct
|
|
106
106
|
```
|
|
@@ -157,16 +157,16 @@ See [ARCHITECTURE.md §Design principles](ARCHITECTURE.md#design-principles) for
|
|
|
157
157
|
|
|
158
158
|
```bash
|
|
159
159
|
# One-shot inference — pipeable
|
|
160
|
-
mo ask anthropic/claude-sonnet-
|
|
160
|
+
mo ask anthropic/claude-sonnet-5-5 "explain monads"
|
|
161
161
|
cat article.txt | mo ask openai/gpt-5.4 "summarize in 3 bullets"
|
|
162
162
|
echo "hello" | mo ask openai/gpt-5.6-luna --json | jq .cost
|
|
163
163
|
mo ask openai/gpt-5.6-luna -q "…" 2>err.log # stderr carries failures only
|
|
164
164
|
|
|
165
165
|
# Streaming
|
|
166
|
-
mo ask anthropic/claude-sonnet-
|
|
166
|
+
mo ask anthropic/claude-sonnet-5-5 --stream "write a haiku about recursion"
|
|
167
167
|
|
|
168
168
|
# With thinking effort
|
|
169
|
-
mo ask anthropic/claude-opus-
|
|
169
|
+
mo ask anthropic/claude-opus-5-5 --effort high "prove P != NP"
|
|
170
170
|
|
|
171
171
|
# On a faster service lane, when the model sells one
|
|
172
172
|
mo ask openai/gpt-5.6-luna@fast "triage this alert"
|
|
@@ -179,7 +179,7 @@ mo transcribe mistral/voxtral-mini-transcribe interview.wav --language fr
|
|
|
179
179
|
mo ls # list all curated models
|
|
180
180
|
mo ls --sort price # sorted by input price
|
|
181
181
|
mo search sonnet # filter by name/label
|
|
182
|
-
mo show anthropic/claude-sonnet-
|
|
182
|
+
mo show anthropic/claude-sonnet-5-5 # model details
|
|
183
183
|
mo stats # catalog summary
|
|
184
184
|
mo providers # providers with key status & rate limits
|
|
185
185
|
|
|
@@ -201,12 +201,16 @@ mo model instructions anthropic # brief: fields, doc links, review comman
|
|
|
201
201
|
mo model check --entry mohdel-candidate.json # validate + diff, no write
|
|
202
202
|
mo model apply mohdel-candidate.json # write, after showing the diff
|
|
203
203
|
|
|
204
|
+
# Copy entries to another host
|
|
205
|
+
mo model export meta/muse-spark-1.3 | ssh host mo model check --entry - # preview there
|
|
206
|
+
mo model export meta/muse-spark-1.3 | ssh host mo model apply - --yes # write; remote backup is the undo
|
|
207
|
+
|
|
204
208
|
# Rate limits
|
|
205
209
|
mo rl show anthropic # provider or model limits
|
|
206
|
-
mo rl set anthropic/claude-sonnet-
|
|
210
|
+
mo rl set anthropic/claude-sonnet-5-5 60 100000
|
|
207
211
|
|
|
208
212
|
# Benchmark with live inference
|
|
209
|
-
mo bench anthropic/claude-sonnet-
|
|
213
|
+
mo bench anthropic/claude-sonnet-5-5 # single model
|
|
210
214
|
mo bench --tag fast --effort low # suite by tag
|
|
211
215
|
```
|
|
212
216
|
|
|
@@ -262,6 +266,16 @@ including any field the candidate would remove — before writing. Entries carry
|
|
|
262
266
|
`source` and `sourcedAt` so a price can be traced back to the page it came
|
|
263
267
|
from. See [docs/CATALOG.md](docs/CATALOG.md#editing-with-a-coding-agent).
|
|
264
268
|
|
|
269
|
+
### Copying entries to another host
|
|
270
|
+
|
|
271
|
+
`mo model export <id…>` prints entries exactly as `curated.json` holds them, in the
|
|
272
|
+
shape `mo model apply` reads. `--provider <p>` and `--tag <t>` select in bulk, and
|
|
273
|
+
`--with-redirects` adds the deprecated stubs that point at what you export. Pipe it
|
|
274
|
+
to the other host: `mo model check --entry -` there previews the diff, and
|
|
275
|
+
`mo model apply - --yes` writes it. A pipe has no terminal to confirm on, so the
|
|
276
|
+
remote backup (`mo model backup restore prev`) is the undo. Keys, provider-level
|
|
277
|
+
limits and `catalog.local.json` stay per host.
|
|
278
|
+
|
|
265
279
|
## Library Usage
|
|
266
280
|
|
|
267
281
|
Two integration paths, same adapters underneath: start with the in-process **factory**; graduate to the cross-process **client** when you want gateway-grade isolation.
|
|
@@ -272,7 +286,7 @@ Two integration paths, same adapters underneath: start with the in-process **fac
|
|
|
272
286
|
import mohdel from 'mohdel'
|
|
273
287
|
|
|
274
288
|
const mo = await mohdel()
|
|
275
|
-
const result = await mo.use('anthropic/claude-sonnet-
|
|
289
|
+
const result = await mo.use('anthropic/claude-sonnet-5-5').answer('Hello')
|
|
276
290
|
console.log(result.output, result.cost)
|
|
277
291
|
```
|
|
278
292
|
|
|
@@ -422,6 +436,7 @@ OPENROUTER_API_SK=sk-or-...
|
|
|
422
436
|
NOVITA_API_SK=...
|
|
423
437
|
QWEN_API_SK=sk-...
|
|
424
438
|
XIAOMI_API_SK=...
|
|
439
|
+
META_API_SK=...
|
|
425
440
|
COHERE_API_SK=...
|
|
426
441
|
MOHDEL_LOCAL_API_SK=...
|
|
427
442
|
```
|
|
@@ -461,6 +476,7 @@ What each provider supports through mohdel's unified interface:
|
|
|
461
476
|
| Mistral | Yes | Yes | Yes | No | No | `tool_choice: "any"` = required |
|
|
462
477
|
| Qwen Cloud | Yes | Yes | No | No | Yes (`enable_thinking` + `thinking_budget`) | Alibaba DashScope intl; hybrid models think by default — effort `none` sends explicit off |
|
|
463
478
|
| Xiaomi | Yes | Yes | Yes | No | Auto | MiMo; shared chat-completions path, `reasoning_content` captured |
|
|
479
|
+
| Meta | Yes | Yes | Yes | No | Yes (`reasoning.effort`) | OpenAI Responses API over `api.meta.ai/v1`, sent with `store: false`; Muse Spark reasoning cannot be turned off |
|
|
464
480
|
| OpenRouter | Yes | Yes | Yes | No | Varies | Meta-provider; `providerOptions.openrouter` for routing prefs |
|
|
465
481
|
| Local | Yes | Yes | Yes | No | No | Any OpenAI-compatible server; endpoint is the catalog entry's `baseURL` |
|
|
466
482
|
| Cohere | n/a | n/a | n/a | n/a | n/a | Embeddings only: no chat models reach mohdel through it |
|
|
@@ -17,18 +17,18 @@
|
|
|
17
17
|
"tags": ["fast", "chat"]
|
|
18
18
|
},
|
|
19
19
|
|
|
20
|
-
"anthropic/claude-sonnet-
|
|
20
|
+
"anthropic/claude-sonnet-5-5": {
|
|
21
21
|
"_comment_b": "Full-featured entry. Adds cache pricing (provider-side prompt caching), thinking effort levels (mohdel translates 'low'/'medium'/'high'/etc. to the provider's native budget), default thinking effort, and a leaderboard tuple. The leaderboard is [intelligence, speed, latency] — used by 'mo rank'.",
|
|
22
|
-
"model": "claude-sonnet-
|
|
22
|
+
"model": "claude-sonnet-5-5",
|
|
23
23
|
"creator": "anthropic",
|
|
24
24
|
"provider": "anthropic",
|
|
25
25
|
"sdk": "anthropic",
|
|
26
|
-
"label": "Claude Sonnet
|
|
26
|
+
"label": "Claude Sonnet 5.5",
|
|
27
27
|
"inputFormat": ["text", "image"],
|
|
28
|
-
"inputPrice":
|
|
29
|
-
"outputPrice":
|
|
30
|
-
"cacheWritePrice":
|
|
31
|
-
"cacheReadPrice": 0.
|
|
28
|
+
"inputPrice": 2,
|
|
29
|
+
"outputPrice": 10,
|
|
30
|
+
"cacheWritePrice": 2.5,
|
|
31
|
+
"cacheReadPrice": 0.20,
|
|
32
32
|
"contextTokenLimit": 1000000,
|
|
33
33
|
"outputTokenLimit": 128000,
|
|
34
34
|
"defaultThinkingEffort": "medium",
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
|
|
45
45
|
"anthropic/claude-3-7-sonnet": {
|
|
46
46
|
"_comment_c": "Deprecated stub: a one-field entry that redirects callers to the replacement. 'mo' will refuse to use this id and point at the target. Stubs do NOT need any other fields.",
|
|
47
|
-
"deprecated": "anthropic/claude-sonnet-
|
|
47
|
+
"deprecated": "anthropic/claude-sonnet-5-5"
|
|
48
48
|
},
|
|
49
49
|
|
|
50
50
|
"novita/flux-2-dev": {
|
|
@@ -20,6 +20,7 @@ import { fireworks } from './fireworks.js'
|
|
|
20
20
|
import { gemini } from './gemini.js'
|
|
21
21
|
import { groq } from './groq.js'
|
|
22
22
|
import { local } from './local.js'
|
|
23
|
+
import { meta } from './meta.js'
|
|
23
24
|
import { mistral } from './mistral.js'
|
|
24
25
|
import { novita } from './novita.js'
|
|
25
26
|
import { openai } from './openai.js'
|
|
@@ -38,6 +39,7 @@ export const adapters = Object.freeze({
|
|
|
38
39
|
gemini,
|
|
39
40
|
groq,
|
|
40
41
|
local,
|
|
42
|
+
meta,
|
|
41
43
|
mistral,
|
|
42
44
|
novita,
|
|
43
45
|
openai,
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Meta Model API adapter — OpenAI Responses API over api.meta.ai/v1.
|
|
3
|
+
* Delegates to the `openai` adapter with a baseURL-configured client;
|
|
4
|
+
* the openai adapter branches on `providerOf(envelope.model)` for the
|
|
5
|
+
* fields that differ between vendors.
|
|
6
|
+
*
|
|
7
|
+
* @module session/adapters/meta
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import OpenAI from 'openai'
|
|
11
|
+
|
|
12
|
+
import { openai } from './openai.js'
|
|
13
|
+
import { streamingDispatcher } from './_dispatcher.js'
|
|
14
|
+
|
|
15
|
+
const BASE_URL = 'https://api.meta.ai/v1'
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @param {import('#core/envelope.js').CallEnvelope} envelope
|
|
19
|
+
* @param {{client?: any, signal?: AbortSignal}} [deps]
|
|
20
|
+
* @returns {AsyncGenerator<import('#core/events.js').Event>}
|
|
21
|
+
*/
|
|
22
|
+
export async function * meta (envelope, deps = {}) {
|
|
23
|
+
const client = deps.client ?? new OpenAI({
|
|
24
|
+
apiKey: envelope.auth.key,
|
|
25
|
+
baseURL: BASE_URL,
|
|
26
|
+
fetchOptions: { dispatcher: streamingDispatcher() }
|
|
27
|
+
})
|
|
28
|
+
yield * openai(envelope, { ...deps, client })
|
|
29
|
+
}
|
|
@@ -360,6 +360,9 @@ function buildRequest (envelope, input, instructions) {
|
|
|
360
360
|
|
|
361
361
|
if (envelope.speed) request.service_tier = envelope.speed
|
|
362
362
|
|
|
363
|
+
// Meta's Responses API stores every prompt and response unless told not to.
|
|
364
|
+
if (provider === 'meta') request.store = false
|
|
365
|
+
|
|
363
366
|
return request
|
|
364
367
|
}
|
|
365
368
|
|
package/js/session/run.js
CHANGED
|
@@ -140,6 +140,16 @@ export async function * run (envelope, {
|
|
|
140
140
|
return
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
+
if (envelope.outputEffort) {
|
|
144
|
+
const effortErr = effortError(key, envelope.outputEffort, spec)
|
|
145
|
+
if (effortErr) {
|
|
146
|
+
log.warn({ provider, effort: envelope.outputEffort }, '[mohdel:answer] unsupported output effort')
|
|
147
|
+
endSpanError(span, new Error(effortErr.error.message))
|
|
148
|
+
yield effortErr
|
|
149
|
+
return
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
143
153
|
if (envelope.speed) {
|
|
144
154
|
const speedErr = speedError(key, envelope.speed, spec, provider, adapter)
|
|
145
155
|
if (speedErr) {
|
|
@@ -337,29 +347,40 @@ export function normalizeModelId (envelope, resolveSpec) {
|
|
|
337
347
|
return { envelope: next, key: base, spec: baseSpec }
|
|
338
348
|
}
|
|
339
349
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
...unresolved,
|
|
343
|
-
error: errorEvent(
|
|
344
|
-
`Model '${base}' does not support output effort (no thinkingEffortLevels). Cannot use ':${effort}' suffix.`,
|
|
345
|
-
'SESSION_INVALID_OUTPUT_EFFORT'
|
|
346
|
-
)
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
if (effort !== 'none' && !baseSpec.thinkingEffortLevels[effort]) {
|
|
350
|
-
return {
|
|
351
|
-
...unresolved,
|
|
352
|
-
error: errorEvent(
|
|
353
|
-
`Model '${base}' does not support output effort level '${effort}'. Available: ${Object.keys(baseSpec.thinkingEffortLevels).join(', ')}`,
|
|
354
|
-
'SESSION_INVALID_OUTPUT_EFFORT'
|
|
355
|
-
)
|
|
356
|
-
}
|
|
357
|
-
}
|
|
350
|
+
const effortErr = effortError(base, effort, baseSpec)
|
|
351
|
+
if (effortErr) return { ...unresolved, error: effortErr }
|
|
358
352
|
|
|
359
353
|
next.outputEffort = effort
|
|
360
354
|
return { envelope: next, key: base, spec: baseSpec }
|
|
361
355
|
}
|
|
362
356
|
|
|
357
|
+
/**
|
|
358
|
+
* An effort level is valid only when the entry's `thinkingEffortLevels`
|
|
359
|
+
* declares it — `none` included, since some models cannot turn
|
|
360
|
+
* thinking off.
|
|
361
|
+
*
|
|
362
|
+
* @param {string} key
|
|
363
|
+
* @param {string} effort
|
|
364
|
+
* @param {any} spec
|
|
365
|
+
* @returns {import('#core/events.js').ErrorEvent | undefined}
|
|
366
|
+
*/
|
|
367
|
+
export function effortError (key, effort, spec) {
|
|
368
|
+
const levels = spec.thinkingEffortLevels
|
|
369
|
+
if (!levels) {
|
|
370
|
+
return errorEvent(
|
|
371
|
+
`Model '${key}' does not support output effort (no thinkingEffortLevels). Cannot use effort '${effort}'.`,
|
|
372
|
+
'SESSION_INVALID_OUTPUT_EFFORT'
|
|
373
|
+
)
|
|
374
|
+
}
|
|
375
|
+
if (!Object.hasOwn(levels, effort)) {
|
|
376
|
+
return errorEvent(
|
|
377
|
+
`Model '${key}' does not support output effort level '${effort}'. Available: ${Object.keys(levels).join(', ')}`,
|
|
378
|
+
'SESSION_INVALID_OUTPUT_EFFORT'
|
|
379
|
+
)
|
|
380
|
+
}
|
|
381
|
+
return undefined
|
|
382
|
+
}
|
|
383
|
+
|
|
363
384
|
/**
|
|
364
385
|
* A `cache` marker is honoured on `text` parts only, with a TTL the
|
|
365
386
|
* adapters know. The adapters would ignore any other marker, so it is
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mohdel",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.3.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Christophe Le Bars",
|
|
7
7
|
"email": "clb@toort.net"
|
|
8
8
|
},
|
|
9
|
-
"description": "Self-hosted LLM gateway and SDK for Node — a LiteLLM-style unified API for
|
|
9
|
+
"description": "Self-hosted LLM gateway and SDK for Node — a LiteLLM-style unified API for 14 providers (Anthropic, OpenAI, Gemini, Mistral, Groq, xAI, DeepSeek, OpenRouter, …) with per-call USD cost tracking, streaming, tool calls, vision, speech-to-text, and built-in OpenTelemetry. Run in-process, or behind the process-isolated thin-gate for fault containment.",
|
|
10
10
|
"type": "module",
|
|
11
11
|
"keywords": [
|
|
12
12
|
"llm",
|
|
@@ -135,11 +135,11 @@
|
|
|
135
135
|
"optionalDependencies": {
|
|
136
136
|
"@opentelemetry/exporter-trace-otlp-grpc": "^0.222.0",
|
|
137
137
|
"@opentelemetry/sdk-node": "^0.222.0",
|
|
138
|
-
"chalk": "^6.0.
|
|
139
|
-
"mohdel-thin-gate-linux-x64-gnu": "3.
|
|
138
|
+
"chalk": "^6.0.1",
|
|
139
|
+
"mohdel-thin-gate-linux-x64-gnu": "3.3.0"
|
|
140
140
|
},
|
|
141
141
|
"dependencies": {
|
|
142
|
-
"@anthropic-ai/sdk": "^0.
|
|
142
|
+
"@anthropic-ai/sdk": "^0.129.0",
|
|
143
143
|
"@cerebras/cerebras_cloud_sdk": "^1.91.0",
|
|
144
144
|
"@clack/prompts": "^1.8.1",
|
|
145
145
|
"@google/genai": "^2.24.0",
|
|
@@ -154,7 +154,7 @@
|
|
|
154
154
|
},
|
|
155
155
|
"devDependencies": {
|
|
156
156
|
"gpt-tokenizer": "^4.0.0",
|
|
157
|
-
"lint-staged": "^17.
|
|
157
|
+
"lint-staged": "^17.6.0",
|
|
158
158
|
"release-it": "^21.0.3",
|
|
159
159
|
"standard": "^17.1.2",
|
|
160
160
|
"typescript": "^7.0.2",
|
package/src/cli/ask.js
CHANGED
|
@@ -82,7 +82,7 @@ Output:
|
|
|
82
82
|
|
|
83
83
|
Examples:
|
|
84
84
|
mo ask openai/gpt-5.6-luna "why is the sky blue"
|
|
85
|
-
cat article.txt | mo ask anthropic/claude-sonnet-
|
|
85
|
+
cat article.txt | mo ask anthropic/claude-sonnet-5-5 "summarize this"
|
|
86
86
|
mo ask openai/gpt-5.4 --effort high "explain monads" --json | jq .cost`)
|
|
87
87
|
process.exit(0)
|
|
88
88
|
}
|
package/src/cli/bench.js
CHANGED
|
@@ -24,7 +24,7 @@ Options:
|
|
|
24
24
|
--json Output as JSON (single model only)
|
|
25
25
|
|
|
26
26
|
Examples:
|
|
27
|
-
mo bench anthropic/claude-sonnet-
|
|
27
|
+
mo bench anthropic/claude-sonnet-5-5
|
|
28
28
|
mo bench --tag fast --effort low
|
|
29
29
|
mo bench openai/gpt-5 --budget 8000 --save results.json`)
|
|
30
30
|
process.exit(0)
|
package/src/cli/complete.js
CHANGED
|
@@ -9,7 +9,7 @@ const NOUNS = ['model', 'provider', 'creator', 'tag', 'ratelimit', 'ask', 'trans
|
|
|
9
9
|
|
|
10
10
|
const VERBS = {
|
|
11
11
|
model: ['list', 'search', 'stats', 'show', 'get', 'set', 'rm', 'add', 'instructions',
|
|
12
|
-
'check', 'apply', 'rank', 'bench', 'curate', 'backup'],
|
|
12
|
+
'check', 'apply', 'export', 'rank', 'bench', 'curate', 'backup'],
|
|
13
13
|
provider: ['list', 'show', 'models', 'setup', 'rm'],
|
|
14
14
|
creator: ['list', 'show'],
|
|
15
15
|
tag: ['list', 'show', 'add', 'rm'],
|
|
@@ -24,6 +24,7 @@ const ARGUMENT = {
|
|
|
24
24
|
'model set': ['model', 'field'],
|
|
25
25
|
'model rm': ['model', 'field'],
|
|
26
26
|
'model bench': ['model'],
|
|
27
|
+
'model export': ['model'],
|
|
27
28
|
'model curate': ['provider'],
|
|
28
29
|
'model instructions': ['provider'],
|
|
29
30
|
'provider list': ['provider'],
|
package/src/cli/entry.js
CHANGED
|
@@ -139,6 +139,7 @@ Write an entry with your coding agent: mo model instructions <provider>`)
|
|
|
139
139
|
if (!nothingToDo && !args.includes('--yes')) {
|
|
140
140
|
if (!process.stdout.isTTY || path === '-') {
|
|
141
141
|
console.error(`\n${err('Not written')} — no terminal to confirm on. Re-run in a terminal, or pass --yes.`)
|
|
142
|
+
if (path === '-') console.error(`${meta('Preview without writing:')} mo model check --entry -`)
|
|
142
143
|
process.exit(1)
|
|
143
144
|
}
|
|
144
145
|
const { confirm, isCancel } = await import('@clack/prompts')
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { catalogEntries } from '../lib/common.js'
|
|
2
|
+
import { loadCuratedCache, getCuratedCacheSnapshot, expandModelAliasSync } from '../lib/curated-cache.js'
|
|
3
|
+
import { providerOf } from '#core/model-id.js'
|
|
4
|
+
import { err } from './colors.js'
|
|
5
|
+
|
|
6
|
+
const USAGE = 'Usage: mo model export <id…> [--provider <p>] [--tag <t>] [--with-redirects]'
|
|
7
|
+
|
|
8
|
+
const HELP = `mohdel model export — print catalog entries for another host's \`mo model apply\`
|
|
9
|
+
|
|
10
|
+
${USAGE}
|
|
11
|
+
|
|
12
|
+
Prints { "<provider>/<model>": <entry> } as written in curated.json. Ids resolve
|
|
13
|
+
through aliases; one that resolves to nothing fails the command and prints nothing.
|
|
14
|
+
|
|
15
|
+
--provider <p> Every entry of a provider (repeatable)
|
|
16
|
+
--tag <t> Every entry carrying a tag (repeatable)
|
|
17
|
+
--with-redirects Add the deprecated stubs whose chain ends at an exported entry
|
|
18
|
+
|
|
19
|
+
Copy to another host — preview, then write (the remote backup is the undo):
|
|
20
|
+
mo model export meta/muse-spark-1.3 | ssh host mo model check --entry -
|
|
21
|
+
mo model export meta/muse-spark-1.3 | ssh host mo model apply - --yes`
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @param {Record<string, any>} catalog
|
|
25
|
+
* @param {{ ids?: string[], providers?: string[], tags?: string[], withRedirects?: boolean }} selection
|
|
26
|
+
* @param {(id: string) => string} [resolve]
|
|
27
|
+
* @returns {{ entries: Record<string, any>, unknown: string[] }}
|
|
28
|
+
*/
|
|
29
|
+
export const selectEntries = (catalog, { ids = [], providers = [], tags = [], withRedirects = false }, resolve = expandModelAliasSync) => {
|
|
30
|
+
const keys = new Set()
|
|
31
|
+
const unknown = []
|
|
32
|
+
|
|
33
|
+
for (const requested of ids) {
|
|
34
|
+
const key = resolve(requested)
|
|
35
|
+
if (catalog[key]) keys.add(key)
|
|
36
|
+
else unknown.push(requested)
|
|
37
|
+
}
|
|
38
|
+
for (const [key, entry] of catalogEntries(catalog)) {
|
|
39
|
+
if (providers.includes(providerOf(key))) keys.add(key)
|
|
40
|
+
if ((entry.tags || []).some(t => tags.includes(t))) keys.add(key)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (withRedirects) {
|
|
44
|
+
for (const [key, entry] of catalogEntries(catalog)) {
|
|
45
|
+
if (!entry.deprecated || keys.has(key)) continue
|
|
46
|
+
const seen = new Set([key])
|
|
47
|
+
let target = entry.deprecated
|
|
48
|
+
while (catalog[target]?.deprecated && !seen.has(target)) {
|
|
49
|
+
seen.add(target)
|
|
50
|
+
target = catalog[target].deprecated
|
|
51
|
+
}
|
|
52
|
+
if (keys.has(target)) {
|
|
53
|
+
for (const hop of seen) keys.add(hop)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const entries = {}
|
|
59
|
+
for (const key of [...keys].sort()) entries[key] = catalog[key]
|
|
60
|
+
return { entries, unknown }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const usageError = (message) => {
|
|
64
|
+
console.error(err(message))
|
|
65
|
+
console.error(USAGE)
|
|
66
|
+
process.exit(1)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const parseArgs = (args) => {
|
|
70
|
+
const selection = { ids: [], providers: [], tags: [], withRedirects: false }
|
|
71
|
+
for (let i = 0; i < args.length; i++) {
|
|
72
|
+
const arg = args[i]
|
|
73
|
+
if (arg === '--provider' || arg === '--tag') {
|
|
74
|
+
const value = args[++i]
|
|
75
|
+
if (!value || value.startsWith('-')) usageError(`${arg} needs a value`)
|
|
76
|
+
selection[arg === '--provider' ? 'providers' : 'tags'].push(value)
|
|
77
|
+
} else if (arg === '--with-redirects') {
|
|
78
|
+
selection.withRedirects = true
|
|
79
|
+
} else if (arg.startsWith('-')) {
|
|
80
|
+
usageError(`Unknown flag: ${arg}`)
|
|
81
|
+
} else {
|
|
82
|
+
selection.ids.push(arg)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return selection
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export async function runExport (args) {
|
|
89
|
+
if (args.includes('-h') || args.includes('--help')) {
|
|
90
|
+
console.log(HELP)
|
|
91
|
+
return
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const { ids, providers, tags, withRedirects } = parseArgs(args)
|
|
95
|
+
|
|
96
|
+
if (!ids.length && !providers.length && !tags.length) {
|
|
97
|
+
console.error(USAGE)
|
|
98
|
+
process.exit(1)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
await loadCuratedCache()
|
|
102
|
+
const { entries, unknown } = selectEntries(getCuratedCacheSnapshot(), { ids, providers, tags, withRedirects })
|
|
103
|
+
|
|
104
|
+
if (unknown.length) {
|
|
105
|
+
console.error(err(`Not in the catalog: ${unknown.join(', ')}`))
|
|
106
|
+
process.exit(1)
|
|
107
|
+
}
|
|
108
|
+
if (!Object.keys(entries).length) {
|
|
109
|
+
console.error(err('No entry matched.'))
|
|
110
|
+
process.exit(1)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
process.stdout.write(`${JSON.stringify(entries, null, 2)}\n`)
|
|
114
|
+
}
|
package/src/cli/model.js
CHANGED
|
@@ -42,6 +42,7 @@ Usage:
|
|
|
42
42
|
model instructions [provider] Print a brief for your coding agent
|
|
43
43
|
model check [--entry <file|->] Validate catalog or candidate entries
|
|
44
44
|
model apply <file|-> Write reviewed entries to the catalog
|
|
45
|
+
model export <id…> Print entries for another host's apply
|
|
45
46
|
model backup list|restore|diff Catalog backups: prev, daily, weekly
|
|
46
47
|
model rank [options] Rank models by benchmark performance
|
|
47
48
|
model bench <model> [options] Benchmark a model with live inference
|
|
@@ -377,6 +378,12 @@ config/curated.example.json for ready-to-copy entries.`)
|
|
|
377
378
|
return
|
|
378
379
|
}
|
|
379
380
|
|
|
381
|
+
if (action === 'export') {
|
|
382
|
+
const { runExport } = await import('./export.js')
|
|
383
|
+
await runExport(rawArgs.slice(1))
|
|
384
|
+
return
|
|
385
|
+
}
|
|
386
|
+
|
|
380
387
|
if (action === 'instructions') {
|
|
381
388
|
const { runInstructions } = await import('./instructions.js')
|
|
382
389
|
await runInstructions(rawArgs.slice(1))
|
package/src/lib/creators.js
CHANGED
|
@@ -36,7 +36,7 @@ const creators = {
|
|
|
36
36
|
description: 'Kuaishou\'s KwaiPilot team builds KAT-Coder, a MoE coding model with strong agentic and multi-step reasoning for software engineering tasks.'
|
|
37
37
|
},
|
|
38
38
|
meta: {
|
|
39
|
-
prefixes: ['llama', 'code-llama'],
|
|
39
|
+
prefixes: ['llama', 'code-llama', 'muse'],
|
|
40
40
|
label: 'Meta',
|
|
41
41
|
logo: 'meta.svg',
|
|
42
42
|
description: 'Meta stewards the Llama ecosystem with open, widely adoptable models for chat, coding, and research.'
|
package/src/lib/index.js
CHANGED
|
@@ -402,7 +402,7 @@ const mohdel = async ({ logger, verbosity: verbosityOpt, onSuccess, onFailure, c
|
|
|
402
402
|
if (!modelSpec.thinkingEffortLevels) {
|
|
403
403
|
throw new Error(`Model '${resolvedModelId}' does not support output effort (no thinkingEffortLevels). Cannot use ':${aliasOutputEffort}' suffix.`)
|
|
404
404
|
}
|
|
405
|
-
if (
|
|
405
|
+
if (!Object.hasOwn(modelSpec.thinkingEffortLevels, aliasOutputEffort)) {
|
|
406
406
|
throw new Error(`Model '${resolvedModelId}' does not support output effort level '${aliasOutputEffort}'. Available: ${Object.keys(modelSpec.thinkingEffortLevels).join(', ')}`)
|
|
407
407
|
}
|
|
408
408
|
}
|
package/src/lib/provider-info.js
CHANGED
|
@@ -79,6 +79,13 @@ const PROVIDER_INFO = {
|
|
|
79
79
|
hint: 'Create an API key at novita.ai → Dashboard → API Key',
|
|
80
80
|
free: false
|
|
81
81
|
},
|
|
82
|
+
meta: {
|
|
83
|
+
label: 'Meta Model API',
|
|
84
|
+
description: 'Muse Spark — reasoning, long context, image, video and PDF input.',
|
|
85
|
+
url: 'https://dev.meta.ai/',
|
|
86
|
+
hint: 'Create an API key in the Meta Model API console at dev.meta.ai',
|
|
87
|
+
free: false
|
|
88
|
+
},
|
|
82
89
|
xiaomi: {
|
|
83
90
|
label: 'Xiaomi MiMo',
|
|
84
91
|
description: 'MiMo — vision and text models.',
|
package/src/lib/providers.js
CHANGED
|
@@ -86,6 +86,18 @@ const providers = {
|
|
|
86
86
|
contextSemantics: 'shared',
|
|
87
87
|
outputCapStrategy: 'accept'
|
|
88
88
|
},
|
|
89
|
+
meta: {
|
|
90
|
+
sdk: 'openai',
|
|
91
|
+
apiKeyEnv: 'META_API_SK',
|
|
92
|
+
baseURL: 'https://api.meta.ai/v1',
|
|
93
|
+
createConfiguration: apiKey => ({ apiKey }),
|
|
94
|
+
references: {
|
|
95
|
+
pricing: 'https://dev.meta.ai/docs/pricing-rate-limits',
|
|
96
|
+
models: 'https://dev.meta.ai/docs/models',
|
|
97
|
+
rateLimits: 'https://dev.meta.ai/docs/pricing-rate-limits'
|
|
98
|
+
},
|
|
99
|
+
outputCapStrategy: 'accept'
|
|
100
|
+
},
|
|
89
101
|
cohere: {
|
|
90
102
|
sdk: 'cohere',
|
|
91
103
|
api: 'embeddings',
|