mohdel 0.90.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 (98) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +377 -0
  3. package/config/benchmarks.json +39 -0
  4. package/js/client/call.js +75 -0
  5. package/js/client/call_image.js +82 -0
  6. package/js/client/gate-binary.js +72 -0
  7. package/js/client/index.js +16 -0
  8. package/js/client/ndjson.js +29 -0
  9. package/js/client/transport.js +48 -0
  10. package/js/core/envelope.js +141 -0
  11. package/js/core/errors.js +75 -0
  12. package/js/core/events.js +96 -0
  13. package/js/core/image.js +58 -0
  14. package/js/core/index.js +10 -0
  15. package/js/core/status.js +48 -0
  16. package/js/factory/bridge.js +372 -0
  17. package/js/session/_cooldown.js +114 -0
  18. package/js/session/_logger.js +138 -0
  19. package/js/session/_rate_limiter.js +77 -0
  20. package/js/session/_tracing.js +58 -0
  21. package/js/session/adapters/_cancelled.js +44 -0
  22. package/js/session/adapters/_catalog.js +58 -0
  23. package/js/session/adapters/_chat_completions.js +439 -0
  24. package/js/session/adapters/_errors.js +85 -0
  25. package/js/session/adapters/_images.js +60 -0
  26. package/js/session/adapters/_lazy_json_cache.js +76 -0
  27. package/js/session/adapters/_pricing.js +67 -0
  28. package/js/session/adapters/_providers.js +60 -0
  29. package/js/session/adapters/_tools.js +185 -0
  30. package/js/session/adapters/_videos.js +283 -0
  31. package/js/session/adapters/anthropic.js +397 -0
  32. package/js/session/adapters/cerebras.js +28 -0
  33. package/js/session/adapters/deepseek.js +32 -0
  34. package/js/session/adapters/echo.js +51 -0
  35. package/js/session/adapters/fake.js +262 -0
  36. package/js/session/adapters/fireworks.js +46 -0
  37. package/js/session/adapters/gemini.js +381 -0
  38. package/js/session/adapters/groq.js +23 -0
  39. package/js/session/adapters/image/fake.js +55 -0
  40. package/js/session/adapters/image/index.js +40 -0
  41. package/js/session/adapters/image/novita.js +135 -0
  42. package/js/session/adapters/image/openai.js +50 -0
  43. package/js/session/adapters/index.js +53 -0
  44. package/js/session/adapters/mistral.js +31 -0
  45. package/js/session/adapters/novita.js +29 -0
  46. package/js/session/adapters/openai.js +381 -0
  47. package/js/session/adapters/openrouter.js +66 -0
  48. package/js/session/adapters/xai.js +27 -0
  49. package/js/session/bin.js +54 -0
  50. package/js/session/driver.js +160 -0
  51. package/js/session/index.js +18 -0
  52. package/js/session/run.js +393 -0
  53. package/js/session/run_image.js +61 -0
  54. package/package.json +107 -0
  55. package/src/cli/ask.js +160 -0
  56. package/src/cli/backup.js +107 -0
  57. package/src/cli/bench.js +262 -0
  58. package/src/cli/check.js +123 -0
  59. package/src/cli/colored-logger.js +67 -0
  60. package/src/cli/colors.js +13 -0
  61. package/src/cli/default.js +39 -0
  62. package/src/cli/index.js +150 -0
  63. package/src/cli/json-output.js +60 -0
  64. package/src/cli/model.js +571 -0
  65. package/src/cli/onboard.js +232 -0
  66. package/src/cli/rank.js +176 -0
  67. package/src/cli/ratelimit.js +160 -0
  68. package/src/cli/tag.js +105 -0
  69. package/src/lib/assets/alibaba.svg +1 -0
  70. package/src/lib/assets/anthropic.svg +5 -0
  71. package/src/lib/assets/deepseek.svg +1 -0
  72. package/src/lib/assets/gemini.svg +1 -0
  73. package/src/lib/assets/google.svg +2 -0
  74. package/src/lib/assets/kwaipilot.svg +1 -0
  75. package/src/lib/assets/meta.svg +1 -0
  76. package/src/lib/assets/minimax.svg +9 -0
  77. package/src/lib/assets/moonshotai.svg +4 -0
  78. package/src/lib/assets/openai.svg +5 -0
  79. package/src/lib/assets/xai.svg +1 -0
  80. package/src/lib/assets/xiaomi.svg +2 -0
  81. package/src/lib/assets/zai.svg +219 -0
  82. package/src/lib/benchmark-score.js +215 -0
  83. package/src/lib/benchmark-truth.js +68 -0
  84. package/src/lib/cache.js +76 -0
  85. package/src/lib/common.js +208 -0
  86. package/src/lib/cooldown.js +63 -0
  87. package/src/lib/creators.js +71 -0
  88. package/src/lib/curated-cache.js +146 -0
  89. package/src/lib/errors.js +126 -0
  90. package/src/lib/index.js +726 -0
  91. package/src/lib/logger.js +29 -0
  92. package/src/lib/providers.js +87 -0
  93. package/src/lib/rank.js +390 -0
  94. package/src/lib/rate-limiter.js +50 -0
  95. package/src/lib/schema.js +150 -0
  96. package/src/lib/select.js +474 -0
  97. package/src/lib/tracing.js +62 -0
  98. package/src/lib/utils.js +85 -0
