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,232 @@
1
+ import { intro, outro, select, text, isCancel, cancel, note } from '@clack/prompts'
2
+ import { id, label, meta, ok } from './colors.js'
3
+ import { chmodSync, existsSync } from 'fs'
4
+ import { readFile, writeFile, mkdir } from 'fs/promises'
5
+ import { dirname } from 'path'
6
+ import { loadDefaultEnv, getAPIKey, ENV_PATH } from '../lib/common.js'
7
+ import providers from '../lib/providers.js'
8
+
9
+ const PROVIDER_INFO = {
10
+ gemini: {
11
+ label: 'Google Gemini',
12
+ description: 'Gemini 2.5/3 — long context, vision, video. Free tier, no card required.',
13
+ url: 'https://aistudio.google.com/apikey',
14
+ hint: 'Create an API key at aistudio.google.com → Get API Key',
15
+ free: true
16
+ },
17
+ groq: {
18
+ label: 'Groq',
19
+ description: 'Llama 4 — fastest inference available. Free tier, no card required.',
20
+ url: 'https://console.groq.com/keys',
21
+ hint: 'Create an API key at console.groq.com → API Keys',
22
+ free: true
23
+ },
24
+ cerebras: {
25
+ label: 'Cerebras',
26
+ description: 'Llama, Qwen — fast inference on custom hardware. Free tier available.',
27
+ url: 'https://cloud.cerebras.ai/platform',
28
+ hint: 'Create an API key at cloud.cerebras.ai → Platform → API Keys',
29
+ free: true
30
+ },
31
+ anthropic: {
32
+ label: 'Anthropic',
33
+ description: 'Claude Opus, Sonnet, Haiku — reasoning, coding, vision, tool use.',
34
+ url: 'https://console.anthropic.com/settings/keys',
35
+ hint: 'Create an API key at console.anthropic.com → Settings → API Keys',
36
+ free: false
37
+ },
38
+ openai: {
39
+ label: 'OpenAI',
40
+ description: 'GPT-5, o-series — reasoning, vision, image generation.',
41
+ url: 'https://platform.openai.com/api-keys',
42
+ hint: 'Create an API key at platform.openai.com → API Keys',
43
+ free: false
44
+ },
45
+ xai: {
46
+ label: 'xAI',
47
+ description: 'Grok — reasoning and tool use.',
48
+ url: 'https://console.x.ai',
49
+ hint: 'Create an API key at console.x.ai',
50
+ free: false
51
+ },
52
+ mistral: {
53
+ label: 'Mistral',
54
+ description: 'Mistral Large, Codestral, Pixtral — coding, reasoning, vision. Free tier available.',
55
+ url: 'https://console.mistral.ai/api-keys',
56
+ hint: 'Create an API key at console.mistral.ai → API Keys',
57
+ free: true
58
+ },
59
+ deepseek: {
60
+ label: 'DeepSeek',
61
+ description: 'DeepSeek R1/V3 — reasoning, coding. Low cost.',
62
+ url: 'https://platform.deepseek.com/api_keys',
63
+ hint: 'Create an API key at platform.deepseek.com → API Keys',
64
+ free: false
65
+ },
66
+ fireworks: {
67
+ label: 'Fireworks',
68
+ description: 'Llama, Qwen, DeepSeek — serverless inference with reasoning.',
69
+ url: 'https://fireworks.ai/account/api-keys',
70
+ hint: 'Create an API key at fireworks.ai → Account → API Keys',
71
+ free: false
72
+ },
73
+ openrouter: {
74
+ label: 'OpenRouter',
75
+ description: 'Multi-provider router — access 200+ models with one key.',
76
+ url: 'https://openrouter.ai/settings/keys',
77
+ hint: 'Create an API key at openrouter.ai → Settings → Keys',
78
+ free: false
79
+ },
80
+ novita: {
81
+ label: 'Novita',
82
+ description: 'Image generation — Flux, SDXL.',
83
+ url: 'https://novita.ai/dashboard/key',
84
+ hint: 'Create an API key at novita.ai → Dashboard → API Key',
85
+ free: false
86
+ }
87
+ }
88
+
89
+ export { PROVIDER_INFO, appendToEnvFile }
90
+
91
+ function getConfiguredProviders () {
92
+ const configured = []
93
+ const unconfigured = []
94
+ for (const [name, config] of Object.entries(providers)) {
95
+ if (!config.apiKeyEnv) continue
96
+ const hasKey = !!getAPIKey(config.apiKeyEnv)
97
+ if (hasKey) configured.push(name)
98
+ else unconfigured.push(name)
99
+ }
100
+ return { configured, unconfigured }
101
+ }
102
+
103
+ async function appendToEnvFile (key, value) {
104
+ const dir = dirname(ENV_PATH)
105
+ if (!existsSync(dir)) await mkdir(dir, { recursive: true })
106
+
107
+ let content = ''
108
+ if (existsSync(ENV_PATH)) {
109
+ content = await readFile(ENV_PATH, 'utf8')
110
+ // Replace existing line if present
111
+ const re = new RegExp(`^${key}=.*$`, 'm')
112
+ if (re.test(content)) {
113
+ content = content.replace(re, `${key}=${value}`)
114
+ await writeFile(ENV_PATH, content, { mode: 0o600 })
115
+ chmodSync(ENV_PATH, 0o600)
116
+ return
117
+ }
118
+ if (!content.endsWith('\n')) content += '\n'
119
+ }
120
+ content += `${key}=${value}\n`
121
+ await writeFile(ENV_PATH, content, { mode: 0o600 })
122
+ chmodSync(ENV_PATH, 0o600)
123
+ }
124
+
125
+ export async function runOnboard () {
126
+ loadDefaultEnv()
127
+ const { configured, unconfigured } = getConfiguredProviders()
128
+
129
+ // Has providers configured — show status
130
+ if (configured.length > 0) {
131
+ console.log(label('mohdel') + meta(` — ${configured.length} provider${configured.length > 1 ? 's' : ''} configured\n`))
132
+ for (const name of configured) {
133
+ console.log(` ${ok('●')} ${id(name)}`)
134
+ }
135
+ if (unconfigured.length) {
136
+ console.log('')
137
+ for (const name of unconfigured) {
138
+ console.log(` ${meta('○')} ${meta(name)}`)
139
+ }
140
+ }
141
+ console.log(`\n${meta('Commands:')}
142
+ mo model list Browse models
143
+ mo model show <model> Model details
144
+ mo default Set default model
145
+ mo --help All commands`)
146
+ return
147
+ }
148
+
149
+ // No providers — onboarding wizard
150
+ intro('mohdel — first-time setup')
151
+
152
+ note(
153
+ 'New to LLM APIs? Start with Gemini, Groq, or Cerebras —\nall offer free tiers with no credit card required.',
154
+ 'Tip'
155
+ )
156
+
157
+ // Sort: free-tier providers first, then paid
158
+ const providerOptions = unconfigured
159
+ .filter(name => PROVIDER_INFO[name])
160
+ .sort((a, b) => {
161
+ const af = PROVIDER_INFO[a].free ? 0 : 1
162
+ const bf = PROVIDER_INFO[b].free ? 0 : 1
163
+ return af - bf
164
+ })
165
+ .map(name => {
166
+ const info = PROVIDER_INFO[name]
167
+ return {
168
+ value: name,
169
+ label: info.label + (info.free ? ok(' (free)') : ''),
170
+ hint: info.description
171
+ }
172
+ })
173
+
174
+ const selected = await select({
175
+ message: 'Select a provider to configure:',
176
+ options: providerOptions
177
+ })
178
+
179
+ if (isCancel(selected)) {
180
+ cancel('Setup cancelled')
181
+ process.exit(0)
182
+ }
183
+
184
+ const info = PROVIDER_INFO[selected]
185
+ const envVar = providers[selected].apiKeyEnv
186
+
187
+ note(
188
+ `${info.hint}\n\n${id(info.url)}`,
189
+ `${info.label} — API Key`
190
+ )
191
+
192
+ const apiKey = await text({
193
+ message: `Paste your ${selected} API key:`,
194
+ placeholder: envVar,
195
+ validate: (value) => {
196
+ if (!value || !value.trim()) return 'API key cannot be empty'
197
+ }
198
+ })
199
+
200
+ if (isCancel(apiKey)) {
201
+ cancel('Setup cancelled')
202
+ process.exit(0)
203
+ }
204
+
205
+ await appendToEnvFile(envVar, apiKey.trim())
206
+
207
+ note(`${ok('✓')} Saved ${envVar} to ${meta(ENV_PATH)}`, 'Done')
208
+
209
+ // Reload env so the new key is visible, then offer to curate models
210
+ loadDefaultEnv()
211
+ const { confirm } = await import('@clack/prompts')
212
+ const shouldCurate = await confirm({
213
+ message: `Fetch and curate models from ${info.label}?`,
214
+ initialValue: true
215
+ })
216
+
217
+ if (isCancel(shouldCurate) || !shouldCurate) {
218
+ outro(`Run ${id('mo model curate ' + selected)} later to browse available models.`)
219
+ return
220
+ }
221
+
222
+ const { initializeAPIs, processModels } = await import('../lib/select.js')
223
+ const { api } = await initializeAPIs()
224
+
225
+ if (!api[selected]) {
226
+ outro(`Could not initialize ${info.label}. Run ${id('mo model curate ' + selected)} to retry.`)
227
+ return
228
+ }
229
+
230
+ await processModels(selected, api[selected])
231
+ outro(`Run ${id('mo model list')} to see your curated models.`)
232
+ }
@@ -0,0 +1,176 @@
1
+ import { label, price, meta as dim } from './colors.js'
2
+ import { getCuratedModels } from '../lib/common.js'
3
+ import { rank } from '../lib/rank.js'
4
+ import { parseJsonFlag } from './json-output.js'
5
+
6
+ const USE_CASES = ['balanced', 'analysis', 'tool-loop', 'cowork']
7
+
8
+ export async function runRank (args) {
9
+ const jsonFlag = parseJsonFlag(args)
10
+
11
+ if (args.includes('-h') || args.includes('--help')) {
12
+ console.log(`mohdel model rank — rank models by benchmark performance
13
+
14
+ Usage:
15
+ model rank [options]
16
+
17
+ Options:
18
+ --use-case <name> Weight preset: ${USE_CASES.join(', ')} (default: balanced)
19
+ --top N Number of results (default: 20)
20
+ --breakdown, -b Show per-group sub-scores
21
+ --all Include all upstream models (default: curated only)
22
+ --since YYYY-MM Filter by release date
23
+ --min-context N Minimum context window
24
+ --fresh Bypass cache, fetch live data
25
+ --json Output as JSON
26
+ --md Output as markdown
27
+
28
+ Sources:
29
+ ZeroEval Backbone — GPQA, MMMU-Pro, MRCR, Toolathlon
30
+ Epoch AI GPQA Diamond, SWE-bench Verified
31
+ Tau2-bench Tool reliability (retail)
32
+
33
+ Cache:
34
+ Benchmark data cached for 24h in ~/.cache/mohdel/rank-*.json
35
+ Use --fresh to bypass`)
36
+ process.exit(0)
37
+ }
38
+
39
+ // Parse flags
40
+ const flag = (name) => {
41
+ const idx = args.indexOf(name)
42
+ if (idx === -1) return undefined
43
+ args.splice(idx, 1)
44
+ return true
45
+ }
46
+ const flagVal = (name) => {
47
+ const idx = args.indexOf(name)
48
+ if (idx === -1) return undefined
49
+ const val = args[idx + 1]
50
+ args.splice(idx, 2)
51
+ return val
52
+ }
53
+
54
+ const useCase = flagVal('--use-case') || 'balanced'
55
+ const top = parseInt(flagVal('--top') || '20', 10)
56
+ const breakdown = flag('--breakdown') || flag('-b')
57
+ const all = flag('--all')
58
+ const fresh = flag('--fresh')
59
+ const since = flagVal('--since')
60
+ const minContext = flagVal('--min-context') ? parseInt(flagVal('--min-context'), 10) : undefined
61
+ const md = flag('--md')
62
+
63
+ if (!USE_CASES.includes(useCase)) {
64
+ console.error(`Unknown use-case: ${useCase}. Available: ${USE_CASES.join(', ')}`)
65
+ process.exit(1)
66
+ }
67
+
68
+ const curated = all ? null : await getCuratedModels()
69
+
70
+ const { rankings, meta } = await rank({
71
+ curated,
72
+ useCase,
73
+ top,
74
+ all,
75
+ since,
76
+ minContext,
77
+ fresh,
78
+ onStatus: (msg) => process.stderr.write(` ${msg}\n`)
79
+ })
80
+
81
+ if (!rankings.length) {
82
+ console.error('No models matched the criteria.')
83
+ process.exit(1)
84
+ }
85
+
86
+ // JSON output
87
+ if (jsonFlag.json) {
88
+ const out = { ...meta, rankings: rankings.map(r => ({ ...r, overall: n(r.overall), analysis: n(r.analysis), tool_loop: n(r.tool_loop), cowork: n(r.cowork), value: n(r.value) })) }
89
+ console.log(JSON.stringify(out, null, 2))
90
+ return
91
+ }
92
+
93
+ // Markdown output
94
+ if (md) {
95
+ outputMarkdown(rankings, meta, breakdown)
96
+ return
97
+ }
98
+
99
+ // Table output (default)
100
+ outputTable(rankings, meta, breakdown)
101
+ }
102
+
103
+ // --- Formatters ---
104
+
105
+ const n = (v) => v != null ? Number(v.toFixed(2)) : null
106
+ const fmtScore = (v) => v != null ? v.toFixed(1) : dim('—')
107
+ const fmtPrice = (v) => v != null ? price(`$${Number(v.toFixed(2))}`) : dim('—')
108
+ const fmtValue = (v) => v != null ? v.toFixed(1) : dim('—')
109
+ const pad = (str, len, right = false) => {
110
+ const s = String(str)
111
+ return right ? s.padStart(len) : s.padEnd(len)
112
+ }
113
+ const trunc = (name, max) => name.length > max ? name.slice(0, max - 2) + '..' : name
114
+
115
+ function outputTable (rankings, meta, breakdown) {
116
+ const nameW = Math.min(32, Math.max(20, ...rankings.map(r => r.model.length)))
117
+
118
+ console.log(`\n${label('Model Rankings')} ${dim(`(${meta.date}, ${meta.useCase})`)}`)
119
+ console.log(dim(`Sources: ${meta.sources.join(', ')}\n`))
120
+
121
+ if (breakdown) {
122
+ const hdr = ` # ${pad('Model', nameW)} Overall Analysis Tool CoWork $/1M out Value Cov`
123
+ console.log(dim(hdr))
124
+ console.log(dim('─'.repeat(hdr.length)))
125
+ for (const r of rankings) {
126
+ console.log(
127
+ `${pad(r.rank, 2, true)} ${pad(trunc(r.model, nameW), nameW)}` +
128
+ ` ${pad(fmtScore(r.overall), 7, true)}` +
129
+ ` ${pad(fmtScore(r.analysis), 8, true)}` +
130
+ ` ${pad(fmtScore(r.tool_loop), 5, true)}` +
131
+ ` ${pad(fmtScore(r.cowork), 6, true)}` +
132
+ ` ${pad(fmtPrice(r.output_price), 8, true)}` +
133
+ ` ${pad(fmtValue(r.value), 5, true)}` +
134
+ ` ${pad(r.coverage, 3, true)}`
135
+ )
136
+ }
137
+ } else {
138
+ const hdr = ` # ${pad('Model', nameW)} Overall $/1M out Value Cov`
139
+ console.log(dim(hdr))
140
+ console.log(dim('─'.repeat(hdr.length)))
141
+ for (const r of rankings) {
142
+ console.log(
143
+ `${pad(r.rank, 2, true)} ${pad(trunc(r.model, nameW), nameW)}` +
144
+ ` ${pad(fmtScore(r.overall), 7, true)}` +
145
+ ` ${pad(fmtPrice(r.output_price), 8, true)}` +
146
+ ` ${pad(fmtValue(r.value), 5, true)}` +
147
+ ` ${pad(r.coverage, 3, true)}`
148
+ )
149
+ }
150
+ }
151
+ console.log()
152
+ }
153
+
154
+ function outputMarkdown (rankings, meta, breakdown) {
155
+ const fs = (v) => v != null ? v.toFixed(1) : '—'
156
+ const fp = (v) => v != null ? `$${Number(v.toFixed(2))}` : '—'
157
+ const fv = (v) => v != null ? v.toFixed(1) : '—'
158
+
159
+ console.log(`## Model Rankings (${meta.date}, ${meta.useCase})`)
160
+ console.log(`*Sources: ${meta.sources.join(', ')}*\n`)
161
+
162
+ if (breakdown) {
163
+ console.log('| # | Model | Overall | Analysis | Tool | CoWork | $/1M out | Value | Cov |')
164
+ console.log('|--:|-------|--------:|---------:|-----:|-------:|---------:|------:|----:|')
165
+ for (const r of rankings) {
166
+ console.log(`| ${r.rank} | ${r.model} | ${fs(r.overall)} | ${fs(r.analysis)} | ${fs(r.tool_loop)} | ${fs(r.cowork)} | ${fp(r.output_price)} | ${fv(r.value)} | ${r.coverage} |`)
167
+ }
168
+ } else {
169
+ console.log('| # | Model | Overall | $/1M out | Value | Cov |')
170
+ console.log('|--:|-------|--------:|---------:|------:|----:|')
171
+ for (const r of rankings) {
172
+ console.log(`| ${r.rank} | ${r.model} | ${fs(r.overall)} | ${fp(r.output_price)} | ${fv(r.value)} | ${r.coverage} |`)
173
+ }
174
+ }
175
+ console.log()
176
+ }
@@ -0,0 +1,160 @@
1
+ import mohdel, { silent } from '../lib/index.js'
2
+ import { parseJsonFlag, jsonOutputOne } from './json-output.js'
3
+
4
+ // CLI logger: silent for noisy levels, console.error for errors and fatals.
5
+ const cliLogger = { ...silent, error: console.error, fatal: console.error }
6
+
7
+ export async function runRateLimit (args) {
8
+ const jsonFlag = parseJsonFlag(args)
9
+ const [action, arg1, arg2, arg3] = args
10
+
11
+ if (!action || action === '-h' || action === '--help') {
12
+ console.log(`mohdel ratelimit — manage rate limits
13
+
14
+ Usage:
15
+ ratelimit show <model|provider> [--json] Show effective limits
16
+ ratelimit set <model> [rpm] [tpm] Set model-level limits
17
+ ratelimit rm <model> Remove model-level limits
18
+ ratelimit provider set <provider> [rpm] [tpm] Set provider-level limits
19
+ ratelimit provider rm <provider> Remove provider-level limits
20
+
21
+ Examples:
22
+ ratelimit show anthropic Show provider limits
23
+ ratelimit show gemini/gemini-2.0-flash Show model limits (with provider fallback)
24
+ ratelimit set gemini/gemini-2.0-flash 15 1000000
25
+ ratelimit provider set anthropic 60 100000
26
+
27
+ Aliases:
28
+ mo rl show <x> ratelimit show <x>
29
+
30
+ Configuration:
31
+ Provider-level limits stored in ~/.config/mohdel/providers.json
32
+ Model-level limits stored in ~/.config/mohdel/curated.json (per model entry)`)
33
+ process.exit(0)
34
+ }
35
+
36
+ const mo = await mohdel({ logger: cliLogger })
37
+
38
+ function useModel (id) {
39
+ try { return mo.use(id) } catch (err) {
40
+ console.error(err.message)
41
+ process.exit(1)
42
+ }
43
+ }
44
+
45
+ // --- provider subcommand ---
46
+ if (action === 'provider') {
47
+ const [providerAction, providerName, ...providerArgs] = args.slice(1)
48
+
49
+ if (providerAction === 'show') {
50
+ if (!providerName) { console.error('Usage: ratelimit provider show <provider>'); process.exit(1) }
51
+ const entry = mo.getProviderRateLimit(providerName)
52
+ if (!entry) {
53
+ console.log(`${providerName}: no limits set`)
54
+ } else {
55
+ const parts = []
56
+ if (entry.rpmLimit) parts.push(`rpm=${entry.rpmLimit}`)
57
+ if (entry.tpmLimit) parts.push(`tpm=${entry.tpmLimit}`)
58
+ console.log(`${providerName}: ${parts.join(' ')}`)
59
+ }
60
+ return
61
+ }
62
+
63
+ if (providerAction === 'set') {
64
+ if (!providerName) { console.error('Usage: ratelimit provider set <provider> [rpm] [tpm]'); process.exit(1) }
65
+ const [rpmStr, tpmStr] = providerArgs
66
+ const rpm = rpmStr ? parseInt(rpmStr, 10) : undefined
67
+ const tpm = tpmStr ? parseInt(tpmStr, 10) : undefined
68
+ if (rpm == null && tpm == null) { console.error('Provide at least rpm or tpm'); process.exit(1) }
69
+ const result = await mo.setProviderRateLimit(providerName, { rpm, tpm })
70
+ const parts = []
71
+ if (result.rpmLimit) parts.push(`rpm=${result.rpmLimit}`)
72
+ if (result.tpmLimit) parts.push(`tpm=${result.tpmLimit}`)
73
+ console.log(`${providerName}: ${parts.join(' ')}`)
74
+ return
75
+ }
76
+
77
+ if (providerAction === 'rm' || providerAction === 'remove') {
78
+ if (!providerName) { console.error('Usage: ratelimit provider rm <provider>'); process.exit(1) }
79
+ await mo.clearProviderRateLimit(providerName)
80
+ console.log(`${providerName}: limits cleared`)
81
+ return
82
+ }
83
+
84
+ console.error(`Unknown provider action: ${providerAction}. Run "ratelimit --help".`)
85
+ process.exit(1)
86
+ }
87
+
88
+ // --- model-level commands ---
89
+ if (action === 'show') {
90
+ if (!arg1) { console.error('Usage: ratelimit show <model|provider>'); process.exit(1) }
91
+
92
+ // Try as model first; fall back to provider
93
+ let model
94
+ try { model = mo.use(arg1) } catch {}
95
+
96
+ if (model) {
97
+ const info = model.info()
98
+ const providerEntry = mo.getProviderRateLimit(info.provider) || {}
99
+ const rpmLimit = info.rpmLimit ?? providerEntry.rpmLimit
100
+ const tpmLimit = info.tpmLimit ?? providerEntry.tpmLimit
101
+ const scope = info.rateLimitScope || 'provider'
102
+ const source = (info.rpmLimit || info.tpmLimit) ? 'model' : 'provider'
103
+ if (jsonFlag.json) {
104
+ jsonOutputOne({ id: arg1, rpmLimit: rpmLimit || null, tpmLimit: tpmLimit || null, scope, source })
105
+ return
106
+ }
107
+ if (!rpmLimit && !tpmLimit) {
108
+ console.log(`${arg1}: no limits`)
109
+ } else {
110
+ const parts = []
111
+ if (rpmLimit) parts.push(`rpm=${rpmLimit}`)
112
+ if (tpmLimit) parts.push(`tpm=${tpmLimit}`)
113
+ parts.push(`scope=${scope}`)
114
+ parts.push(`(${source})`)
115
+ console.log(`${arg1}: ${parts.join(' ')}`)
116
+ }
117
+ } else {
118
+ // Treat as provider name
119
+ const entry = mo.getProviderRateLimit(arg1)
120
+ if (jsonFlag.json) {
121
+ jsonOutputOne({ provider: arg1, rpmLimit: entry?.rpmLimit || null, tpmLimit: entry?.tpmLimit || null })
122
+ return
123
+ }
124
+ if (!entry) {
125
+ console.log(`${arg1}: no limits set`)
126
+ } else {
127
+ const parts = []
128
+ if (entry.rpmLimit) parts.push(`rpm=${entry.rpmLimit}`)
129
+ if (entry.tpmLimit) parts.push(`tpm=${entry.tpmLimit}`)
130
+ console.log(`${arg1}: ${parts.join(' ')}`)
131
+ }
132
+ }
133
+ return
134
+ }
135
+
136
+ if (action === 'set') {
137
+ if (!arg1) { console.error('Usage: ratelimit set <model> [rpm] [tpm]'); process.exit(1) }
138
+ const rpm = arg2 ? parseInt(arg2, 10) : undefined
139
+ const tpm = arg3 ? parseInt(arg3, 10) : undefined
140
+ if (rpm == null && tpm == null) { console.error('Provide at least rpm or tpm'); process.exit(1) }
141
+ const model = useModel(arg1)
142
+ const result = await model.setRateLimit({ rpm, tpm })
143
+ const parts = []
144
+ if (result.rpmLimit) parts.push(`rpm=${result.rpmLimit}`)
145
+ if (result.tpmLimit) parts.push(`tpm=${result.tpmLimit}`)
146
+ console.log(`${arg1}: ${parts.join(' ')} scope=model`)
147
+ return
148
+ }
149
+
150
+ if (action === 'rm' || action === 'remove') {
151
+ if (!arg1) { console.error('Usage: ratelimit rm <model>'); process.exit(1) }
152
+ const model = useModel(arg1)
153
+ await model.clearRateLimit()
154
+ console.log(`${arg1}: model limits cleared`)
155
+ return
156
+ }
157
+
158
+ console.error(`Unknown action: ${action}. Run "ratelimit --help".`)
159
+ process.exit(1)
160
+ }
package/src/cli/tag.js ADDED
@@ -0,0 +1,105 @@
1
+ import mohdel, { silent } from '../lib/index.js'
2
+ import { parseJsonFlag, jsonOutput } from './json-output.js'
3
+ import { id, label, tag, meta, err } from './colors.js'
4
+
5
+ // CLI logger: silent for noisy levels, console.error for errors and fatals.
6
+ const cliLogger = { ...silent, error: console.error, fatal: console.error }
7
+
8
+ export async function runTag (args) {
9
+ const jsonFlag = parseJsonFlag(args)
10
+ const [action, arg1, arg2] = args
11
+
12
+ if (!action || action === '-h' || action === '--help') {
13
+ console.log(`mohdel tag — manage model tags
14
+
15
+ Usage:
16
+ tag list [--json] List all unique tags
17
+ tag list <model> [--json] Show tags on a model
18
+ tag show <tag> [--json] List models with a tag
19
+ tag add <model> <tag> Add a tag to a model
20
+ tag rm <model> <tag> Remove a tag from a model`)
21
+ process.exit(0)
22
+ }
23
+
24
+ const mo = await mohdel({ logger: cliLogger })
25
+
26
+ function useModel (modelId) {
27
+ try { return mo.use(modelId) } catch (e) {
28
+ console.error(err(e.message))
29
+ process.exit(1)
30
+ }
31
+ }
32
+
33
+ if (action === 'list') {
34
+ // tag list <model> — show tags on a model
35
+ if (arg1) {
36
+ const modelTags = useModel(arg1).tags()
37
+ if (jsonFlag.json) {
38
+ jsonOutput(modelTags.map(t => ({ tag: t })), jsonFlag.fields)
39
+ return
40
+ }
41
+ console.log(modelTags.length ? modelTags.map(t => tag(t)).join(', ') : meta('(no tags)'))
42
+ return
43
+ }
44
+ // tag list — list all unique tags
45
+ const all = mo.list()
46
+ const tags = new Set()
47
+ for (const m of all) {
48
+ for (const t of mo.use(m.value).tags()) tags.add(t)
49
+ }
50
+ const sorted = [...tags].sort()
51
+ if (jsonFlag.json) {
52
+ jsonOutput(sorted.map(t => ({ tag: t })), jsonFlag.fields)
53
+ return
54
+ }
55
+ for (const t of sorted) console.log(tag(t))
56
+ return
57
+ }
58
+
59
+ // tag show <tag> — list models with a tag
60
+ if (action === 'show') {
61
+ if (!arg1) { console.error('Usage: tag show <tag>'); process.exit(1) }
62
+ const models = mo.list(arg1)
63
+ if (!models.length) { console.log(meta(`No models with tag "${arg1}"`)); return }
64
+ if (jsonFlag.json) {
65
+ jsonOutput(models.map(m => ({ id: m.value, label: m.label })), jsonFlag.fields)
66
+ return
67
+ }
68
+ for (const m of models) console.log(`${id(m.value)} ${label(m.label)}`)
69
+ return
70
+ }
71
+
72
+ // tag model <model> — backward compat alias for tag list <model>
73
+ if (action === 'model') {
74
+ if (!arg1) { console.error('Usage: tag list <model>'); process.exit(1) }
75
+ const modelTags = useModel(arg1).tags()
76
+ if (jsonFlag.json) {
77
+ jsonOutput(modelTags.map(t => ({ tag: t })), jsonFlag.fields)
78
+ return
79
+ }
80
+ console.log(modelTags.length ? modelTags.map(t => tag(t)).join(', ') : meta('(no tags)'))
81
+ return
82
+ }
83
+
84
+ if (action === 'add') {
85
+ if (!arg1 || !arg2) { console.error('Usage: tag add <model> <tag>'); process.exit(1) }
86
+ try {
87
+ const modelTags = await useModel(arg1).addTag(arg2)
88
+ console.log(`${id(arg1)}: ${modelTags.map(t => tag(t)).join(', ')}`)
89
+ } catch (e) {
90
+ console.error(err(e.message))
91
+ process.exit(1)
92
+ }
93
+ return
94
+ }
95
+
96
+ if (action === 'rm' || action === 'remove') {
97
+ if (!arg1 || !arg2) { console.error('Usage: tag rm <model> <tag>'); process.exit(1) }
98
+ const modelTags = await useModel(arg1).delTag(arg2)
99
+ console.log(`${id(arg1)}: ${modelTags.length ? modelTags.map(t => tag(t)).join(', ') : meta('(no tags)')}`)
100
+ return
101
+ }
102
+
103
+ console.error(`Unknown action: ${action}. Run "tag --help".`)
104
+ process.exit(1)
105
+ }
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="-381.005 -57.828 64 64"><path d="M-373.848-13.722c-.26-.03-.665-.26-.953-.5-2.74-2.685.778-5.77 4.848-7.56v-5.105c.952.607 1.472.635 1.5.72l3.116-3.086-1.04-2.655c9.12-3.145 12.782-4.357 20.198-5.598l-1.33-1.242 2.165-1.24c5 1.646 9.696 2.078 8.887 6.26.203-2.828-3.318-3.925-8.685-5.425l-1.038.634 2 1.645c-8.886 1.53-14.34 3.26-20.832 5.5l.893 2.3-3.26 3.202c.55.144 6.232 2.05 12.146-2.077 0 0 .117-.087.117-.116-.172-.288-.578-.607-1.067-1.038 1.73.116 2.854 1.645 2.653 3.232h-.807a2.9 2.9 0 0 0-.231-1.443c-4.588 3.348-9.897 3.838-14.428 2.25v3.952c-2.192.78-6.118 3.2-6.1 5.54.144 1.124.722 1.528 1.24 1.818"/><path d="M-355.293-14.733c-4.908 2.482-9.6 4.473-16.852 4.76-9.435-.203-10.906-6.464-6.435-13.014 4.213-6.607 10.907-12.58 21.555-16.304 3.086-1.094 7.62-2.336 12.002-2.394 6.3-.058 12.4 1.905 12.003 7.936-.23 4.617-6.895 11.138-10.417 15.813-1.5 2.046-1.76 3.375.807 3.26 9.322-.607 17.775-3.838 25.624-7.185-5.308 3.607-32.75 17.196-32.864 7.936.028-1.185.576-2.424 1.47-3.752.867-1.327 2.08-2.714 3.32-4.127 1.875-2.135 6.518-7.242 8.078-10.302 2.626-5.8-3.26-6.1-8.34-7.877l-2.165 1.24 1.33 1.242c-7.416 1.24-11.08 2.452-20.198 5.598l1.04 2.655-3.116 3.086c-.087-.086-.55-.114-1.5-.72v5.107c-4.07 1.8-7.6 4.875-4.848 7.56.288.23.694.462.954.5 5 2.828 18.58-1 18.554-1" fill="#f60"/></svg>
@@ -0,0 +1,5 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <svg width="46" height="46" viewBox="0 0 46 46" fill="none" xmlns="http://www.w3.org/2000/svg">
3
+ <circle cx="23" cy="23" r="23" fill="white"/>
4
+ <path d="M32.73 7h-6.945L38.45 39h6.945L32.73 7ZM12.665 7 0 39h7.082l2.59-6.72h13.25l2.59 6.72h7.082L19.929 7h-7.264Zm-.702 19.337 4.334-11.246 4.334 11.246h-8.668Z" fill="#000000"></path>
5
+ </svg>
@@ -0,0 +1 @@
1
+ <svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>DeepSeek</title><path d="M23.748 4.482c-.254-.124-.364.113-.512.234-.051.039-.094.09-.137.136-.372.397-.806.657-1.373.626-.829-.046-1.537.214-2.163.848-.133-.782-.575-1.248-1.247-1.548-.352-.156-.708-.311-.955-.65-.172-.241-.219-.51-.305-.774-.055-.16-.11-.323-.293-.35-.2-.031-.278.136-.356.276-.313.572-.434 1.202-.422 1.84.027 1.436.633 2.58 1.838 3.393.137.093.172.187.129.323-.082.28-.18.552-.266.833-.055.179-.137.217-.329.14a5.526 5.526 0 01-1.736-1.18c-.857-.828-1.631-1.742-2.597-2.458a11.365 11.365 0 00-.689-.471c-.985-.957.13-1.743.388-1.836.27-.098.093-.432-.779-.428-.872.004-1.67.295-2.687.684a3.055 3.055 0 01-.465.137 9.597 9.597 0 00-2.883-.102c-1.885.21-3.39 1.102-4.497 2.623C.082 8.606-.231 10.684.152 12.85c.403 2.284 1.569 4.175 3.36 5.653 1.858 1.533 3.997 2.284 6.438 2.14 1.482-.085 3.133-.284 4.994-1.86.47.234.962.327 1.78.397.63.059 1.236-.03 1.705-.128.735-.156.684-.837.419-.961-2.155-1.004-1.682-.595-2.113-.926 1.096-1.296 2.746-2.642 3.392-7.003.05-.347.007-.565 0-.845-.004-.17.035-.237.23-.256a4.173 4.173 0 001.545-.475c1.396-.763 1.96-2.015 2.093-3.517.02-.23-.004-.467-.247-.588zM11.581 18c-2.089-1.642-3.102-2.183-3.52-2.16-.392.024-.321.471-.235.763.09.288.207.486.371.739.114.167.192.416-.113.603-.673.416-1.842-.14-1.897-.167-1.361-.802-2.5-1.86-3.301-3.307-.774-1.393-1.224-2.887-1.298-4.482-.02-.386.093-.522.477-.592a4.696 4.696 0 011.529-.039c2.132.312 3.946 1.265 5.468 2.774.868.86 1.525 1.887 2.202 2.891.72 1.066 1.494 2.082 2.48 2.914.348.292.625.514.891.677-.802.09-2.14.11-3.054-.614zm1-6.44a.306.306 0 01.415-.287.302.302 0 01.2.288.306.306 0 01-.31.307.303.303 0 01-.304-.308zm3.11 1.596c-.2.081-.399.151-.59.16a1.245 1.245 0 01-.798-.254c-.274-.23-.47-.358-.552-.758a1.73 1.73 0 01.016-.588c.07-.327-.008-.537-.239-.727-.187-.156-.426-.199-.688-.199a.559.559 0 01-.254-.078c-.11-.054-.2-.19-.114-.358.028-.054.16-.186.192-.21.356-.202.767-.136 1.146.016.352.144.618.408 1.001.782.391.451.462.576.685.914.176.265.336.537.445.848.067.195-.019.354-.25.452z" fill="#4D6BFE"></path></svg>