mohdel 3.2.0 → 3.4.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 CHANGED
@@ -27,7 +27,7 @@ Mohdel runs the inference layer of production stacks, among them [docAnalyzer](h
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-4-6` to `openai/gpt-5.4-mini` is one string change — adapter differences stay inside mohdel.
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-4-6
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-4-6 "explain monads"
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-4-6 --stream "write a haiku about recursion"
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-4-6 --effort high "prove P != NP"
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-4-6 # model details
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-4-6 60 100000
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-4-6 # single model
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-4-6').answer('Hello')
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
 
@@ -17,18 +17,18 @@
17
17
  "tags": ["fast", "chat"]
18
18
  },
19
19
 
20
- "anthropic/claude-sonnet-4-6": {
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-4-6",
22
+ "model": "claude-sonnet-5-5",
23
23
  "creator": "anthropic",
24
24
  "provider": "anthropic",
25
25
  "sdk": "anthropic",
26
- "label": "Claude Sonnet 4.6",
26
+ "label": "Claude Sonnet 5.5",
27
27
  "inputFormat": ["text", "image"],
28
- "inputPrice": 3,
29
- "outputPrice": 15,
30
- "cacheWritePrice": 3.75,
31
- "cacheReadPrice": 0.30,
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-4-6"
47
+ "deprecated": "anthropic/claude-sonnet-5-5"
48
48
  },
49
49
 
50
50
  "novita/flux-2-dev": {
@@ -33,7 +33,7 @@
33
33
  "properties": {
34
34
  "deprecated": {
35
35
  "type": "string",
36
- "description": "Replacement model id (e.g. 'anthropic/claude-sonnet-4-6')."
36
+ "description": "Replacement model id (e.g. 'anthropic/claude-sonnet-5-5')."
37
37
  }
38
38
  }
39
39
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mohdel",
3
- "version": "3.2.0",
3
+ "version": "3.4.0",
4
4
  "license": "MIT",
5
5
  "author": {
6
6
  "name": "Christophe Le Bars",
@@ -136,7 +136,7 @@
136
136
  "@opentelemetry/exporter-trace-otlp-grpc": "^0.222.0",
137
137
  "@opentelemetry/sdk-node": "^0.222.0",
138
138
  "chalk": "^6.0.1",
139
- "mohdel-thin-gate-linux-x64-gnu": "3.2.0"
139
+ "mohdel-thin-gate-linux-x64-gnu": "3.4.0"
140
140
  },
141
141
  "dependencies": {
142
142
  "@anthropic-ai/sdk": "^0.129.0",
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-4-6 "summarize this"
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-4-6
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)
@@ -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/index.js CHANGED
@@ -40,7 +40,7 @@ if (command === '-h' || command === '--help') {
40
40
  const { default: providerDefs } = await import('../lib/providers.js')
41
41
  const keyRows = Object.keys(PROVIDER_INFO)
42
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']])
43
+ .concat([[providerDefs.local.optionalApiKeyEnv, 'Bearer token for a local server, if it wants one']])
44
44
  const keyWidth = Math.max(...keyRows.map(([k]) => k.length))
45
45
  const keys = keyRows.map(([k, d]) => ` ${k.padEnd(keyWidth + 2)}${d}`).join('\n')
46
46
  console.log(`mohdel — self-hosted LLM gateway and SDK for Node
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))
@@ -1,3 +1,5 @@
1
+ const LOCAL_API_KEY_ENV = 'MOHDEL_LOCAL_API_SK'
2
+
1
3
  // `contextSemantics` and `outputCapStrategy` are published facts about a
2
4
  // provider, not switches: mohdel caps `outputBudget` to the model's
3
5
  // `outputTokenLimit` whatever they say. They exist so an embedder building its
@@ -82,7 +84,8 @@ const providers = {
82
84
  sdk: 'openai',
83
85
  api: 'chatCompletions',
84
86
  catalog: false,
85
- resolveConfiguration: () => ({ apiKey: process.env.MOHDEL_LOCAL_API_SK || '' }),
87
+ optionalApiKeyEnv: LOCAL_API_KEY_ENV,
88
+ resolveConfiguration: () => ({ apiKey: process.env[LOCAL_API_KEY_ENV] || '' }),
86
89
  contextSemantics: 'shared',
87
90
  outputCapStrategy: 'accept'
88
91
  },