mohdel 0.124.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +155 -30
- package/config/curated.schema.json +32 -10
- package/js/client/call.js +76 -19
- package/js/client/gate-binary.js +5 -0
- package/js/client/index.js +1 -0
- package/js/core/envelope.js +5 -1
- package/js/factory/bridge.js +2 -2
- package/js/session/adapters/_cancelled.js +0 -6
- package/js/session/adapters/_chat_completions.js +25 -0
- package/js/session/adapters/_output_cap.js +30 -0
- package/js/session/adapters/_registry.js +44 -0
- package/js/session/adapters/anthropic.js +8 -5
- package/js/session/adapters/gemini.js +4 -0
- package/js/session/adapters/openai.js +2 -1
- package/js/session/run.js +14 -4
- package/js/session/run_image.js +12 -3
- package/package.json +49 -19
- package/src/cli/aliases.js +20 -0
- package/src/cli/ask.js +58 -12
- package/src/cli/backup.js +2 -1
- package/src/cli/check.js +15 -86
- package/src/cli/complete.js +130 -0
- package/src/cli/default.js +34 -13
- package/src/cli/doctor.js +33 -14
- package/src/cli/entry.js +173 -0
- package/src/cli/index.js +77 -66
- package/src/cli/instructions.js +349 -0
- package/src/cli/local.js +14 -0
- package/src/cli/model.js +184 -37
- package/src/cli/onboard.js +186 -121
- package/src/cli/rank.js +2 -1
- package/src/cli/ratelimit.js +3 -3
- package/src/cli/tag.js +2 -0
- package/src/lib/assistants.js +93 -0
- package/src/lib/catalog/openrouter.js +6 -1
- package/src/lib/catalog-review.js +195 -0
- package/src/lib/common.js +14 -1
- package/src/lib/creators.js +35 -0
- package/src/lib/index.js +17 -3
- package/src/lib/local-conventions.js +120 -0
- package/src/lib/provider-info.js +98 -0
- package/src/lib/providers.js +69 -14
- package/src/lib/schema.js +15 -3
- package/src/lib/select.js +125 -67
- package/js/session/adapters/image/index.js +0 -40
package/src/lib/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
|
package/src/lib/creators.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
package/src/lib/providers.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|