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.
- package/LICENSE +21 -0
- package/README.md +377 -0
- package/config/benchmarks.json +39 -0
- package/js/client/call.js +75 -0
- package/js/client/call_image.js +82 -0
- package/js/client/gate-binary.js +72 -0
- package/js/client/index.js +16 -0
- package/js/client/ndjson.js +29 -0
- package/js/client/transport.js +48 -0
- package/js/core/envelope.js +141 -0
- package/js/core/errors.js +75 -0
- package/js/core/events.js +96 -0
- package/js/core/image.js +58 -0
- package/js/core/index.js +10 -0
- package/js/core/status.js +48 -0
- package/js/factory/bridge.js +372 -0
- package/js/session/_cooldown.js +114 -0
- package/js/session/_logger.js +138 -0
- package/js/session/_rate_limiter.js +77 -0
- package/js/session/_tracing.js +58 -0
- package/js/session/adapters/_cancelled.js +44 -0
- package/js/session/adapters/_catalog.js +58 -0
- package/js/session/adapters/_chat_completions.js +439 -0
- package/js/session/adapters/_errors.js +85 -0
- package/js/session/adapters/_images.js +60 -0
- package/js/session/adapters/_lazy_json_cache.js +76 -0
- package/js/session/adapters/_pricing.js +67 -0
- package/js/session/adapters/_providers.js +60 -0
- package/js/session/adapters/_tools.js +185 -0
- package/js/session/adapters/_videos.js +283 -0
- package/js/session/adapters/anthropic.js +397 -0
- package/js/session/adapters/cerebras.js +28 -0
- package/js/session/adapters/deepseek.js +32 -0
- package/js/session/adapters/echo.js +51 -0
- package/js/session/adapters/fake.js +262 -0
- package/js/session/adapters/fireworks.js +46 -0
- package/js/session/adapters/gemini.js +381 -0
- package/js/session/adapters/groq.js +23 -0
- package/js/session/adapters/image/fake.js +55 -0
- package/js/session/adapters/image/index.js +40 -0
- package/js/session/adapters/image/novita.js +135 -0
- package/js/session/adapters/image/openai.js +50 -0
- package/js/session/adapters/index.js +53 -0
- package/js/session/adapters/mistral.js +31 -0
- package/js/session/adapters/novita.js +29 -0
- package/js/session/adapters/openai.js +381 -0
- package/js/session/adapters/openrouter.js +66 -0
- package/js/session/adapters/xai.js +27 -0
- package/js/session/bin.js +54 -0
- package/js/session/driver.js +160 -0
- package/js/session/index.js +18 -0
- package/js/session/run.js +393 -0
- package/js/session/run_image.js +61 -0
- package/package.json +107 -0
- package/src/cli/ask.js +160 -0
- package/src/cli/backup.js +107 -0
- package/src/cli/bench.js +262 -0
- package/src/cli/check.js +123 -0
- package/src/cli/colored-logger.js +67 -0
- package/src/cli/colors.js +13 -0
- package/src/cli/default.js +39 -0
- package/src/cli/index.js +150 -0
- package/src/cli/json-output.js +60 -0
- package/src/cli/model.js +571 -0
- package/src/cli/onboard.js +232 -0
- package/src/cli/rank.js +176 -0
- package/src/cli/ratelimit.js +160 -0
- package/src/cli/tag.js +105 -0
- package/src/lib/assets/alibaba.svg +1 -0
- package/src/lib/assets/anthropic.svg +5 -0
- package/src/lib/assets/deepseek.svg +1 -0
- package/src/lib/assets/gemini.svg +1 -0
- package/src/lib/assets/google.svg +2 -0
- package/src/lib/assets/kwaipilot.svg +1 -0
- package/src/lib/assets/meta.svg +1 -0
- package/src/lib/assets/minimax.svg +9 -0
- package/src/lib/assets/moonshotai.svg +4 -0
- package/src/lib/assets/openai.svg +5 -0
- package/src/lib/assets/xai.svg +1 -0
- package/src/lib/assets/xiaomi.svg +2 -0
- package/src/lib/assets/zai.svg +219 -0
- package/src/lib/benchmark-score.js +215 -0
- package/src/lib/benchmark-truth.js +68 -0
- package/src/lib/cache.js +76 -0
- package/src/lib/common.js +208 -0
- package/src/lib/cooldown.js +63 -0
- package/src/lib/creators.js +71 -0
- package/src/lib/curated-cache.js +146 -0
- package/src/lib/errors.js +126 -0
- package/src/lib/index.js +726 -0
- package/src/lib/logger.js +29 -0
- package/src/lib/providers.js +87 -0
- package/src/lib/rank.js +390 -0
- package/src/lib/rate-limiter.js +50 -0
- package/src/lib/schema.js +150 -0
- package/src/lib/select.js +474 -0
- package/src/lib/tracing.js +62 -0
- 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
|
+
}
|
package/src/cli/rank.js
ADDED
|
@@ -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>
|