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,208 @@
1
+ import { join } from 'path'
2
+ import { existsSync } from 'fs'
3
+ import { readFile, writeFile, mkdir, copyFile, stat } from 'fs/promises'
4
+ import envPaths from 'env-paths'
5
+ import { validate, stripComputed } from './schema.js'
6
+ import { silent } from './logger.js'
7
+
8
+ // Module-level logger — set by mohdel factory via setLogger().
9
+ // Defaults to silent so file-load operations don't spam console when imported standalone.
10
+ let moduleLogger = silent
11
+ export const setLogger = (logger) => { moduleLogger = logger || silent }
12
+
13
+ export const CONFIG_DIR = envPaths('mohdel', { suffix: null }).config
14
+ export const CONFIG_PATH = join(CONFIG_DIR, 'default.json')
15
+ export const CURATED_PATH = join(CONFIG_DIR, 'curated.json')
16
+ export const EXCLUDED_PATH = join(CONFIG_DIR, 'excluded.json')
17
+ export const PROVIDERS_CONFIG_PATH = join(CONFIG_DIR, 'providers.json')
18
+ export const ENV_PATH = join(CONFIG_DIR, 'environment')
19
+
20
+ const DEFAULT_CURATED = {}
21
+
22
+ const DEFAULT_EXCLUDED = {}
23
+
24
+ export const loadEnvFile = (envPath) => {
25
+ try {
26
+ process.loadEnvFile(envPath)
27
+ } catch (err) {
28
+ if (err.code !== 'ENOENT') {
29
+ throw err
30
+ }
31
+ }
32
+ }
33
+
34
+ export const loadDefaultEnv = () => {
35
+ loadEnvFile(ENV_PATH)
36
+ }
37
+
38
+ export const getAPIKey = (envVarName) => {
39
+ if (process.env[envVarName]) {
40
+ return process.env[envVarName]
41
+ }
42
+ return null
43
+ }
44
+
45
+ const sortObjectKeys = (obj) => {
46
+ if (!obj || typeof obj !== 'object' || Array.isArray(obj)) return obj
47
+
48
+ return Object.keys(obj)
49
+ .sort()
50
+ .reduce((sorted, key) => {
51
+ sorted[key] = obj[key]
52
+ return sorted
53
+ }, {})
54
+ }
55
+
56
+ // 3-slot backup rotation: .prev (every save), .daily (first save of the day), .weekly (first save of the week)
57
+ const getWeek = (ms) => {
58
+ const d = new Date(ms)
59
+ return `${d.getFullYear()}-W${String(Math.ceil((d.getDate() + new Date(d.getFullYear(), d.getMonth(), 1).getDay()) / 7)).padStart(2, '0')}-${d.getMonth()}`
60
+ }
61
+
62
+ const rotateBackup = async (filePath) => {
63
+ if (!existsSync(filePath)) return
64
+
65
+ const prev = filePath + '.prev'
66
+ const daily = filePath + '.daily'
67
+ const weekly = filePath + '.weekly'
68
+
69
+ try {
70
+ // Always: current → .prev
71
+ await copyFile(filePath, prev)
72
+
73
+ // First save of new day: .prev → .daily
74
+ const prevMtime = (await stat(prev)).mtimeMs
75
+ const dailyMtime = existsSync(daily) ? (await stat(daily)).mtimeMs : 0
76
+ if (new Date(prevMtime).toDateString() !== new Date(dailyMtime).toDateString()) {
77
+ await copyFile(prev, daily)
78
+ }
79
+
80
+ // First save of new week: .daily → .weekly
81
+ if (existsSync(daily)) {
82
+ const dMtime = (await stat(daily)).mtimeMs
83
+ const wMtime = existsSync(weekly) ? (await stat(weekly)).mtimeMs : 0
84
+ if (getWeek(dMtime) !== getWeek(wMtime)) {
85
+ await copyFile(daily, weekly)
86
+ }
87
+ }
88
+ } catch (err) {
89
+ moduleLogger.warn(`[mohdel:common] backup rotation failed: ${err.message}`)
90
+ }
91
+ }
92
+
93
+ export const BACKUP_SLOTS = ['prev', 'daily', 'weekly']
94
+
95
+ const createFileOperation = (filePath, defaultValue = {}, operationType) => {
96
+ const loadHandler = async () => {
97
+ let loadedData
98
+ try {
99
+ if (!existsSync(CONFIG_DIR)) {
100
+ await mkdir(CONFIG_DIR, { recursive: true })
101
+ }
102
+
103
+ if (!existsSync(filePath)) {
104
+ if (defaultValue && Object.keys(defaultValue).length > 0) {
105
+ await writeFile(filePath, JSON.stringify(defaultValue, null, 2))
106
+ loadedData = JSON.parse(JSON.stringify(defaultValue))
107
+ } else {
108
+ loadedData = {}
109
+ }
110
+ } else {
111
+ const fileContent = await readFile(filePath, 'utf8')
112
+ loadedData = JSON.parse(fileContent)
113
+ }
114
+
115
+ if (typeof loadedData === 'object' && loadedData !== null && !Array.isArray(loadedData)) {
116
+ const processedData = {}
117
+ for (const [key, entryValue] of Object.entries(loadedData)) {
118
+ if (typeof entryValue !== 'object' || entryValue === null || Array.isArray(entryValue)) {
119
+ processedData[key] = entryValue
120
+ continue
121
+ }
122
+ const entry = { ...entryValue }
123
+ const upstreamIds = []
124
+ if (entry.model) {
125
+ upstreamIds.push(entry.model)
126
+ }
127
+ if (entry.aliases && Array.isArray(entry.aliases) && entry.aliases.length > 0) {
128
+ upstreamIds.push(...entry.aliases)
129
+ }
130
+ processedData[key] = { ...entry, upstreamIds }
131
+ }
132
+
133
+ if (operationType === 'curated models') {
134
+ for (const [curatedKey, entry] of Object.entries(processedData)) {
135
+ const issues = validate(entry, curatedKey)
136
+ for (const issue of issues) {
137
+ moduleLogger.warn(`[mohdel:schema] ${curatedKey}: ${issue.field} — ${issue.message}`)
138
+ }
139
+ }
140
+ }
141
+
142
+ return processedData
143
+ }
144
+
145
+ return loadedData
146
+ } catch (err) {
147
+ moduleLogger.warn(`[mohdel:common] failed to load ${operationType}: ${err.message}`)
148
+ return JSON.parse(JSON.stringify(defaultValue || {}))
149
+ }
150
+ }
151
+
152
+ const saveHandler = async (data) => {
153
+ try {
154
+ if (!existsSync(CONFIG_DIR)) {
155
+ await mkdir(CONFIG_DIR, { recursive: true })
156
+ }
157
+
158
+ await rotateBackup(filePath)
159
+
160
+ let dataToSave = data
161
+ if (typeof data === 'object' && data !== null && !Array.isArray(data)) {
162
+ const cleanedData = {}
163
+ for (const [key, entry] of Object.entries(data)) {
164
+ cleanedData[key] = (typeof entry === 'object' && entry !== null && !Array.isArray(entry))
165
+ ? stripComputed(entry)
166
+ : entry
167
+ }
168
+ dataToSave = cleanedData
169
+ }
170
+
171
+ const sortedData = sortObjectKeys(dataToSave)
172
+ await writeFile(filePath, JSON.stringify(sortedData, null, 2))
173
+ return true
174
+ } catch (err) {
175
+ moduleLogger.error(`[mohdel:common] failed to save ${operationType}: ${err.message}`)
176
+ if (operationType !== 'configuration') {
177
+ throw new Error(`Failed to save ${operationType}: ${err.message}`)
178
+ }
179
+ return false
180
+ }
181
+ }
182
+
183
+ return { load: loadHandler, save: saveHandler }
184
+ }
185
+
186
+ const configOps = createFileOperation(CONFIG_PATH, {}, 'configuration')
187
+ const curatedOps = createFileOperation(CURATED_PATH, DEFAULT_CURATED, 'curated models')
188
+ const excludedOps = createFileOperation(EXCLUDED_PATH, DEFAULT_EXCLUDED, 'excluded models')
189
+ const providersConfigOps = createFileOperation(PROVIDERS_CONFIG_PATH, {}, 'provider config')
190
+
191
+ export const getConfig = configOps.load
192
+ export const saveConfig = configOps.save
193
+ export const getCuratedModels = curatedOps.load
194
+ export const saveCuratedModels = curatedOps.save
195
+ export const getExcludedModels = excludedOps.load
196
+ export const saveExcludedModels = excludedOps.save
197
+ export const getProvidersConfig = providersConfigOps.load
198
+ export const saveProvidersConfig = providersConfigOps.save
199
+
200
+ export const getDefaultModelId = async () => {
201
+ const config = await getConfig()
202
+
203
+ if (config.defaultModel) {
204
+ return config.defaultModel
205
+ }
206
+
207
+ throw new Error('No default model configured. Run \'mo default\' to set up a default model.')
208
+ }
@@ -0,0 +1,63 @@
1
+ import { Severity, MohdelError } from './errors.js'
2
+ import { silent } from './logger.js'
3
+
4
+ const createCooldownTracker = (threshold = 3, durationMs = 60000, { logger = silent } = {}) => {
5
+ // key → { failCount, until, reason }
6
+ const entries = new Map()
7
+
8
+ const check = (key) => {
9
+ const entry = entries.get(key)
10
+ if (!entry || !entry.until) return null
11
+ if (Date.now() >= entry.until) {
12
+ entries.delete(key)
13
+ logger.debug({ key }, '[mohdel:cooldown] expired')
14
+ return null
15
+ }
16
+ return entry
17
+ }
18
+
19
+ const recordFailure = (key, { immediate = false, cause } = {}) => {
20
+ const entry = entries.get(key) || { failCount: 0, until: null, reason: null }
21
+ entry.failCount++
22
+ if (cause) entry.lastCause = cause
23
+ const shouldCooldown = immediate || entry.failCount >= threshold
24
+ if (shouldCooldown) {
25
+ entry.until = Date.now() + durationMs
26
+ entry.reason = immediate ? 'auth' : 'consecutive_failures'
27
+ entries.set(key, entry)
28
+ logger.info({
29
+ key,
30
+ reason: entry.reason,
31
+ failCount: entry.failCount,
32
+ durationMs,
33
+ triggeredBy: entry.lastCause || null
34
+ }, '[mohdel:cooldown] activated')
35
+ return true
36
+ }
37
+ entries.set(key, entry)
38
+ return false
39
+ }
40
+
41
+ const reset = (key) => {
42
+ const had = entries.has(key)
43
+ entries.delete(key)
44
+ if (had) logger.debug({ key }, '[mohdel:cooldown] reset')
45
+ }
46
+
47
+ const throwIfCoolingDown = (key, span) => {
48
+ const entry = check(key)
49
+ if (!entry) return
50
+ const secsLeft = Math.ceil((entry.until - Date.now()) / 1000)
51
+ logger.trace({ key, secsLeft }, '[mohdel:cooldown] fast-fail')
52
+ throw new MohdelError('PROVIDER_COOLDOWN', {
53
+ severity: Severity.WARN,
54
+ retryable: true,
55
+ detail: `Provider ${key} is in cooldown for ${secsLeft}s after ${entry.failCount} consecutive failures (${entry.reason}).`,
56
+ context: { provider: key, failCount: entry.failCount, reason: entry.reason, cooldownUntil: entry.until }
57
+ })
58
+ }
59
+
60
+ return { check, recordFailure, reset, throwIfCoolingDown }
61
+ }
62
+
63
+ export default createCooldownTracker
@@ -0,0 +1,71 @@
1
+ const creators = {
2
+ alibaba: {
3
+ label: 'Alibaba',
4
+ logo: 'alibaba.svg',
5
+ description: 'Alibaba Cloud’s Qwen models target large-scale enterprise scenarios with strong multilingual and commerce-focused capabilities.'
6
+ },
7
+ bfl: {
8
+ label: 'Black Forest Labs',
9
+ logo: 'bfl.svg',
10
+ description: 'Black Forest Labs builds the Flux family of image generation models, delivering fast, high-quality text-to-image synthesis.'
11
+ },
12
+ anthropic: {
13
+ label: 'Anthropic',
14
+ logo: 'anthropic.svg',
15
+ description: 'Anthropic builds Claude models that emphasize safe reasoning, tool use, and reliable outputs for production assistants.'
16
+ },
17
+ deepseek: {
18
+ label: 'DeepSeek',
19
+ logo: 'deepseek.svg',
20
+ description: 'DeepSeek offers fast, cost-efficient foundation models optimized for coding, chat, and multilingual reasoning.'
21
+ },
22
+ google: {
23
+ label: 'Google',
24
+ logo: 'gemini.svg',
25
+ description: 'Google’s Gemini family blends multimodal understanding, coding assistance, and long-context reasoning from Google DeepMind research.'
26
+ },
27
+ kwaipilot: {
28
+ label: 'Kwaipilot',
29
+ logo: 'kwaipilot.svg',
30
+ description: 'Kuaishou\'s KwaiPilot team builds KAT-Coder, a MoE coding model with strong agentic and multi-step reasoning for software engineering tasks.'
31
+ },
32
+ meta: {
33
+ label: 'Meta',
34
+ logo: 'meta.svg',
35
+ description: 'Meta stewards the Llama ecosystem with open, widely adoptable models for chat, coding, and research.'
36
+ },
37
+ minimax: {
38
+ label: 'Minimax',
39
+ logo: 'minimax.svg',
40
+ description: 'Minimax offers versatile Chinese-first chat and coding models tuned for fast, cost-aware assistants and enterprise integrations.'
41
+ },
42
+ moonshotai: {
43
+ label: 'Moonshot AI',
44
+ logo: 'moonshotai.svg',
45
+ description: 'Moonshot AI ships fluent, Chinese-first assistants and lean models tuned for consumer chat and business workflows.'
46
+ },
47
+ openai: {
48
+ label: 'OpenAI',
49
+ logo: 'openai.svg',
50
+ description: 'OpenAI’s GPT and o-series models focus on broad tool-use, reasoning quality, and multimodal support across developer platforms.'
51
+ },
52
+ xai: {
53
+ label: 'xAI',
54
+ logo: 'xai.svg',
55
+ description: 'xAI develops Grok with real-time, web-aware chat and coding behavior aimed at terse, fast responses.'
56
+ },
57
+ xiaomi: {
58
+ label: 'Xiaomi',
59
+ logo: 'xiaomi.svg',
60
+ description: 'Xiaomi develops MiMo, a high-efficiency MoE reasoning model optimized for agentic coding and tool use at low inference cost.'
61
+ },
62
+ zai: {
63
+ label: 'Z AI',
64
+ logo: 'zai.svg',
65
+ description: 'Z AI delivers streamlined assistants with lightweight models oriented toward pragmatic productivity use cases.'
66
+ }
67
+ }
68
+
69
+ Object.freeze(creators)
70
+
71
+ export default creators
@@ -0,0 +1,146 @@
1
+ import { getCuratedModels, saveCuratedModels } from './common.js'
2
+
3
+ let curatedCache = null
4
+ let aliasMapCache = null
5
+
6
+ export const getMohdelModel = curatedKey => {
7
+ const [provider, ...modelParts] = curatedKey.split('/')
8
+ return { provider, model: modelParts.join('/') }
9
+ }
10
+
11
+ const BASE_NAME_RE = /^([^-\d]+-\d+(?:-\d+)*(?:-[a-z]+)?)/
12
+
13
+ const buildAliasMap = (curatedModels) => {
14
+ const aliasMap = new Map()
15
+ const modelCountByName = new Map()
16
+ const parsed = []
17
+
18
+ // Pass 1: count names and cache parsed results
19
+ for (const fullModelId in curatedModels) {
20
+ const { provider, model: modelName } = getMohdelModel(fullModelId)
21
+ const baseMatch = modelName.match(BASE_NAME_RE)
22
+ const baseName = baseMatch?.[1] || null
23
+
24
+ if (baseName) modelCountByName.set(baseName, (modelCountByName.get(baseName) || 0) + 1)
25
+ modelCountByName.set(modelName, (modelCountByName.get(modelName) || 0) + 1)
26
+ parsed.push({ fullModelId, provider, modelName, baseName, entry: curatedModels[fullModelId] })
27
+ }
28
+
29
+ // Pass 2: build aliases + explicit aliases
30
+ for (const { fullModelId, provider, modelName, baseName, entry } of parsed) {
31
+ if (modelCountByName.get(modelName) === 1) aliasMap.set(modelName, fullModelId)
32
+
33
+ if (baseName) {
34
+ if (modelCountByName.get(baseName) === 1) aliasMap.set(baseName, fullModelId)
35
+ aliasMap.set(`${provider}/${baseName}`, fullModelId)
36
+ }
37
+
38
+ if (Array.isArray(entry.aliases)) {
39
+ for (const alias of entry.aliases) {
40
+ if (!aliasMap.has(alias)) aliasMap.set(alias, fullModelId)
41
+ }
42
+ }
43
+ }
44
+
45
+ return aliasMap
46
+ }
47
+
48
+ const expandModelAliasInternal = (modelId) => {
49
+ if (curatedCache[modelId]) return modelId
50
+ if (aliasMapCache.has(modelId)) {
51
+ return aliasMapCache.get(modelId)
52
+ }
53
+ return modelId
54
+ }
55
+
56
+ const ensureCuratedCache = async () => {
57
+ if (!curatedCache) {
58
+ curatedCache = await getCuratedModels()
59
+ aliasMapCache = buildAliasMap(curatedCache)
60
+ }
61
+ return curatedCache
62
+ }
63
+
64
+ const rebuildAliasMap = () => {
65
+ aliasMapCache = buildAliasMap(curatedCache || {})
66
+ return aliasMapCache
67
+ }
68
+
69
+ export const loadCuratedCache = ensureCuratedCache
70
+
71
+ export const getCuratedCacheSnapshot = () => curatedCache
72
+
73
+ export const getAliasMapSnapshot = () => aliasMapCache
74
+
75
+ // Find close matches for a model ID that wasn't found.
76
+ // Returns array of { id, label } sorted by relevance (max 5).
77
+ export const suggestModels = (query, maxResults = 5) => {
78
+ if (!curatedCache) return []
79
+ const q = query.toLowerCase()
80
+ const scored = []
81
+
82
+ for (const fullId of Object.keys(curatedCache)) {
83
+ if (curatedCache[fullId].deprecated) continue
84
+ const entry = curatedCache[fullId]
85
+ const label = (entry.label || '').toLowerCase()
86
+ const id = fullId.toLowerCase()
87
+ const model = (entry.model || '').toLowerCase()
88
+
89
+ // Score by how well the query matches
90
+ let score = 0
91
+ if (id.includes(q)) score = 3
92
+ else if (model.includes(q)) score = 2.5
93
+ else if (label.includes(q)) score = 2
94
+ else {
95
+ // Fuzzy: check if all query segments appear somewhere
96
+ const terms = q.split(/[\s/\-_]+/).filter(Boolean)
97
+ const haystack = `${id} ${label} ${model}`
98
+ const matched = terms.filter(t => haystack.includes(t))
99
+ if (matched.length === terms.length) score = 1.5
100
+ else if (matched.length > 0) score = matched.length / terms.length
101
+ }
102
+
103
+ if (score > 0) scored.push({ id: fullId, label: entry.label || fullId, score })
104
+ }
105
+
106
+ scored.sort((a, b) => b.score - a.score || a.id.localeCompare(b.id))
107
+ return scored.slice(0, maxResults)
108
+ }
109
+
110
+ export const expandModelAliasSync = (modelId) => {
111
+ if (!curatedCache || !aliasMapCache) {
112
+ throw new Error('Curated cache has not been loaded yet')
113
+ }
114
+ return expandModelAliasInternal(modelId)
115
+ }
116
+
117
+ export const expandModelAlias = async (modelId) => {
118
+ await ensureCuratedCache()
119
+ return expandModelAliasInternal(modelId)
120
+ }
121
+
122
+ export const persistCuratedCache = async () => {
123
+ if (!curatedCache) {
124
+ await ensureCuratedCache()
125
+ }
126
+
127
+ rebuildAliasMap()
128
+ await saveCuratedModels(curatedCache)
129
+ return curatedCache
130
+ }
131
+
132
+ export const overwriteCuratedCache = async (nextCache) => {
133
+ curatedCache = nextCache || {}
134
+ return persistCuratedCache()
135
+ }
136
+
137
+ export const reloadCuratedCache = async () => {
138
+ curatedCache = null
139
+ aliasMapCache = null
140
+ return ensureCuratedCache()
141
+ }
142
+
143
+ export const clearCuratedCache = () => {
144
+ curatedCache = null
145
+ aliasMapCache = null
146
+ }
@@ -0,0 +1,126 @@
1
+ export const Severity = Object.freeze({
2
+ TRACE: Symbol('TRACE'),
3
+ DEBUG: Symbol('DEBUG'),
4
+ INFO: Symbol('INFO'),
5
+ WARN: Symbol('WARN'),
6
+ ERROR: Symbol('ERROR'),
7
+ FATAL: Symbol('FATAL')
8
+ })
9
+
10
+ export const getSeverityNumber = (severitySymbol) => {
11
+ switch (severitySymbol) {
12
+ case Severity.TRACE:
13
+ return 1
14
+ case Severity.DEBUG:
15
+ return 5
16
+ case Severity.INFO:
17
+ return 9
18
+ case Severity.WARN:
19
+ return 13
20
+ case Severity.ERROR:
21
+ return 17
22
+ case Severity.FATAL:
23
+ return 21
24
+ default:
25
+ throw new Error(`[mohdel] unknown severity symbol: ${String(severitySymbol)}`)
26
+ }
27
+ }
28
+
29
+ // NOTE used to mock upstream Error
30
+ export class APIError extends Error {
31
+ constructor (message, status = 500) {
32
+ super(message)
33
+ this.name = 'APIError'
34
+ this.status = status
35
+ }
36
+ }
37
+
38
+ // usually Severity at least info
39
+ export class MohdelError extends Error {
40
+ constructor (
41
+ message,
42
+ { cause, severity, detail, context, component = 'inference', retryable = false, silent = false } = {}
43
+ ) {
44
+ super(message, { cause })
45
+ this.name = 'MohdelError'
46
+ this.severity = severity
47
+ this.detail = detail
48
+ this.context = context
49
+ this.component = component
50
+ this.retryable = retryable
51
+ this.silent = silent
52
+ }
53
+ }
54
+
55
+ // Convert any error to the serialized transport shape. Duck-types on
56
+ // `detail` to distinguish typed errors (MohdelError) from plain Error.
57
+ export const toTransportError = (err, span) => {
58
+ const isTyped = err.detail !== undefined
59
+ return {
60
+ message: isTyped ? err.message : 'UNEXPECTED_ERROR',
61
+ detail: isTyped ? err.detail : 'An unexpected error occurred',
62
+ trace: span?.spanContext()?.traceId,
63
+ component: err.component || undefined,
64
+ context: err.context || undefined,
65
+ retryable: err.retryable ?? false,
66
+ silent: err.silent ?? false
67
+ }
68
+ }
69
+
70
+ export const retryableWarn = (err, detail) => {
71
+ return {
72
+ message: 'PROVIDER_OVERLOADED',
73
+ severity: Severity.WARN,
74
+ retryable: true,
75
+ detail,
76
+ cause: err
77
+ }
78
+ }
79
+ export const reportRetryable = (err, provider, detail) => {
80
+ detail ||= `**An unexpected error occurred**: ${provider}'s API failed to respond. Try again or switch to a different model. If the issue persists, please contact support and provide the Trace ID.`
81
+ return {
82
+ message: 'PROVIDER_RETRYABLE_ERROR',
83
+ severity: Severity.ERROR,
84
+ detail,
85
+ retryable: true,
86
+ cause: err
87
+ }
88
+ }
89
+
90
+ export const reportDefault = (err, provider) => {
91
+ return {
92
+ message: 'PROVIDER_ERROR',
93
+ severity: Severity.ERROR,
94
+ detail: `**An unexpected error occurred**: ${provider}'s API failed to respond. Try switching to a different model or please contact support and provide the Trace ID.`,
95
+ retryable: isConnectionError(err),
96
+ cause: err
97
+ }
98
+ }
99
+
100
+ export const reportContextOverflow = (err, provider) => {
101
+ return {
102
+ message: 'CONTEXT_OVERFLOW',
103
+ severity: Severity.WARN,
104
+ detail: `The prompt exceeds ${provider}'s context limit. Reduce input or switch to a larger context model.`,
105
+ retryable: false,
106
+ cause: err
107
+ }
108
+ }
109
+
110
+ export function isContextOverflowMessage (errMessage) {
111
+ const msg = (errMessage || '').toLowerCase()
112
+ if (msg.includes('context_length') || msg.includes('context length')) return true
113
+ if (msg.includes('token limit') || msg.includes('too long') || msg.includes('too many tokens')) return true
114
+ if (msg.includes('maximum context') || msg.includes('prompt is too long')) return true
115
+ if (msg.includes('max_tokens') && msg.includes('exceed')) return true
116
+ return false
117
+ }
118
+
119
+ function isConnectionError (err) {
120
+ const code = err?.code || err?.cause?.code
121
+ if (code === 'ECONNRESET' || code === 'ECONNREFUSED' || code === 'ETIMEDOUT' ||
122
+ code === 'EPIPE' || code === 'ENOTFOUND' || code === 'UND_ERR_CONNECT_TIMEOUT') return true
123
+ const msg = (err?.message || '').toLowerCase()
124
+ if (msg.includes('fetch failed') || msg.includes('socket hang up') || msg.includes('network')) return true
125
+ return false
126
+ }