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 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 is the exception — it publishes per-token
18
- prices in its own model list, so mohdel can read them. Setup counts the models
19
- that cost nothing and offers to add all of them in one keystroke; `mo curate
20
- openrouter` writes complete, priced entries for the paid ones. Free tier, no
21
- card, and a working catalog without a pricing page or a brief.
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 | No | No | No | No | Image generation only |
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.2",
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.2"
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.0",
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.2",
157
+ "release-it": "^21.0.3",
158
158
  "standard": "^17.1.2",
159
159
  "typescript": "^7.0.2",
160
160
  "vitest": "^4.1.11"
@@ -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} is the exception among providers: its model list carries per-token
306
- prices, so step 1 gives you the ids *and* the prices, and \`mo curate ${only}\`
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
+ }
@@ -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