mohdel 1.0.2 → 1.0.3
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 +6 -6
- package/package.json +4 -4
- package/src/cli/instructions.js +2 -2
- package/src/cli/model.js +1 -1
- package/src/lib/catalog/novita.js +62 -0
- package/src/lib/providers.js +2 -0
- package/src/lib/select.js +1 -1
package/README.md
CHANGED
|
@@ -14,11 +14,11 @@ They live on a docs page, so mohdel writes a brief and the coding agent you
|
|
|
14
14
|
already run reads the page and drafts the entries. `mo` offers this at the end
|
|
15
15
|
of setup. Nothing runs on your key but that agent.
|
|
16
16
|
|
|
17
|
-
**No coding agent?** OpenRouter
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
17
|
+
**No coding agent?** OpenRouter and Novita publish per-token prices in their
|
|
18
|
+
own model lists, so mohdel reads them directly. `mo curate openrouter` or `mo
|
|
19
|
+
curate novita` writes complete, priced entries on its own, and setup counts the
|
|
20
|
+
models that cost nothing and offers to add all of them in one keystroke. A
|
|
21
|
+
working catalog without a pricing page or a brief.
|
|
22
22
|
|
|
23
23
|
Providers: Anthropic, OpenAI, Gemini, Mistral, Groq, xAI, Cerebras, Fireworks, DeepSeek, Qwen Cloud, Xiaomi, OpenRouter, Novita. Node 22+, ES modules.
|
|
24
24
|
|
|
@@ -462,7 +462,7 @@ What each provider supports through mohdel's unified interface:
|
|
|
462
462
|
| Xiaomi | No | Yes | Yes | No | Auto | MiMo; shared chat-completions path, `reasoning_content` captured |
|
|
463
463
|
| OpenRouter | Yes | Yes | Yes | No | Varies | Meta-provider; `providerOptions.openrouter` for routing prefs |
|
|
464
464
|
| Local | Yes | Yes | Yes | No | No | Any OpenAI-compatible server; endpoint is the catalog entry's `baseURL` |
|
|
465
|
-
| Novita | No |
|
|
465
|
+
| Novita | No | Yes | Yes | No | No | Prices in the model list; text via the shared chat-completions path, separate image adapter |
|
|
466
466
|
|
|
467
467
|
Adapter capability ≠ model capability — whether a given model accepts images, tools, or thinking effort depends on the model spec in `curated.json`. The adapter passes through what the envelope supplies; the provider rejects unsupported combos.
|
|
468
468
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mohdel",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Christophe Le Bars",
|
|
@@ -135,12 +135,12 @@
|
|
|
135
135
|
"@opentelemetry/exporter-trace-otlp-grpc": "^0.222.0",
|
|
136
136
|
"@opentelemetry/sdk-node": "^0.222.0",
|
|
137
137
|
"chalk": "^6.0.0",
|
|
138
|
-
"mohdel-thin-gate-linux-x64-gnu": "1.0.
|
|
138
|
+
"mohdel-thin-gate-linux-x64-gnu": "1.0.3"
|
|
139
139
|
},
|
|
140
140
|
"dependencies": {
|
|
141
141
|
"@anthropic-ai/sdk": "^0.125.0",
|
|
142
142
|
"@cerebras/cerebras_cloud_sdk": "^1.91.0",
|
|
143
|
-
"@clack/prompts": "^1.8.
|
|
143
|
+
"@clack/prompts": "^1.8.1",
|
|
144
144
|
"@google/genai": "^2.22.0",
|
|
145
145
|
"@opentelemetry/api": "^1.9.1",
|
|
146
146
|
"env-paths": "^4.0.0",
|
|
@@ -154,7 +154,7 @@
|
|
|
154
154
|
"devDependencies": {
|
|
155
155
|
"gpt-tokenizer": "^4.0.0",
|
|
156
156
|
"lint-staged": "^17.5.1",
|
|
157
|
-
"release-it": "^21.0.
|
|
157
|
+
"release-it": "^21.0.3",
|
|
158
158
|
"standard": "^17.1.2",
|
|
159
159
|
"typescript": "^7.0.2",
|
|
160
160
|
"vitest": "^4.1.11"
|
package/src/cli/instructions.js
CHANGED
|
@@ -302,8 +302,8 @@ the command, do not run it for them.
|
|
|
302
302
|
${names.map(referenceList).join('\n')}
|
|
303
303
|
|
|
304
304
|
${only && providerDefs[only]?.pricesFromApi
|
|
305
|
-
? `${only}
|
|
306
|
-
|
|
305
|
+
? `${only} publishes prices in its own model list, which most providers do not,
|
|
306
|
+
so step 1 gives you the ids *and* the prices, and \`mo curate ${only}\`
|
|
307
307
|
writes complete entries on its own. Check what it produced rather than
|
|
308
308
|
transcribing anything, and use the pages below only for what the list omits.`
|
|
309
309
|
: `Provider APIs return model *ids*, not prices. Step 1 above gets you the ids;
|
package/src/cli/model.js
CHANGED
|
@@ -330,7 +330,7 @@ config/curated.example.json for ready-to-copy entries.`)
|
|
|
330
330
|
if (apiKey && providerConfig.catalog !== false) {
|
|
331
331
|
try {
|
|
332
332
|
const sdkConfig = providerConfig.createConfiguration(apiKey)
|
|
333
|
-
const { default: API } = await import(`../lib/catalog/${providerConfig.sdk}.js`)
|
|
333
|
+
const { default: API } = await import(`../lib/catalog/${providerConfig.catalogClient || providerConfig.sdk}.js`)
|
|
334
334
|
const noop = () => {}
|
|
335
335
|
const api = API(sdkConfig, {}, { trace: noop, debug: noop, info: noop, warn: noop, error: noop, fatal: noop })
|
|
336
336
|
if (api.getModelInfo) {
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
const toPrice = (entry) => {
|
|
2
|
+
const raw = entry?.price_per_m_decimal
|
|
3
|
+
if (typeof raw !== 'string') return undefined
|
|
4
|
+
const n = parseFloat(raw)
|
|
5
|
+
return Number.isFinite(n) ? n : undefined
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const MODALITIES = new Set(['text', 'image', 'video', 'audio'])
|
|
9
|
+
|
|
10
|
+
const BASE_URL = 'https://api.novita.ai/openai/v1'
|
|
11
|
+
|
|
12
|
+
const fetchModels = async ({ apiKey }) => {
|
|
13
|
+
const res = await fetch(`${BASE_URL}/models`, {
|
|
14
|
+
headers: apiKey ? { Authorization: `Bearer ${apiKey}` } : {}
|
|
15
|
+
})
|
|
16
|
+
if (!res.ok) throw new Error(`${res.status} ${res.statusText} fetching novita models`)
|
|
17
|
+
const body = await res.json()
|
|
18
|
+
return Array.isArray(body?.data) ? body.data : []
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export default (sdkConfig) => {
|
|
22
|
+
let cache = null
|
|
23
|
+
const load = async () => {
|
|
24
|
+
if (!cache) cache = await fetchModels(sdkConfig)
|
|
25
|
+
// The same endpoint serves image and video models; those belong to the
|
|
26
|
+
// image adapter and have no place in a chat catalog.
|
|
27
|
+
return cache.filter(m => m.model_type === 'chat')
|
|
28
|
+
}
|
|
29
|
+
return {
|
|
30
|
+
listModels: async () => (await load()).map(m => ({
|
|
31
|
+
id: m.id,
|
|
32
|
+
label: m.display_name || m.id,
|
|
33
|
+
inputPrice: toPrice(m.pricing?.prompt),
|
|
34
|
+
outputPrice: toPrice(m.pricing?.completion)
|
|
35
|
+
})),
|
|
36
|
+
getModelInfo: async (id) => {
|
|
37
|
+
const m = (await load()).find(x => x.id === id)
|
|
38
|
+
if (!m) return null
|
|
39
|
+
const info = { model: m.id }
|
|
40
|
+
if (m.display_name) info.label = m.display_name
|
|
41
|
+
if (m.description) info.description = m.description
|
|
42
|
+
if (typeof m.context_size === 'number') info.contextTokenLimit = m.context_size
|
|
43
|
+
if (typeof m.max_output_tokens === 'number') info.outputTokenLimit = m.max_output_tokens
|
|
44
|
+
|
|
45
|
+
// Prices are USD per million as a decimal string, which is what the
|
|
46
|
+
// catalog stores. The integer `*_price_per_m` fields are the same number
|
|
47
|
+
// scaled by 10,000.
|
|
48
|
+
const input = toPrice(m.pricing?.prompt)
|
|
49
|
+
const output = toPrice(m.pricing?.completion)
|
|
50
|
+
const cacheRead = toPrice(m.pricing?.input_cache_read)
|
|
51
|
+
if (input !== undefined) info.inputPrice = input
|
|
52
|
+
if (output !== undefined) info.outputPrice = output
|
|
53
|
+
if (cacheRead !== undefined) info.cacheReadPrice = cacheRead
|
|
54
|
+
|
|
55
|
+
const modalities = (m.input_modalities || []).filter(x => MODALITIES.has(x))
|
|
56
|
+
if (modalities.length) info.inputFormat = modalities
|
|
57
|
+
if (Array.isArray(m.features)) info.supportsTools = m.features.includes('function-calling')
|
|
58
|
+
if (typeof m.created === 'number') info.created = m.created
|
|
59
|
+
return info
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
package/src/lib/providers.js
CHANGED
|
@@ -104,6 +104,8 @@ const providers = {
|
|
|
104
104
|
imageHandler: 'novita',
|
|
105
105
|
apiKeyEnv: 'NOVITA_API_SK',
|
|
106
106
|
baseURL: 'https://api.novita.ai/openai',
|
|
107
|
+
pricesFromApi: true,
|
|
108
|
+
catalogClient: 'novita',
|
|
107
109
|
createConfiguration: apiKey => ({ apiKey }),
|
|
108
110
|
references: {
|
|
109
111
|
pricing: 'https://novita.ai/pricing',
|
package/src/lib/select.js
CHANGED
|
@@ -24,7 +24,7 @@ export const providerApi = async (name) => {
|
|
|
24
24
|
if (!config || config.catalog === false || !config.apiKeyEnv) return null
|
|
25
25
|
const apiKey = getAPIKey(config.apiKeyEnv)
|
|
26
26
|
if (!apiKey) return null
|
|
27
|
-
const { default: API } = await import(`./catalog/${config.sdk}.js`)
|
|
27
|
+
const { default: API } = await import(`./catalog/${config.catalogClient || config.sdk}.js`)
|
|
28
28
|
return API({ ...config.createConfiguration(apiKey), baseURL: config.baseURL }, {}, silent)
|
|
29
29
|
}
|
|
30
30
|
|