@@ -0,0 +1,123 @@
1
+ import { label, err, warn, ok } from './colors.js'
2
+ import providers from '../lib/providers.js'
3
+ import { validate, isValidTag } from '../lib/schema.js'
4
+ import { getCuratedModels, loadDefaultEnv } from '../lib/common.js'
5
+
6
+ // --- Local validation ---
7
+
8
+ const checkLocal = (curated) => {
9
+ const errors = []
10
+ const warnings = []
11
+ const knownProviders = new Set(Object.keys(providers))
12
+
13
+ for (const [key, spec] of Object.entries(curated)) {
14
+ const [keyProvider] = key.split('/')
15
+
16
+ if (spec.deprecated) {
17
+ if (!curated[spec.deprecated]) {
18
+ errors.push(`${key}: deprecated target '${spec.deprecated}' not in curated`)
19
+ }
20
+ continue
21
+ }
22
+
23
+ for (const issue of validate(spec, key)) {
24
+ if (issue.severity === 'error') errors.push(`${key}: ${issue.field} — ${issue.message}`)
25
+ else warnings.push(`${key}: ${issue.field} — ${issue.message}`)
26
+ }
27
+
28
+ if (!knownProviders.has(keyProvider)) {
29
+ errors.push(`${key}: provider '${keyProvider}' not in providers.js`)
30
+ }
31
+ if (spec.provider && spec.provider !== keyProvider) {
32
+ errors.push(`${key}: spec.provider '${spec.provider}' doesn't match key prefix '${keyProvider}'`)
33
+ }
34
+
35
+ const providerConfig = providers[keyProvider]
36
+ if (providerConfig && spec.sdk && spec.sdk !== providerConfig.sdk) {
37
+ errors.push(`${key}: spec.sdk '${spec.sdk}' doesn't match provider sdk '${providerConfig.sdk}'`)
38
+ }
39
+
40
+ if (!spec.label) warnings.push(`${key}: missing label`)
41
+
42
+ for (const priceField of ['inputPrice', 'outputPrice', 'thinkingPrice']) {
43
+ const val = spec[priceField]
44
+ if (val != null && typeof val === 'object' && val.default == null) {
45
+ errors.push(`${key}: ${priceField} is tiered but missing 'default' key`)
46
+ }
47
+ }
48
+
49
+ if (spec.thinkingEffortLevels && !spec.defaultThinkingEffort) {
50
+ warnings.push(`${key}: has thinkingEffortLevels but no defaultThinkingEffort`)
51
+ }
52
+
53
+ if (Array.isArray(spec.tags)) {
54
+ for (const t of spec.tags) {
55
+ if (!isValidTag(t)) warnings.push(`${key}: invalid tag "${t}" — must match /^[a-zA-Z][a-zA-Z0-9._-]{0,31}$/`)
56
+ }
57
+ }
58
+ }
59
+
60
+ return { errors, warnings }
61
+ }
62
+
63
+ // --- CLI ---
64
+
65
+ export async function runCheck (args) {
66
+ if (args.includes('-h') || args.includes('--help')) {
67
+ console.log(`mohdel model check — validate curated catalog
68
+
69
+ Usage:
70
+ model check [options]
71
+
72
+ Options:
73
+ --json Output as JSON
74
+
75
+ Checks:
76
+ Schema types, required fields, deprecated targets, provider/sdk
77
+ consistency, tiered pricing, thinking config.
78
+
79
+ Note: 0.90 drops the upstream-drift check that piggybacked on the
80
+ legacy per-provider SDK factory. If you need upstream drift
81
+ detection, file an issue — it'll be rebuilt on the /session stack.`)
82
+ process.exit(0)
83
+ }
84
+
85
+ loadDefaultEnv()
86
+
87
+ const json = args.includes('--json')
88
+
89
+ const curated = await getCuratedModels()
90
+ const active = Object.values(curated).filter(s => !s.deprecated).length
91
+ const deprecated = Object.values(curated).length - active
92
+
93
+ if (!json) {
94
+ console.log(`${label('Catalog:')} ${active} active, ${deprecated} deprecated\n`)
95
+ }
96
+
97
+ const { errors, warnings: localWarnings } = checkLocal(curated)
98
+
99
+ if (!json) {
100
+ if (errors.length) {
101
+ console.log(err(`${errors.length} error(s):`))
102
+ for (const e of errors) console.log(` ${err('✗')} ${e}`)
103
+ }
104
+ if (localWarnings.length) {
105
+ console.log(warn(`${localWarnings.length} warning(s):`))
106
+ for (const w of localWarnings) console.log(` ${warn('!')} ${w}`)
107
+ }
108
+ if (!errors.length && !localWarnings.length) {
109
+ console.log(ok('Local validation passed'))
110
+ }
111
+ }
112
+
113
+ if (json) {
114
+ console.log(JSON.stringify({
115
+ active,
116
+ deprecated,
117
+ errors,
118
+ warnings: localWarnings
119
+ }, null, 2))
120
+ }
121
+
122
+ if (errors.length) process.exit(1)
123
+ }
@@ -0,0 +1,67 @@
1
+ /**
2
+ * CLI-only colored stderr logger. Extracted from `src/lib/logger.js`
3
+ * so that library consumers (who use `silent` or pass their own pino
4
+ * logger) don't pay for loading chalk. Only CLI code imports this.
5
+ *
6
+ * Matches the logger interface contract
7
+ * `{ trace, debug, info, warn, error, fatal, child }`. Filters by minimum
8
+ * level so noise stays out of the user's view.
9
+ *
10
+ * @module cli/colored-logger
11
+ */
12
+
13
+ import chalk from 'chalk'
14
+
15
+ const noop = () => {}
16
+
17
+ const LEVELS = { trace: 10, debug: 20, info: 30, warn: 40, error: 50, fatal: 60 }
18
+
19
+ const COLORS = {
20
+ trace: chalk.dim,
21
+ debug: chalk.cyan,
22
+ info: chalk.white,
23
+ warn: chalk.yellow,
24
+ error: chalk.red,
25
+ fatal: chalk.bold.red
26
+ }
27
+
28
+ const LABELS = {
29
+ trace: 'TRC',
30
+ debug: 'DBG',
31
+ info: 'INF',
32
+ warn: 'WRN',
33
+ error: 'ERR',
34
+ fatal: 'FTL'
35
+ }
36
+
37
+ /**
38
+ * Build a colored stderr logger filtered by minimum level.
39
+ *
40
+ * @param {string} [minLevel='warn'] — minimum level to print: trace|debug|info|warn|error|fatal
41
+ * @returns {object} logger object
42
+ */
43
+ export function cliLogger (minLevel = 'warn') {
44
+ const threshold = LEVELS[minLevel] ?? LEVELS.warn
45
+ const lg = {}
46
+
47
+ for (const level of Object.keys(LEVELS)) {
48
+ if (LEVELS[level] < threshold) {
49
+ lg[level] = noop
50
+ } else {
51
+ const color = COLORS[level]
52
+ const label = color(LABELS[level])
53
+ lg[level] = (...args) => {
54
+ // Pino-style: first arg may be an object with structured fields
55
+ if (args.length && typeof args[0] === 'object' && args[0] !== null) {
56
+ const [, ...rest] = args
57
+ console.error(label, ...rest)
58
+ } else {
59
+ console.error(label, ...args)
60
+ }
61
+ }
62
+ }
63
+ }
64
+
65
+ lg.child = () => lg
66
+ return lg
67
+ }
@@ -0,0 +1,13 @@
1
+ // Semantic color roles for CLI output.
2
+ import chalk from 'chalk'
3
+
4
+ export const id = chalk.cyan // model IDs, provider names
5
+ export const label = chalk.bold // display names, titles
6
+ export const tag = chalk.yellow // tags
7
+ export const price = chalk.green // money values
8
+ export const meta = chalk.dim // counts, field labels, headers, separators
9
+ export const missing = chalk.dim('—') // null/absent values
10
+ export const err = chalk.red // errors
11
+ export const warn = chalk.yellow // warnings
12
+ export const ok = chalk.green // success, ● dots
13
+ export const inactive = chalk.dim // unconfigured, ○ dots
@@ -0,0 +1,39 @@
1
+ import { intro, outro, select, isCancel, cancel } from '@clack/prompts'
2
+ import { getCuratedModels, CONFIG_PATH, saveConfig } from '../lib/common.js'
3
+ import providers from '../lib/providers.js'
4
+
5
+ export async function runDefault () {
6
+ intro('Mohdel — Set Default Model')
7
+
8
+ const curated = await getCuratedModels()
9
+ const modelOptions = Object.entries(curated).map(([modelId, info]) => ({
10
+ value: modelId,
11
+ label: `${info.label} (${modelId})`
12
+ }))
13
+ modelOptions.sort((a, b) => a.label.localeCompare(b.label))
14
+
15
+ const selectedModelId = await select({
16
+ message: 'Select your default model:',
17
+ options: modelOptions
18
+ })
19
+
20
+ if (isCancel(selectedModelId)) {
21
+ cancel('Cancelled')
22
+ process.exit(0)
23
+ }
24
+
25
+ const [providerName] = selectedModelId.split('/')
26
+
27
+ try {
28
+ const config = { defaultModel: selectedModelId }
29
+ const apiKeyEnv = providers[providerName]?.apiKeyEnv
30
+ if (apiKeyEnv) {
31
+ config.apiKeyInfo = `Set ${apiKeyEnv} environment variable for this provider`
32
+ }
33
+ await saveConfig(config)
34
+ outro(`Default set to ${selectedModelId} — saved to ${CONFIG_PATH}`)
35
+ } catch (err) {
36
+ cancel(`Error: ${err.message}`)
37
+ process.exit(1)
38
+ }
39
+ }
@@ -0,0 +1,150 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * mohdel CLI — model management (noun-verb pattern).
5
+ *
6
+ * Nouns: model, provider, creator, tag, ratelimit
7
+ * Each noun supports: list, show, and noun-specific verbs.
8
+ *
9
+ * Aliases: ls → model list, rl → ratelimit
10
+ */
11
+
12
+ const [command, ...args] = process.argv.slice(2)
13
+
14
+ if (!command) {
15
+ const { runOnboard } = await import('./onboard.js')
16
+ await runOnboard()
17
+ process.exit(0)
18
+ }
19
+
20
+ if (command === '-h' || command === '--help') {
21
+ console.log(`mohdel — model catalog management
22
+
23
+ Commands:
24
+ model list [--sort price|context|name] List all curated models
25
+ model search <term> Filter models by name/label
26
+ model stats Catalog summary
27
+ model show <model> Show model details
28
+ model get <model> <key> Get a field value
29
+ model set <model> <key> <value> Set a field
30
+ model rm <model> <key> Remove a field
31
+ model add <provider>/<model-id> Add a model manually
32
+ model check [--local] Validate catalog
33
+ model rank [--use-case <name>] Rank models by benchmarks
34
+ model bench <model> Benchmark with live inference
35
+ model curate [provider] Add upstream models to catalog
36
+
37
+ provider list List all providers
38
+ provider list <provider> List models from a provider
39
+ provider setup <provider> Configure API key (interactive)
40
+ provider rm <provider> Remove API key
41
+
42
+ creator list List all creators
43
+ creator list <creator> List models by a creator
44
+
45
+ tag list List all unique tags
46
+ tag list <model> Show tags on a model
47
+ tag show <tag> List models with a tag
48
+ tag add <model> <tag> Add a tag
49
+ tag rm <model> <tag> Remove a tag
50
+
51
+ ratelimit show <model|provider> Show effective limits
52
+ ratelimit set <model> [rpm] [tpm] Set model-level limits
53
+ ratelimit rm <model> Remove model-level limits
54
+ ratelimit provider set <p> [rpm] [tpm] Set provider-level limits
55
+ ratelimit provider rm <p> Remove provider-level limits
56
+
57
+ ask <provider/model> [prompt] One-shot inference (pipeable)
58
+
59
+ default Set default model (interactive)
60
+
61
+ Aliases:
62
+ models model list
63
+ providers provider list
64
+ creators creator list
65
+ tags tag list
66
+ ls model list
67
+ show <model> model show <model>
68
+ search <term> model search <term>
69
+ stats model stats
70
+ check model check
71
+ setup <provider> provider setup <provider>
72
+ rank model rank
73
+ bench <model> model bench <model>
74
+ curate [provider] model curate [provider]
75
+ rl ratelimit
76
+
77
+ Global flags:
78
+ --json [fields] Output as JSON (omit fields to list available)
79
+
80
+ Environment:
81
+ API keys are loaded from ~/.config/mohdel/environment (KEY=value format).
82
+ Run "mo" with no arguments to configure interactively.
83
+
84
+ ANTHROPIC_API_SK Anthropic API key
85
+ OPENAI_API_SK OpenAI API key
86
+ GEMINI_API_SK Google Gemini API key
87
+ GROQ_API_SK Groq API key
88
+ CEREBRAS_API_SK Cerebras API key
89
+ XAI_API_SK xAI API key
90
+ MISTRAL_API_SK Mistral API key
91
+ DEEPSEEK_API_SK DeepSeek API key
92
+ FIREWORKS_API_SK Fireworks API key
93
+ OPENROUTER_API_SK OpenRouter API key
94
+ NOVITA_API_SK Novita API key
95
+
96
+ Configuration:
97
+ ~/.config/mohdel/environment API keys (loaded automatically)
98
+ ~/.config/mohdel/curated.json Model catalog
99
+ ~/.config/mohdel/providers.json Provider-level rate limits
100
+ ~/.config/mohdel/default.json Default model selection`)
101
+ process.exit(0)
102
+ }
103
+
104
+ // Alias resolution: short commands → noun + verb
105
+ const ALIASES = {
106
+ models: { noun: 'model', inject: ['list'] },
107
+ providers: { noun: 'provider', inject: ['list'] },
108
+ creators: { noun: 'creator', inject: ['list'] },
109
+ tags: { noun: 'tag', inject: ['list'] },
110
+ ls: { noun: 'model', inject: ['list'] },
111
+ show: { noun: 'model', inject: ['show'] },
112
+ search: { noun: 'model', inject: ['search'] },
113
+ stats: { noun: 'model', inject: ['stats'] },
114
+ check: { noun: 'model', inject: ['check'] },
115
+ setup: { noun: 'provider', inject: ['setup'] },
116
+ rank: { noun: 'model', inject: ['rank'] },
117
+ bench: { noun: 'model', inject: ['bench'] },
118
+ curate: { noun: 'model', inject: ['curate'] },
119
+ rl: { noun: 'ratelimit', inject: [] }
120
+ }
121
+
122
+ const alias = ALIASES[command]
123
+ const resolved = alias ? alias.noun : command
124
+ const resolvedArgs = alias ? [...alias.inject, ...args] : args
125
+
126
+ if (resolved === 'default') {
127
+ const { runDefault } = await import('./default.js')
128
+ await runDefault()
129
+ } else if (resolved === 'ask') {
130
+ const { runAsk } = await import('./ask.js')
131
+ await runAsk(resolvedArgs)
132
+ } else if (resolved === 'model') {
133
+ const { runModel } = await import('./model.js')
134
+ await runModel(resolvedArgs)
135
+ } else if (resolved === 'provider') {
136
+ const { runProvider } = await import('./model.js')
137
+ await runProvider(resolvedArgs)
138
+ } else if (resolved === 'creator') {
139
+ const { runCreator } = await import('./model.js')
140
+ await runCreator(resolvedArgs)
141
+ } else if (resolved === 'tag') {
142
+ const { runTag } = await import('./tag.js')
143
+ await runTag(resolvedArgs)
144
+ } else if (resolved === 'ratelimit' || resolved === 'rl') {
145
+ const { runRateLimit } = await import('./ratelimit.js')
146
+ await runRateLimit(resolvedArgs)
147
+ } else {
148
+ console.error(`Unknown command: ${command}. Run "mo --help" for usage.`)
149
+ process.exit(1)
150
+ }
@@ -0,0 +1,60 @@
1
+ // --json flag support (gh-style):
2
+ // --json → list available fields
3
+ // --json f1,f2,f3 → output only those fields
4
+
5
+ /**
6
+ * Extract --json flag and its value from args.
7
+ * Returns { json: false } or { json: true, fields: null } (list fields)
8
+ * or { json: true, fields: ['f1','f2'] } (filter).
9
+ * Mutates args in place to remove consumed --json [value].
10
+ */
11
+ export function parseJsonFlag (args) {
12
+ const idx = args.indexOf('--json')
13
+ if (idx === -1) return { json: false }
14
+
15
+ args.splice(idx, 1)
16
+
17
+ // Next arg is the field list (if it exists and doesn't look like a flag)
18
+ const next = args[idx]
19
+ if (next && !next.startsWith('-')) {
20
+ args.splice(idx, 1)
21
+ return { json: true, fields: next.split(',').map(f => f.trim()).filter(Boolean) }
22
+ }
23
+
24
+ return { json: true, fields: null }
25
+ }
26
+
27
+ /**
28
+ * Print available fields for a --json call with no field list.
29
+ */
30
+ export function printAvailableFields (fields) {
31
+ console.log('Available JSON fields:')
32
+ for (const f of fields) console.log(` ${f}`)
33
+ }
34
+
35
+ /**
36
+ * Pick selected fields from an object.
37
+ */
38
+ function pick (obj, fields) {
39
+ const out = {}
40
+ for (const f of fields) {
41
+ if (f in obj) out[f] = obj[f]
42
+ }
43
+ return out
44
+ }
45
+
46
+ /**
47
+ * Output a list of objects as JSON, optionally filtered to specific fields.
48
+ */
49
+ export function jsonOutput (items, fields) {
50
+ const out = fields ? items.map(item => pick(item, fields)) : items
51
+ console.log(JSON.stringify(out, null, 2))
52
+ }
53
+
54
+ /**
55
+ * Output a single object as JSON, optionally filtered to specific fields.
56
+ */
57
+ export function jsonOutputOne (item, fields) {
58
+ const out = fields ? pick(item, fields) : item
59
+ console.log(JSON.stringify(out, null, 2))
60
+ }