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.
Files changed (45) hide show
  1. package/README.md +155 -30
  2. package/config/curated.schema.json +32 -10
  3. package/js/client/call.js +76 -19
  4. package/js/client/gate-binary.js +5 -0
  5. package/js/client/index.js +1 -0
  6. package/js/core/envelope.js +5 -1
  7. package/js/factory/bridge.js +2 -2
  8. package/js/session/adapters/_cancelled.js +0 -6
  9. package/js/session/adapters/_chat_completions.js +25 -0
  10. package/js/session/adapters/_output_cap.js +30 -0
  11. package/js/session/adapters/_registry.js +44 -0
  12. package/js/session/adapters/anthropic.js +8 -5
  13. package/js/session/adapters/gemini.js +4 -0
  14. package/js/session/adapters/openai.js +2 -1
  15. package/js/session/run.js +14 -4
  16. package/js/session/run_image.js +12 -3
  17. package/package.json +49 -19
  18. package/src/cli/aliases.js +20 -0
  19. package/src/cli/ask.js +58 -12
  20. package/src/cli/backup.js +2 -1
  21. package/src/cli/check.js +15 -86
  22. package/src/cli/complete.js +130 -0
  23. package/src/cli/default.js +34 -13
  24. package/src/cli/doctor.js +33 -14
  25. package/src/cli/entry.js +173 -0
  26. package/src/cli/index.js +77 -66
  27. package/src/cli/instructions.js +349 -0
  28. package/src/cli/local.js +14 -0
  29. package/src/cli/model.js +184 -37
  30. package/src/cli/onboard.js +186 -121
  31. package/src/cli/rank.js +2 -1
  32. package/src/cli/ratelimit.js +3 -3
  33. package/src/cli/tag.js +2 -0
  34. package/src/lib/assistants.js +93 -0
  35. package/src/lib/catalog/openrouter.js +6 -1
  36. package/src/lib/catalog-review.js +195 -0
  37. package/src/lib/common.js +14 -1
  38. package/src/lib/creators.js +35 -0
  39. package/src/lib/index.js +17 -3
  40. package/src/lib/local-conventions.js +120 -0
  41. package/src/lib/provider-info.js +98 -0
  42. package/src/lib/providers.js +69 -14
  43. package/src/lib/schema.js +15 -3
  44. package/src/lib/select.js +125 -67
  45. package/js/session/adapters/image/index.js +0 -40
package/src/lib/common.js CHANGED
@@ -1,4 +1,5 @@
1
- import { join } from 'path'
1
+ import { join, sep } from 'path'
2
+ import { homedir } from 'os'
2
3
  import { existsSync } from 'fs'
3
4
  import { readFile, writeFile, mkdir, copyFile, stat } from 'fs/promises'
4
5
  import envPaths from 'env-paths'
@@ -17,6 +18,18 @@ export const EXCLUDED_PATH = join(CONFIG_DIR, 'excluded.json')
17
18
  export const PROVIDERS_CONFIG_PATH = join(CONFIG_DIR, 'providers.json')
18
19
  export const ENV_PATH = join(CONFIG_DIR, 'environment')
19
20
 
21
+ export const tildePath = (path) => {
22
+ const home = homedir()
23
+ return home && path.startsWith(home + sep) ? '~' + path.slice(home.length) : path
24
+ }
25
+
26
+ // A path that must stay readable off this machine: the portable form leads,
27
+ // the resolved one follows only when they differ.
28
+ export const portablePath = (path) => {
29
+ const short = tildePath(path)
30
+ return short === path ? path : `${short} (${path} on this machine)`
31
+ }
32
+
20
33
  // Meta keys (e.g. $schema for JSON Schema editors, _* for inline notes) live at
21
34
  // the top level of curated.json alongside model entries. They're preserved on
22
35
  // load/save but excluded from every iteration site so they don't pollute alias
@@ -1,70 +1,84 @@
1
1
  const creators = {
2
2
  alibaba: {
3
+ prefixes: ['qwen', 'qwq'],
3
4
  label: 'Alibaba',
4
5
  logo: 'alibaba.svg',
5
6
  description: 'Alibaba Cloud’s Qwen models target large-scale enterprise scenarios with strong multilingual and commerce-focused capabilities.'
6
7
  },
7
8
  bfl: {
9
+ prefixes: ['flux'],
8
10
  label: 'Black Forest Labs',
9
11
  logo: 'bfl.svg',
10
12
  description: 'Black Forest Labs builds the Flux family of image generation models, delivering fast, high-quality text-to-image synthesis.'
11
13
  },
12
14
  anthropic: {
15
+ prefixes: ['claude'],
13
16
  label: 'Anthropic',
14
17
  logo: 'anthropic.svg',
15
18
  description: 'Anthropic builds Claude models that emphasize safe reasoning, tool use, and reliable outputs for production assistants.'
16
19
  },
17
20
  deepseek: {
21
+ prefixes: ['deepseek'],
18
22
  label: 'DeepSeek',
19
23
  logo: 'deepseek.svg',
20
24
  description: 'DeepSeek offers fast, cost-efficient foundation models optimized for coding, chat, and multilingual reasoning.'
21
25
  },
22
26
  google: {
27
+ prefixes: ['gemini', 'gemma', 'imagen', 'nano-banana'],
23
28
  label: 'Google',
24
29
  logo: 'gemini.svg',
25
30
  description: 'Google’s Gemini family blends multimodal understanding, coding assistance, and long-context reasoning from Google DeepMind research.'
26
31
  },
27
32
  kwaipilot: {
33
+ prefixes: ['kwaipilot', 'kat-coder'],
28
34
  label: 'Kwaipilot',
29
35
  logo: 'kwaipilot.svg',
30
36
  description: 'Kuaishou\'s KwaiPilot team builds KAT-Coder, a MoE coding model with strong agentic and multi-step reasoning for software engineering tasks.'
31
37
  },
32
38
  meta: {
39
+ prefixes: ['llama', 'code-llama'],
33
40
  label: 'Meta',
34
41
  logo: 'meta.svg',
35
42
  description: 'Meta stewards the Llama ecosystem with open, widely adoptable models for chat, coding, and research.'
36
43
  },
37
44
  minimax: {
45
+ prefixes: ['minimax'],
38
46
  label: 'Minimax',
39
47
  logo: 'minimax.svg',
40
48
  description: 'Minimax offers versatile Chinese-first chat and coding models tuned for fast, cost-aware assistants and enterprise integrations.'
41
49
  },
42
50
  mistral: {
51
+ prefixes: ['mistral', 'codestral', 'pixtral', 'ministral', 'magistral', 'voxtral', 'devstral'],
43
52
  label: 'Mistral',
44
53
  logo: 'mistral.svg',
45
54
  description: 'Mistral AI ships strong open-weight and proprietary models with a focus on European hosting, multilingual quality, and efficient deployment.'
46
55
  },
47
56
  moonshotai: {
57
+ prefixes: ['moonshotai', 'kimi'],
48
58
  label: 'Moonshot AI',
49
59
  logo: 'moonshotai.svg',
50
60
  description: 'Moonshot AI ships fluent, Chinese-first assistants and lean models tuned for consumer chat and business workflows.'
51
61
  },
52
62
  openai: {
63
+ prefixes: ['gpt', 'whisper', 'dall-e', 'sora', 'text-embedding', 'o1', 'o3', 'o4'],
53
64
  label: 'OpenAI',
54
65
  logo: 'openai.svg',
55
66
  description: 'OpenAI’s GPT and o-series models focus on broad tool-use, reasoning quality, and multimodal support across developer platforms.'
56
67
  },
57
68
  xai: {
69
+ prefixes: ['grok'],
58
70
  label: 'xAI',
59
71
  logo: 'xai.svg',
60
72
  description: 'xAI develops Grok with real-time, web-aware chat and coding behavior aimed at terse, fast responses.'
61
73
  },
62
74
  xiaomi: {
75
+ prefixes: ['mimo'],
63
76
  label: 'Xiaomi',
64
77
  logo: 'xiaomi.svg',
65
78
  description: 'Xiaomi develops MiMo, a high-efficiency MoE reasoning model optimized for agentic coding and tool use at low inference cost.'
66
79
  },
67
80
  zai: {
81
+ prefixes: ['zai', 'zai-org', 'glm'],
68
82
  label: 'Z AI',
69
83
  logo: 'zai.svg',
70
84
  description: 'Z AI delivers streamlined assistants with lightweight models oriented toward pragmatic productivity use cases.'
@@ -73,4 +87,25 @@ const creators = {
73
87
 
74
88
  Object.freeze(creators)
75
89
 
90
+ // Which organisation trained a model, guessed from its id. Two signals: a
91
+ // router-style id carries the creator as a namespace (`moonshotai/kimi-k3`),
92
+ // and a bare id usually starts with a family name (`claude-`, `qwen3-`).
93
+ // A guess is only ever offered as a default — it is never written unasked.
94
+ const startsWithToken = (id, prefix) => {
95
+ if (!id.startsWith(prefix)) return false
96
+ const next = id[prefix.length]
97
+ return next === undefined || !/[a-z]/.test(next)
98
+ }
99
+
100
+ export const creatorFromModelId = (bare) => {
101
+ const segments = String(bare).toLowerCase().split('/')
102
+ const candidates = segments.length > 1 ? [segments[0], segments[segments.length - 1]] : segments
103
+ for (const segment of candidates) {
104
+ for (const [name, def] of Object.entries(creators)) {
105
+ if ((def.prefixes || []).some(p => startsWithToken(segment, p))) return name
106
+ }
107
+ }
108
+ return null
109
+ }
110
+
76
111
  export default creators
package/src/lib/index.js CHANGED
@@ -170,7 +170,7 @@ const resolveProviderConfiguration = async (provider, providerName) => {
170
170
  // unchanged.
171
171
  const buildHandlers = ({ logger, onSuccess, onFailure }) => {
172
172
  const log = logger || silent
173
- return {
173
+ const bag = {
174
174
  trace: typeof log.trace === 'function' ? (...args) => log.trace(...args) : noop,
175
175
  debug: typeof log.debug === 'function' ? (...args) => log.debug(...args) : noop,
176
176
  info: typeof log.info === 'function' ? (...args) => log.info(...args) : noop,
@@ -180,6 +180,13 @@ const buildHandlers = ({ logger, onSuccess, onFailure }) => {
180
180
  onSuccess,
181
181
  onFailure
182
182
  }
183
+ // The session scopes its logger with per-call context before using it. A
184
+ // logger that understands context gets it; one that does not keeps its
185
+ // levels rather than being replaced by the session's own default.
186
+ bag.withContext = typeof log.withContext === 'function'
187
+ ? (context) => buildHandlers({ logger: log.withContext(context), onSuccess, onFailure })
188
+ : () => bag
189
+ return bag
183
190
  }
184
191
 
185
192
  // @internal — exported under an underscore-prefixed alias for unit tests only.
@@ -335,7 +342,14 @@ const mohdel = async ({ logger, verbosity: verbosityOpt, onSuccess, onFailure, c
335
342
  if (!modelSpec) {
336
343
  const suggestions = suggestModels(modelId)
337
344
  let msg = `Model '${modelId}' not found in catalog.`
338
- if (suggestions.length) {
345
+ // An empty catalog is a fresh install, not a typo. Saying only
346
+ // "not found" leaves a library caller with nowhere to go — the
347
+ // CLI prints these next steps, and nothing else did.
348
+ if (!Object.keys(catalog).some(k => !k.startsWith('$') && !k.startsWith('_'))) {
349
+ msg += ' The catalog is empty.' +
350
+ '\n Populate it: mo curate <provider> then mo model instructions <provider>' +
351
+ '\n Or pass one: mohdel({ models: { "<provider>/<model>": { … } } })'
352
+ } else if (suggestions.length) {
339
353
  msg += ' Did you mean?\n' + suggestions.map(s => ` ${s.id} ${s.label}`).join('\n')
340
354
  }
341
355
  throw new Error(msg)
@@ -581,7 +595,7 @@ const createModelProxy = (resolvedModelId, modelSpec, handlers, aliasOutputEffor
581
595
  configuration: effectiveConfiguration,
582
596
  prompt,
583
597
  options: sdkOptions
584
- }, { cooldown, limiter: rateLimiter, resolveProviderLimits })
598
+ }, { cooldown, limiter: rateLimiter, resolveProviderLimits, logger: handlers })
585
599
 
586
600
  // End span with result attributes
587
601
  const endAttrs = {
@@ -0,0 +1,120 @@
1
+ import { existsSync } from 'node:fs'
2
+ import { readFile } from 'node:fs/promises'
3
+ import { join } from 'node:path'
4
+ import { CONFIG_DIR, isMetaKey } from './common.js'
5
+ import { fieldDefs, isValidTag } from './schema.js'
6
+
7
+ export const LOCAL_PATH = join(CONFIG_DIR, 'catalog.local.json')
8
+
9
+ const TOP_LEVEL = ['fields', 'tags', 'adding', 'notes']
10
+ const FIELD_KEYS = ['type', 'description', 'measured', 'readBy']
11
+ const TAG_KEYS = ['description', 'requires', 'severity']
12
+ const TYPES = ['string', 'number', 'boolean', 'array', 'object']
13
+ const SEVERITIES = ['error', 'warn']
14
+
15
+ const isPlainObject = (v) => typeof v === 'object' && v !== null && !Array.isArray(v)
16
+
17
+ export const parseLocalConventions = (text) => {
18
+ let doc
19
+ try {
20
+ doc = JSON.parse(text)
21
+ } catch (e) {
22
+ throw new Error(`not valid JSON: ${e.message}`)
23
+ }
24
+ if (!isPlainObject(doc)) throw new Error('must be a JSON object')
25
+
26
+ for (const key of Object.keys(doc)) {
27
+ if (isMetaKey(key)) continue
28
+ if (!TOP_LEVEL.includes(key)) {
29
+ throw new Error(`unknown key '${key}' (expected: ${TOP_LEVEL.join(', ')})`)
30
+ }
31
+ }
32
+
33
+ const fields = {}
34
+ for (const [name, def] of Object.entries(doc.fields || {})) {
35
+ if (!isPlainObject(def)) throw new Error(`fields.${name} must be an object`)
36
+ for (const key of Object.keys(def)) {
37
+ if (!FIELD_KEYS.includes(key)) throw new Error(`fields.${name}.${key} is not a known key (expected: ${FIELD_KEYS.join(', ')})`)
38
+ }
39
+ if (fieldDefs[name]) throw new Error(`fields.${name} is already a mohdel field — remove it, mohdel validates it already`)
40
+ if (!TYPES.includes(def.type)) throw new Error(`fields.${name}.type must be one of ${TYPES.join(', ')}`)
41
+ if (typeof def.description !== 'string' || !def.description) throw new Error(`fields.${name}.description is required`)
42
+ for (const key of ['measured', 'readBy']) {
43
+ if (def[key] !== undefined && typeof def[key] !== 'string') throw new Error(`fields.${name}.${key} must be a string`)
44
+ }
45
+ fields[name] = def
46
+ }
47
+
48
+ const tags = {}
49
+ for (const [name, def] of Object.entries(doc.tags || {})) {
50
+ if (!isPlainObject(def)) throw new Error(`tags.${name} must be an object`)
51
+ for (const key of Object.keys(def)) {
52
+ if (!TAG_KEYS.includes(key)) throw new Error(`tags.${name}.${key} is not a known key (expected: ${TAG_KEYS.join(', ')})`)
53
+ }
54
+ if (!isValidTag(name)) throw new Error(`tags.${name} is not a valid tag name`)
55
+ if (typeof def.description !== 'string' || !def.description) throw new Error(`tags.${name}.description is required`)
56
+ if (def.severity !== undefined && !SEVERITIES.includes(def.severity)) {
57
+ throw new Error(`tags.${name}.severity must be one of ${SEVERITIES.join(', ')}`)
58
+ }
59
+ if (def.requires !== undefined) {
60
+ if (!Array.isArray(def.requires)) throw new Error(`tags.${name}.requires must be an array of field names`)
61
+ for (const required of def.requires) {
62
+ if (!fieldDefs[required] && !fields[required]) {
63
+ throw new Error(`tags.${name}.requires names '${required}', which is neither a mohdel field nor declared under fields`)
64
+ }
65
+ }
66
+ }
67
+ tags[name] = { severity: 'error', requires: [], ...def }
68
+ }
69
+
70
+ const adding = doc.adding || {}
71
+ if (!isPlainObject(adding)) throw new Error('adding must be an object')
72
+ for (const [key, value] of Object.entries(adding)) {
73
+ if (!['field', 'tag'].includes(key)) throw new Error(`adding.${key} is not a known key (expected: field, tag)`)
74
+ if (typeof value !== 'string') throw new Error(`adding.${key} must be a string`)
75
+ }
76
+
77
+ if (doc.notes !== undefined && typeof doc.notes !== 'string') throw new Error('notes must be a string')
78
+
79
+ return { fields, tags, adding, notes: doc.notes || '' }
80
+ }
81
+
82
+ // Absent is the default state, not an error. A file that exists but does not
83
+ // parse is broken policy — it must not read as "no conventions".
84
+ export const loadLocalConventions = async (path = LOCAL_PATH) => {
85
+ if (!existsSync(path)) return null
86
+ try {
87
+ return parseLocalConventions(await readFile(path, 'utf8'))
88
+ } catch (e) {
89
+ throw new Error(`${path}: ${e.message}`)
90
+ }
91
+ }
92
+
93
+ export const TEMPLATE = `{
94
+ "_comment": "Declares what THIS installation adds to the mohdel catalog: custom fields your own services read, tags that mean something to them, and how a new one gets introduced. Absent by default; mohdel ships nothing here. See docs/CATALOG.md > Local conventions.",
95
+
96
+ "fields": {
97
+ "yourField": {
98
+ "type": "number",
99
+ "description": "What it means. The coding agent reads this before filling it in.",
100
+ "measured": "the command that produces this value, when it is measured rather than published",
101
+ "readBy": "which of your services read it"
102
+ }
103
+ },
104
+
105
+ "tags": {
106
+ "yourTag": {
107
+ "description": "What applying this tag does in your stack.",
108
+ "requires": ["yourField"],
109
+ "severity": "error"
110
+ }
111
+ },
112
+
113
+ "adding": {
114
+ "field": "How a new custom field is introduced here. Name every step that has to happen before it counts as existing.",
115
+ "tag": "How a new tag is introduced here, and what reads it."
116
+ },
117
+
118
+ "notes": "Anything else the coding agent should know about this catalog."
119
+ }
120
+ `
@@ -0,0 +1,98 @@
1
+ // What first-run setup shows about each provider: how to get a key, and
2
+ // whether you can use it without paying. Kept beside the provider registry
3
+ // rather than inside the onboarding flow, because the brief needs it too.
4
+ const PROVIDER_INFO = {
5
+ gemini: {
6
+ label: 'Google Gemini',
7
+ description: 'Long context, vision, video, audio. Free tier, no card required.',
8
+ url: 'https://aistudio.google.com/apikey',
9
+ hint: 'Create an API key at aistudio.google.com → Get API Key',
10
+ free: true
11
+ },
12
+ groq: {
13
+ label: 'Groq',
14
+ description: 'Open-weight models at the fastest inference available. Free tier, no card required.',
15
+ url: 'https://console.groq.com/keys',
16
+ hint: 'Create an API key at console.groq.com → API Keys',
17
+ free: true
18
+ },
19
+ cerebras: {
20
+ label: 'Cerebras',
21
+ description: 'Llama, Qwen — fast inference on custom hardware. Starter credits for new accounts.',
22
+ url: 'https://cloud.cerebras.ai/platform',
23
+ hint: 'Create an API key at cloud.cerebras.ai → Platform → API Keys',
24
+ free: false
25
+ },
26
+ anthropic: {
27
+ label: 'Anthropic',
28
+ description: 'Claude Opus, Sonnet, Haiku — reasoning, coding, vision, tool use.',
29
+ url: 'https://console.anthropic.com/settings/keys',
30
+ hint: 'Create an API key at console.anthropic.com → Settings → API Keys',
31
+ free: false
32
+ },
33
+ openai: {
34
+ label: 'OpenAI',
35
+ description: 'GPT and reasoning models — vision, image generation, tool use.',
36
+ url: 'https://platform.openai.com/api-keys',
37
+ hint: 'Create an API key at platform.openai.com → API Keys',
38
+ free: false
39
+ },
40
+ xai: {
41
+ label: 'xAI',
42
+ description: 'Grok — reasoning and tool use.',
43
+ url: 'https://console.x.ai',
44
+ hint: 'Create an API key at console.x.ai',
45
+ free: false
46
+ },
47
+ mistral: {
48
+ label: 'Mistral',
49
+ description: 'Mistral Large, Codestral, Pixtral — coding, reasoning, vision. Free tier, no card required.',
50
+ url: 'https://console.mistral.ai/api-keys',
51
+ hint: 'Create an API key at console.mistral.ai → API Keys',
52
+ free: true
53
+ },
54
+ deepseek: {
55
+ label: 'DeepSeek',
56
+ description: 'Reasoning and coding models, at low cost.',
57
+ url: 'https://platform.deepseek.com/api_keys',
58
+ hint: 'Create an API key at platform.deepseek.com → API Keys',
59
+ free: false
60
+ },
61
+ fireworks: {
62
+ label: 'Fireworks',
63
+ description: 'Llama, Qwen, DeepSeek — serverless inference with reasoning.',
64
+ url: 'https://fireworks.ai/account/api-keys',
65
+ hint: 'Create an API key at fireworks.ai → Account → API Keys',
66
+ free: false
67
+ },
68
+ openrouter: {
69
+ label: 'OpenRouter',
70
+ description: 'Multi-provider router — hundreds of models behind one key, some of them at no cost. Free tier, no card required.',
71
+ url: 'https://openrouter.ai/settings/keys',
72
+ hint: 'Create an API key at openrouter.ai → Settings → Keys',
73
+ free: true
74
+ },
75
+ novita: {
76
+ label: 'Novita',
77
+ description: 'Image generation and open-weight inference.',
78
+ url: 'https://novita.ai/dashboard/key',
79
+ hint: 'Create an API key at novita.ai → Dashboard → API Key',
80
+ free: false
81
+ },
82
+ xiaomi: {
83
+ label: 'Xiaomi MiMo',
84
+ description: 'MiMo — vision and text models.',
85
+ url: 'https://platform.xiaomimimo.com/',
86
+ hint: 'Create an API key in the MiMo open platform console',
87
+ free: false
88
+ },
89
+ qwen: {
90
+ label: 'Qwen Cloud',
91
+ description: 'Qwen — reasoning, coding, long context. Free quota for new users.',
92
+ url: 'https://home.qwencloud.com/api-keys',
93
+ hint: 'Create an API key at home.qwencloud.com → API Keys',
94
+ free: false
95
+ }
96
+ }
97
+
98
+ export default PROVIDER_INFO
@@ -1,9 +1,19 @@
1
+ // `contextSemantics` and `outputCapStrategy` are published facts about a
2
+ // provider, not switches: mohdel caps `outputBudget` to the model's
3
+ // `outputTokenLimit` whatever they say. They exist so an embedder building its
4
+ // own provider requests does not have to rediscover the behaviour one 400 at a
5
+ // time. Entries may override `outputCapStrategy` per model. See
6
+ // ARCHITECTURE.md > "The output budget is capped to the model's ceiling".
1
7
  const providers = {
2
8
  anthropic: {
3
9
  sdk: 'anthropic',
4
10
  apiKeyEnv: 'ANTHROPIC_API_SK',
5
11
  createConfiguration: apiKey => ({ apiKey }),
6
- creators: ['anthropic'],
12
+ references: {
13
+ pricing: 'https://platform.claude.com/docs/en/about-claude/pricing',
14
+ models: 'https://platform.claude.com/docs/en/models/overview',
15
+ rateLimits: 'https://platform.claude.com/docs/en/api/rate-limits'
16
+ },
7
17
  contextSemantics: 'shared',
8
18
  outputCapStrategy: 'error'
9
19
  },
@@ -11,7 +21,11 @@ const providers = {
11
21
  sdk: 'cerebras',
12
22
  apiKeyEnv: 'CEREBRAS_API_SK',
13
23
  createConfiguration: apiKey => ({ apiKey }),
14
- creators: ['openai', 'zai'],
24
+ references: {
25
+ pricing: 'https://www.cerebras.ai/pricing',
26
+ models: 'https://inference-docs.cerebras.ai/models/overview',
27
+ rateLimits: 'https://inference-docs.cerebras.ai/support/rate-limits'
28
+ },
15
29
  contextSemantics: 'shared',
16
30
  outputCapStrategy: 'accept'
17
31
  },
@@ -21,7 +35,11 @@ const providers = {
21
35
  apiKeyEnv: 'DEEPSEEK_API_SK',
22
36
  baseURL: 'https://api.deepseek.com',
23
37
  createConfiguration: apiKey => ({ apiKey }),
24
- creators: ['deepseek'],
38
+ references: {
39
+ pricing: 'https://api-docs.deepseek.com/quick_start/pricing',
40
+ models: 'https://api-docs.deepseek.com/quick_start/pricing',
41
+ rateLimits: 'https://api-docs.deepseek.com/quick_start/rate_limit'
42
+ },
25
43
  contextSemantics: 'shared',
26
44
  outputCapStrategy: 'accept'
27
45
  },
@@ -30,7 +48,11 @@ const providers = {
30
48
  apiKeyEnv: 'FIREWORKS_API_SK',
31
49
  baseURL: 'https://api.fireworks.ai/inference/v1',
32
50
  createConfiguration: apiKey => ({ apiKey }),
33
- creators: ['meta', 'alibaba'],
51
+ references: {
52
+ pricing: 'https://fireworks.ai/pricing',
53
+ models: 'https://fireworks.ai/models',
54
+ rateLimits: 'https://docs.fireworks.ai/guides/quotas_usage/account-quotas'
55
+ },
34
56
  contextSemantics: 'shared',
35
57
  outputCapStrategy: 'accept'
36
58
  },
@@ -38,7 +60,11 @@ const providers = {
38
60
  sdk: 'gemini',
39
61
  apiKeyEnv: 'GEMINI_API_SK',
40
62
  createConfiguration: apiKey => ({ apiKey }),
41
- creators: ['google'],
63
+ references: {
64
+ pricing: 'https://ai.google.dev/gemini-api/docs/pricing',
65
+ models: 'https://ai.google.dev/gemini-api/docs/models',
66
+ rateLimits: 'https://ai.google.dev/gemini-api/docs/rate-limits'
67
+ },
42
68
  contextSemantics: 'separate',
43
69
  outputCapStrategy: 'accept'
44
70
  },
@@ -46,14 +72,17 @@ const providers = {
46
72
  sdk: 'groq',
47
73
  apiKeyEnv: 'GROQ_API_SK',
48
74
  createConfiguration: apiKey => ({ apiKey }),
49
- creators: ['meta']
75
+ references: {
76
+ pricing: 'https://console.groq.com/docs/models',
77
+ models: 'https://console.groq.com/docs/models',
78
+ rateLimits: 'https://console.groq.com/docs/rate-limits'
79
+ }
50
80
  },
51
81
  local: {
52
82
  sdk: 'openai',
53
83
  api: 'chatCompletions',
54
84
  catalog: false,
55
85
  resolveConfiguration: () => ({ apiKey: process.env.MOHDEL_LOCAL_API_SK || '' }),
56
- creators: [],
57
86
  contextSemantics: 'shared',
58
87
  outputCapStrategy: 'accept'
59
88
  },
@@ -63,7 +92,11 @@ const providers = {
63
92
  apiKeyEnv: 'MISTRAL_API_SK',
64
93
  baseURL: 'https://api.mistral.ai/v1',
65
94
  createConfiguration: apiKey => ({ apiKey }),
66
- creators: ['mistral']
95
+ references: {
96
+ pricing: 'https://mistral.ai/pricing',
97
+ models: 'https://docs.mistral.ai/models',
98
+ rateLimits: 'https://docs.mistral.ai/admin/billing-usage/usage-limits'
99
+ }
67
100
  },
68
101
  novita: {
69
102
  sdk: 'openai',
@@ -72,7 +105,10 @@ const providers = {
72
105
  apiKeyEnv: 'NOVITA_API_SK',
73
106
  baseURL: 'https://api.novita.ai/openai',
74
107
  createConfiguration: apiKey => ({ apiKey }),
75
- creators: ['deepseek', 'openai', 'bfl'],
108
+ references: {
109
+ pricing: 'https://novita.ai/pricing',
110
+ models: 'https://novita.ai/models'
111
+ },
76
112
  contextSemantics: 'shared',
77
113
  outputCapStrategy: 'error'
78
114
  },
@@ -80,7 +116,11 @@ const providers = {
80
116
  sdk: 'openai',
81
117
  apiKeyEnv: 'OPENAI_API_SK',
82
118
  createConfiguration: apiKey => ({ apiKey }),
83
- creators: ['openai'],
119
+ references: {
120
+ pricing: 'https://developers.openai.com/api/docs/pricing',
121
+ models: 'https://developers.openai.com/api/docs/models',
122
+ rateLimits: 'https://developers.openai.com/api/docs/guides/rate-limits'
123
+ },
84
124
  contextSemantics: 'shared',
85
125
  outputCapStrategy: 'accept'
86
126
  },
@@ -89,7 +129,15 @@ const providers = {
89
129
  apiKeyEnv: 'OPENROUTER_API_SK',
90
130
  baseURL: 'https://openrouter.ai/api/v1',
91
131
  createConfiguration: apiKey => ({ apiKey }),
92
- creators: []
132
+ // Alone among the providers, OpenRouter's model list carries per-token
133
+ // prices, so `mo curate openrouter` writes complete entries with no
134
+ // pricing page to read.
135
+ pricesFromApi: true,
136
+ references: {
137
+ pricing: 'https://openrouter.ai/models',
138
+ models: 'https://openrouter.ai/models',
139
+ rateLimits: 'https://openrouter.ai/docs/api_reference/limits'
140
+ }
93
141
  },
94
142
  qwen: {
95
143
  sdk: 'openai',
@@ -97,7 +145,11 @@ const providers = {
97
145
  apiKeyEnv: 'QWEN_API_SK',
98
146
  baseURL: 'https://dashscope-intl.aliyuncs.com/compatible-mode/v1',
99
147
  createConfiguration: apiKey => ({ apiKey }),
100
- creators: ['alibaba'],
148
+ references: {
149
+ pricing: 'https://www.alibabacloud.com/help/en/model-studio/models',
150
+ models: 'https://www.alibabacloud.com/help/en/model-studio/models',
151
+ rateLimits: 'https://www.alibabacloud.com/help/en/model-studio/rate-limit'
152
+ },
101
153
  contextSemantics: 'shared',
102
154
  outputCapStrategy: 'accept'
103
155
  },
@@ -106,7 +158,11 @@ const providers = {
106
158
  apiKeyEnv: 'XAI_API_SK',
107
159
  baseURL: 'https://api.x.ai/v1',
108
160
  createConfiguration: apiKey => ({ apiKey }),
109
- creators: ['xai'],
161
+ references: {
162
+ pricing: 'https://docs.x.ai/developers/models',
163
+ models: 'https://docs.x.ai/developers/models',
164
+ rateLimits: 'https://docs.x.ai/developers/rate-limits'
165
+ },
110
166
  contextSemantics: 'shared',
111
167
  outputCapStrategy: 'accept'
112
168
  },
@@ -116,7 +172,6 @@ const providers = {
116
172
  apiKeyEnv: 'XIAOMI_API_SK',
117
173
  baseURL: 'https://api.xiaomimimo.com/v1',
118
174
  createConfiguration: apiKey => ({ apiKey }),
119
- creators: ['xiaomi'],
120
175
  contextSemantics: 'shared',
121
176
  outputCapStrategy: 'accept'
122
177
  }
package/src/lib/schema.js CHANGED
@@ -15,6 +15,13 @@ const validateSpeeds = (speeds) => {
15
15
  return null
16
16
  }
17
17
 
18
+ const INPUT_FORMATS = ['text', 'image', 'video', 'audio']
19
+
20
+ const validateInputFormat = (formats) => {
21
+ const unknown = formats.filter(f => !INPUT_FORMATS.includes(f))
22
+ return unknown.length ? `unknown modality '${unknown.join("', '")}' (allowed: ${INPUT_FORMATS.join(', ')})` : null
23
+ }
24
+
18
25
  const fieldDefs = {
19
26
  model: { type: 'string', required: true },
20
27
  baseURL: { type: 'string' },
@@ -32,6 +39,7 @@ const fieldDefs = {
32
39
  cacheWritePrice: { type: 'number', altType: 'object' },
33
40
  cacheWrite1hPrice: { type: 'number', altType: 'object' },
34
41
  contextTokenLimit: { type: 'number' },
42
+ inputCeilingMargin: { type: 'number' },
35
43
  outputTokenLimit: { type: 'number' },
36
44
  thinkingTokenLimit: { type: 'number' },
37
45
  thinkingEffortLevels: { type: 'object', nullable: true, default: null },
@@ -42,7 +50,7 @@ const fieldDefs = {
42
50
  replaces: { type: 'array', itemType: 'string', default: [] },
43
51
  leaderboard: { type: 'array', itemType: 'number', validate: (v) => Array.isArray(v) && v.length === 3 ? null : 'must be [intelligence, speed, latency]' },
44
52
  leaderboardNote: { type: 'string' },
45
- inputFormat: { type: 'array', itemType: 'string', required: true, default: ['text'] },
53
+ inputFormat: { type: 'array', itemType: 'string', required: true, default: ['text'], validate: validateInputFormat, severity: 'error' },
46
54
  version: { type: 'string' },
47
55
  createdAt: { type: 'string' },
48
56
  created: { type: 'number' },
@@ -55,7 +63,11 @@ const fieldDefs = {
55
63
  rpmLimit: { type: 'number' },
56
64
  tpmLimit: { type: 'number' },
57
65
  rateLimitScope: { type: 'string', validate: (v) => ['model', 'provider'].includes(v) ? null : 'must be "model" or "provider"' },
58
- supportsTools: { type: 'boolean' }
66
+ outputCapStrategy: { type: 'string', validate: (v) => ['error', 'accept'].includes(v) ? null : "must be 'error' or 'accept'" },
67
+ supportsTools: { type: 'boolean' },
68
+ reasoningContentPlaceholder: { type: 'string' },
69
+ source: { type: 'string' },
70
+ sourcedAt: { type: 'string' }
59
71
  }
60
72
 
61
73
  const knownFields = new Set(Object.keys(fieldDefs))
@@ -106,7 +118,7 @@ const validate = (entry, curatedKey, { strict = false } = {}) => {
106
118
  if (def.validate) {
107
119
  const msg = def.validate(value)
108
120
  if (msg) {
109
- issues.push({ field, message: msg, severity: 'warn' })
121
+ issues.push({ field, message: msg, severity: def.severity || 'warn' })
110
122
  }
111
123
  }
112
124