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,474 @@
|
|
|
1
|
+
import * as clack from '@clack/prompts'
|
|
2
|
+
import providers from './providers.js'
|
|
3
|
+
import {
|
|
4
|
+
getAPIKey,
|
|
5
|
+
getCuratedModels,
|
|
6
|
+
getExcludedModels,
|
|
7
|
+
saveCuratedModels,
|
|
8
|
+
saveExcludedModels,
|
|
9
|
+
loadEnvFile,
|
|
10
|
+
loadDefaultEnv
|
|
11
|
+
} from './common.js'
|
|
12
|
+
import { getMohdelModel } from './curated-cache.js'
|
|
13
|
+
import { stripUnknown } from './schema.js'
|
|
14
|
+
import { silent } from './logger.js'
|
|
15
|
+
|
|
16
|
+
loadEnvFile('.env')
|
|
17
|
+
loadDefaultEnv()
|
|
18
|
+
|
|
19
|
+
export const initializeAPIs = async () => {
|
|
20
|
+
const api = {}
|
|
21
|
+
const providersWithKeys = []
|
|
22
|
+
|
|
23
|
+
for (const [name, config] of Object.entries(providers)) {
|
|
24
|
+
try {
|
|
25
|
+
if (config.catalog === false || !config.apiKeyEnv) {
|
|
26
|
+
continue
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Get API key using the common.js functionality
|
|
30
|
+
const apiKey = getAPIKey(config.apiKeyEnv)
|
|
31
|
+
|
|
32
|
+
if (!apiKey) {
|
|
33
|
+
console.warn(`Warning: No API key found for ${name} (env var: ${config.apiKeyEnv})`)
|
|
34
|
+
continue
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Create configuration
|
|
38
|
+
const sdkConfig = config.createConfiguration(apiKey)
|
|
39
|
+
|
|
40
|
+
// Import the SDK module dynamically
|
|
41
|
+
const sdkPath = `./sdk/${config.sdk}.js`
|
|
42
|
+
const { default: API } = await import(sdkPath)
|
|
43
|
+
|
|
44
|
+
// Initialize the provider with the configuration, no specs
|
|
45
|
+
api[name] = API(sdkConfig, {}, silent)
|
|
46
|
+
providersWithKeys.push(name)
|
|
47
|
+
} catch (err) {
|
|
48
|
+
console.error(`Error initializing provider ${name} api:`, err.message)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return { api, providersWithKeys }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const getModelDetails = async (providerName, modelId, api) => {
|
|
56
|
+
try {
|
|
57
|
+
if (!api.getModelInfo) {
|
|
58
|
+
console.warn(`Provider ${providerName} does not support getModelInfo method`)
|
|
59
|
+
return null
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const modelInfo = await api.getModelInfo(modelId)
|
|
63
|
+
|
|
64
|
+
if (!modelInfo) {
|
|
65
|
+
console.warn(`Model ${modelId} not found in provider response`)
|
|
66
|
+
return null
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return modelInfo
|
|
70
|
+
} catch (err) {
|
|
71
|
+
console.error(`Error getting model details for ${modelId}:`, err.message)
|
|
72
|
+
return null
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Find potential models to replace based on the new model name
|
|
77
|
+
const findReplacementCandidates = (providerName, modelId, curated) => {
|
|
78
|
+
const baseNameMatch = modelId.match(/^(\w+[0-9.-]+)/)
|
|
79
|
+
if (!baseNameMatch) return []
|
|
80
|
+
|
|
81
|
+
const baseName = baseNameMatch[1]
|
|
82
|
+
const baseRegExp = new RegExp(`^${baseName}`)
|
|
83
|
+
|
|
84
|
+
const candidates = []
|
|
85
|
+
|
|
86
|
+
for (const [curatedKey, curatedInfo] of Object.entries(curated)) {
|
|
87
|
+
const { provider: curProviderName, model: curModelId } = getMohdelModel(curatedKey)
|
|
88
|
+
if (curProviderName === providerName && curModelId !== modelId) {
|
|
89
|
+
if (baseRegExp.test(curModelId)) {
|
|
90
|
+
candidates.push({
|
|
91
|
+
key: curatedKey,
|
|
92
|
+
label: curatedInfo.label,
|
|
93
|
+
modelId: curModelId
|
|
94
|
+
})
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return candidates
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Replace a model: move it to excluded and add the new one to curated
|
|
103
|
+
const replaceModel = async (modelToReplace, newCuratedKey, newModelLabel, newModelDetails, curated, excluded) => {
|
|
104
|
+
// Get the full data of the model to be replaced from the curated list
|
|
105
|
+
const oldModelDataFromCurated = curated[modelToReplace.key]
|
|
106
|
+
|
|
107
|
+
// Move the model to be replaced to excluded, preserving its original data
|
|
108
|
+
if (oldModelDataFromCurated) {
|
|
109
|
+
excluded[modelToReplace.key] = { ...oldModelDataFromCurated }
|
|
110
|
+
} else {
|
|
111
|
+
// Fallback if old data wasn't in curated (should not typically happen)
|
|
112
|
+
excluded[modelToReplace.key] = { label: modelToReplace.label }
|
|
113
|
+
}
|
|
114
|
+
delete curated[modelToReplace.key]
|
|
115
|
+
|
|
116
|
+
// Preserve custom properties from the old model entry,
|
|
117
|
+
// but explicitly exclude 'model' and 'models' fields from the old entry.
|
|
118
|
+
// This prevents old model identifiers from polluting the new entry if the
|
|
119
|
+
// new model's details (newModelDetails) don't specify them, ensuring consistent `upstreamIds`.
|
|
120
|
+
const {
|
|
121
|
+
model: _discardedOldModelIdentifier, // eslint-disable-line no-unused-vars
|
|
122
|
+
models: _discardedOldModelIdentifiers, // eslint-disable-line no-unused-vars
|
|
123
|
+
...restOfOldModelDataProperties
|
|
124
|
+
} = oldModelDataFromCurated || {}
|
|
125
|
+
|
|
126
|
+
// Add new model to curated.
|
|
127
|
+
// Merge properties: Start with applicable old properties (restOfOldModelDataProperties),
|
|
128
|
+
// then layer new model details (newModelDetails), which includes new provider & sdk.
|
|
129
|
+
// Finally, ensure the new label (newModelLabel) is set.
|
|
130
|
+
// Properties in newModelDetails and newModelLabel will override any from restOfOldModelDataProperties.
|
|
131
|
+
curated[newCuratedKey] = {
|
|
132
|
+
...restOfOldModelDataProperties,
|
|
133
|
+
...newModelDetails,
|
|
134
|
+
label: newModelLabel,
|
|
135
|
+
replaces: [...(restOfOldModelDataProperties.replaces || []), modelToReplace.key]
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Update both files
|
|
139
|
+
await saveCuratedModels(curated)
|
|
140
|
+
await saveExcludedModels(excluded)
|
|
141
|
+
|
|
142
|
+
return true
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const addAliasToExistingModel = async (targetCuratedKey, aliasModelId, curated) => {
|
|
146
|
+
const existingEntry = curated[targetCuratedKey]
|
|
147
|
+
if (!existingEntry) {
|
|
148
|
+
clack.log.error(`Unable to add alias: ${targetCuratedKey} not found in curated models`)
|
|
149
|
+
return false
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const nextAliases = Array.isArray(existingEntry.aliases) ? [...existingEntry.aliases] : []
|
|
153
|
+
if (!nextAliases.includes(aliasModelId)) {
|
|
154
|
+
nextAliases.push(aliasModelId)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const nextUpstreamIds = Array.isArray(existingEntry.upstreamIds) ? [...existingEntry.upstreamIds] : []
|
|
158
|
+
if (!nextUpstreamIds.includes(aliasModelId)) {
|
|
159
|
+
nextUpstreamIds.push(aliasModelId)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
curated[targetCuratedKey] = {
|
|
163
|
+
...existingEntry,
|
|
164
|
+
aliases: nextAliases,
|
|
165
|
+
upstreamIds: nextUpstreamIds
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
await saveCuratedModels(curated)
|
|
169
|
+
return true
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const isModelTrackedInCollection = (collection, providerName, modelId) => {
|
|
173
|
+
for (const [curatedKey, entry] of Object.entries(collection)) {
|
|
174
|
+
const { provider, model: keyModelId } = getMohdelModel(curatedKey)
|
|
175
|
+
if (provider !== providerName) continue
|
|
176
|
+
|
|
177
|
+
if (keyModelId === modelId) return true
|
|
178
|
+
|
|
179
|
+
const upstreamIds = Array.isArray(entry.upstreamIds) ? entry.upstreamIds : []
|
|
180
|
+
if (upstreamIds.includes(modelId)) {
|
|
181
|
+
return true
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return false
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export const promptMissingFields = async (entry, curatedKey) => {
|
|
188
|
+
if (!entry.creator) {
|
|
189
|
+
const [providerName] = curatedKey.split('/')
|
|
190
|
+
const providerConfig = providers[providerName]
|
|
191
|
+
const creatorsList = providerConfig?.creators || []
|
|
192
|
+
|
|
193
|
+
if (creatorsList.length === 1) {
|
|
194
|
+
entry.creator = creatorsList[0]
|
|
195
|
+
} else {
|
|
196
|
+
const creatorVal = await clack.select({
|
|
197
|
+
message: `Creator for ${curatedKey}:`,
|
|
198
|
+
options: creatorsList.map(c => ({ value: c, label: c }))
|
|
199
|
+
})
|
|
200
|
+
if (clack.isCancel(creatorVal)) return entry
|
|
201
|
+
entry.creator = creatorVal
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const labelVal = await clack.text({
|
|
206
|
+
message: `Label for ${curatedKey}:`,
|
|
207
|
+
initialValue: entry.label || entry.model || '',
|
|
208
|
+
validate: (v) => v ? undefined : 'Label is required'
|
|
209
|
+
})
|
|
210
|
+
if (clack.isCancel(labelVal)) return entry
|
|
211
|
+
entry.label = labelVal
|
|
212
|
+
|
|
213
|
+
const numericFields = [
|
|
214
|
+
{ key: 'contextTokenLimit', message: 'Context token limit:' },
|
|
215
|
+
{ key: 'outputTokenLimit', message: 'Output token limit:' },
|
|
216
|
+
{ key: 'inputPrice', message: 'Input price (per million tokens):' },
|
|
217
|
+
{ key: 'outputPrice', message: 'Output price (per million tokens):' }
|
|
218
|
+
]
|
|
219
|
+
|
|
220
|
+
for (const { key, message } of numericFields) {
|
|
221
|
+
if (entry[key] !== undefined) continue
|
|
222
|
+
const val = await clack.text({
|
|
223
|
+
message: `${message} (Enter to skip)`,
|
|
224
|
+
initialValue: '',
|
|
225
|
+
validate: (v) => {
|
|
226
|
+
if (!v) return undefined
|
|
227
|
+
const n = Number(v)
|
|
228
|
+
return Number.isFinite(n) ? undefined : 'Must be a number'
|
|
229
|
+
}
|
|
230
|
+
})
|
|
231
|
+
if (clack.isCancel(val)) return entry
|
|
232
|
+
if (val) entry[key] = Number(val)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// inputFormat — ask if model supports image input
|
|
236
|
+
if (!entry.inputFormat || !entry.inputFormat.length) {
|
|
237
|
+
const supportsImage = await clack.confirm({
|
|
238
|
+
message: `Does ${curatedKey} support image input?`,
|
|
239
|
+
initialValue: false
|
|
240
|
+
})
|
|
241
|
+
if (clack.isCancel(supportsImage)) return entry
|
|
242
|
+
entry.inputFormat = supportsImage ? ['text', 'image'] : ['text']
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return entry
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Threshold: providers with more uncurated models than this use search mode
|
|
249
|
+
const SEARCH_MODE_THRESHOLD = 50
|
|
250
|
+
const SEARCH_MAX_RESULTS = 15
|
|
251
|
+
|
|
252
|
+
const filterUncurated = (models, providerName, curated, excluded) => {
|
|
253
|
+
return models.filter(model => {
|
|
254
|
+
if (!model || typeof model !== 'object' || typeof model.id !== 'string') return false
|
|
255
|
+
return !isModelTrackedInCollection(curated, providerName, model.id) &&
|
|
256
|
+
!isModelTrackedInCollection(excluded, providerName, model.id)
|
|
257
|
+
})
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const searchModels = (models, query) => {
|
|
261
|
+
const q = query.toLowerCase()
|
|
262
|
+
const terms = q.split(/\s+/).filter(Boolean)
|
|
263
|
+
return models.filter(m => {
|
|
264
|
+
const haystack = `${m.id} ${m.label || ''}`.toLowerCase()
|
|
265
|
+
return terms.every(t => haystack.includes(t))
|
|
266
|
+
})
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const processModelsSearchMode = async (providerName, providerInstance, allModels) => {
|
|
270
|
+
const curated = await getCuratedModels()
|
|
271
|
+
const excluded = await getExcludedModels()
|
|
272
|
+
const uncurated = filterUncurated(allModels, providerName, curated, excluded)
|
|
273
|
+
|
|
274
|
+
clack.log.info(`${providerName}: ${allModels.length} models upstream, ${uncurated.length} uncurated`)
|
|
275
|
+
|
|
276
|
+
while (true) {
|
|
277
|
+
const query = await clack.text({
|
|
278
|
+
message: `Search ${providerName} models (or "done" to finish):`,
|
|
279
|
+
placeholder: 'e.g. claude sonnet, llama 70b, gemini flash',
|
|
280
|
+
validate: (v) => v?.trim() ? undefined : 'Type a few characters to search'
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
if (clack.isCancel(query) || query.trim().toLowerCase() === 'done') break
|
|
284
|
+
|
|
285
|
+
const matches = searchModels(uncurated, query.trim())
|
|
286
|
+
|
|
287
|
+
if (matches.length === 0) {
|
|
288
|
+
clack.log.warn(`No uncurated models matching "${query.trim()}"`)
|
|
289
|
+
continue
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (matches.length > SEARCH_MAX_RESULTS) {
|
|
293
|
+
clack.log.warn(`${matches.length} matches — narrow your search (showing first ${SEARCH_MAX_RESULTS})`)
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const shown = matches.slice(0, SEARCH_MAX_RESULTS)
|
|
297
|
+
|
|
298
|
+
const selected = await clack.select({
|
|
299
|
+
message: `${matches.length} match${matches.length > 1 ? 'es' : ''} — select a model to curate:`,
|
|
300
|
+
options: [
|
|
301
|
+
...shown.map(m => ({ value: m.id, label: `${m.id} ${m.label !== m.id ? m.label : ''}`.trim() })),
|
|
302
|
+
{ value: '__refine', label: '← Search again' }
|
|
303
|
+
]
|
|
304
|
+
})
|
|
305
|
+
|
|
306
|
+
if (clack.isCancel(selected)) break
|
|
307
|
+
if (selected === '__refine') continue
|
|
308
|
+
|
|
309
|
+
const model = allModels.find(m => m.id === selected)
|
|
310
|
+
await processOneModel(providerName, providerInstance, model, curated, excluded)
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const processOneModel = async (providerName, providerInstance, model, curated, excluded) => {
|
|
315
|
+
const modelId = model.id
|
|
316
|
+
const modelLabel = model.label || modelId
|
|
317
|
+
const curatedKey = `${providerName}/${modelId}`
|
|
318
|
+
|
|
319
|
+
console.log('\nModel details:')
|
|
320
|
+
console.log(JSON.stringify(model, null, 2))
|
|
321
|
+
|
|
322
|
+
const replacementCandidates = findReplacementCandidates(providerName, modelId, curated)
|
|
323
|
+
|
|
324
|
+
const options = [
|
|
325
|
+
{ value: 'include', label: 'Include in curated models' },
|
|
326
|
+
{ value: 'exclude', label: 'Add to excluded models' },
|
|
327
|
+
{ value: 'skip', label: 'Skip for now' }
|
|
328
|
+
]
|
|
329
|
+
|
|
330
|
+
for (let i = 0; i < replacementCandidates.length; i++) {
|
|
331
|
+
const candidate = replacementCandidates[i]
|
|
332
|
+
const candidateLabel = candidate.label || candidate.modelId
|
|
333
|
+
options.push({
|
|
334
|
+
value: `alias_${i}`,
|
|
335
|
+
label: `Add as alias of: ${candidate.key} (${candidateLabel})`
|
|
336
|
+
})
|
|
337
|
+
options.push({
|
|
338
|
+
value: `replace_${i}`,
|
|
339
|
+
label: `Replace existing model: ${candidate.key} (${candidateLabel})`
|
|
340
|
+
})
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const answer = await clack.select({
|
|
344
|
+
message: `Model ${curatedKey} found. What would you like to do?`,
|
|
345
|
+
options
|
|
346
|
+
})
|
|
347
|
+
|
|
348
|
+
if (clack.isCancel(answer)) return
|
|
349
|
+
|
|
350
|
+
const providerConfig = providers[providerName]
|
|
351
|
+
const baseModelMeta = {
|
|
352
|
+
provider: providerName,
|
|
353
|
+
sdk: providerConfig.sdk,
|
|
354
|
+
model: modelId,
|
|
355
|
+
label: modelLabel
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
let modelInfoWithMeta = null
|
|
359
|
+
if (answer === 'include' || answer.startsWith('replace_')) {
|
|
360
|
+
const s = clack.spinner()
|
|
361
|
+
s.start(`Fetching detailed information for ${curatedKey}...`)
|
|
362
|
+
|
|
363
|
+
const modelInfo = await getModelDetails(providerName, modelId, providerInstance)
|
|
364
|
+
s.stop(modelInfo ? 'Model details retrieved successfully' : 'Could not retrieve detailed model information')
|
|
365
|
+
|
|
366
|
+
modelInfoWithMeta = stripUnknown({
|
|
367
|
+
...baseModelMeta,
|
|
368
|
+
...(modelInfo || {})
|
|
369
|
+
})
|
|
370
|
+
modelInfoWithMeta = await promptMissingFields(modelInfoWithMeta, curatedKey)
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if (answer === 'include') {
|
|
374
|
+
curated[curatedKey] = modelInfoWithMeta || baseModelMeta
|
|
375
|
+
await saveCuratedModels(curated)
|
|
376
|
+
clack.log.success(`Added ${curatedKey} to curated models with detailed information`)
|
|
377
|
+
} else if (answer === 'exclude') {
|
|
378
|
+
excluded[curatedKey] = baseModelMeta
|
|
379
|
+
await saveExcludedModels(excluded)
|
|
380
|
+
clack.log.success(`Added ${curatedKey} to excluded models`)
|
|
381
|
+
} else if (answer.startsWith('alias_')) {
|
|
382
|
+
const index = parseInt(answer.split('_')[1], 10)
|
|
383
|
+
const candidate = replacementCandidates[index]
|
|
384
|
+
const aliasAdded = await addAliasToExistingModel(candidate.key, modelId, curated)
|
|
385
|
+
if (aliasAdded) {
|
|
386
|
+
clack.log.success(`${modelId} added as alias of ${candidate.key}`)
|
|
387
|
+
}
|
|
388
|
+
} else if (answer.startsWith('replace_')) {
|
|
389
|
+
const index = parseInt(answer.split('_')[1], 10)
|
|
390
|
+
const modelToReplace = replacementCandidates[index]
|
|
391
|
+
|
|
392
|
+
await replaceModel(
|
|
393
|
+
modelToReplace,
|
|
394
|
+
curatedKey,
|
|
395
|
+
modelLabel,
|
|
396
|
+
modelInfoWithMeta || baseModelMeta,
|
|
397
|
+
curated,
|
|
398
|
+
excluded
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
clack.log.success(
|
|
402
|
+
`Replaced ${modelToReplace.key} with ${curatedKey}. ` +
|
|
403
|
+
'The old model has been moved to excluded models.'
|
|
404
|
+
)
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const processModelsSelectMode = async (providerName, providerInstance, allModels) => {
|
|
409
|
+
const curated = await getCuratedModels()
|
|
410
|
+
const excluded = await getExcludedModels()
|
|
411
|
+
const uncurated = filterUncurated(allModels, providerName, curated, excluded)
|
|
412
|
+
|
|
413
|
+
if (!uncurated.length) {
|
|
414
|
+
clack.log.info(`${providerName}: no new models`)
|
|
415
|
+
return
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
clack.log.info(`${providerName}: ${uncurated.length} new model${uncurated.length > 1 ? 's' : ''}`)
|
|
419
|
+
|
|
420
|
+
const selected = await clack.select({
|
|
421
|
+
message: 'Select a model to curate (or skip all):',
|
|
422
|
+
options: [
|
|
423
|
+
...uncurated.map(m => ({
|
|
424
|
+
value: m.id,
|
|
425
|
+
label: `${m.id} ${m.label !== m.id ? m.label : ''}`.trim()
|
|
426
|
+
})),
|
|
427
|
+
{ value: '__skip', label: '← Skip all' }
|
|
428
|
+
]
|
|
429
|
+
})
|
|
430
|
+
|
|
431
|
+
if (clack.isCancel(selected) || selected === '__skip') return
|
|
432
|
+
|
|
433
|
+
const model = allModels.find(m => m.id === selected)
|
|
434
|
+
await processOneModel(providerName, providerInstance, model, curated, excluded)
|
|
435
|
+
|
|
436
|
+
// After curating one, recurse to offer the rest
|
|
437
|
+
await processModelsSelectMode(providerName, providerInstance, allModels)
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
export const processModels = async (providerName, providerInstance) => {
|
|
441
|
+
if (!providerInstance.listModels) {
|
|
442
|
+
console.log(`Provider ${providerName} does not support listModels`)
|
|
443
|
+
return
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
try {
|
|
447
|
+
const s = clack.spinner()
|
|
448
|
+
s.start(`Fetching model list from ${providerName}...`)
|
|
449
|
+
|
|
450
|
+
const models = await providerInstance.listModels()
|
|
451
|
+
if (!Array.isArray(models)) {
|
|
452
|
+
s.stop(`${providerName} returned an invalid model list`)
|
|
453
|
+
return
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
const curated = await getCuratedModels()
|
|
457
|
+
const excluded = await getExcludedModels()
|
|
458
|
+
const uncurated = filterUncurated(models, providerName, curated, excluded)
|
|
459
|
+
s.stop(`${providerName}: ${models.length} models upstream, ${uncurated.length} uncurated`)
|
|
460
|
+
|
|
461
|
+
if (!uncurated.length) {
|
|
462
|
+
clack.log.info(`Nothing new to curate. To add a model not in the upstream catalog:\n mo model add ${providerName}/<model-id>`)
|
|
463
|
+
return
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
if (uncurated.length > SEARCH_MODE_THRESHOLD) {
|
|
467
|
+
await processModelsSearchMode(providerName, providerInstance, models)
|
|
468
|
+
} else {
|
|
469
|
+
await processModelsSelectMode(providerName, providerInstance, models)
|
|
470
|
+
}
|
|
471
|
+
} catch (err) {
|
|
472
|
+
console.error(`Error processing models for ${providerName}:`, err.message)
|
|
473
|
+
}
|
|
474
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { trace, context, SpanStatusCode } from '@opentelemetry/api'
|
|
2
|
+
|
|
3
|
+
// Lazy: defer getTracer until first use so the host app can register its TracerProvider first.
|
|
4
|
+
let tracer
|
|
5
|
+
const getTracer = () => (tracer ??= trace.getTracer('mohdel'))
|
|
6
|
+
|
|
7
|
+
export const startSpan = (name, attributes, parentSpan) => {
|
|
8
|
+
const parentCtx = parentSpan
|
|
9
|
+
? trace.setSpan(context.active(), parentSpan)
|
|
10
|
+
: undefined
|
|
11
|
+
return getTracer().startSpan(name, { attributes }, parentCtx)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const endSpanOk = (span, attributes) => {
|
|
15
|
+
if (attributes) span.setAttributes(attributes)
|
|
16
|
+
span.setStatus({ code: SpanStatusCode.OK })
|
|
17
|
+
span.end()
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const endSpanError = (span, err) => {
|
|
21
|
+
span.recordException(err)
|
|
22
|
+
span.setStatus({ code: SpanStatusCode.ERROR, message: err.message })
|
|
23
|
+
span.end()
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// W3C Trace Context — https://www.w3.org/TR/trace-context/#traceparent-header
|
|
27
|
+
// Format: 00-<traceId>-<spanId>-<traceFlags> (lowercase hex). Used by the unix-socket
|
|
28
|
+
// sidecar so trace context can cross the process boundary as a header instead of trying
|
|
29
|
+
// to serialize a live span object (which has circular OTel internals).
|
|
30
|
+
const TRACEPARENT_RE = /^00-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})$/
|
|
31
|
+
const INVALID_TRACE_ID = '00000000000000000000000000000000'
|
|
32
|
+
const INVALID_SPAN_ID = '0000000000000000'
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Parse a W3C `traceparent` header into an OTel SpanContext.
|
|
36
|
+
* Returns null if the header is missing, malformed, or contains invalid all-zero IDs.
|
|
37
|
+
*/
|
|
38
|
+
export const parseTraceparent = (header) => {
|
|
39
|
+
if (!header || typeof header !== 'string') return null
|
|
40
|
+
const m = header.match(TRACEPARENT_RE)
|
|
41
|
+
if (!m) return null
|
|
42
|
+
const [, traceId, spanId, flags] = m
|
|
43
|
+
if (traceId === INVALID_TRACE_ID) return null
|
|
44
|
+
if (spanId === INVALID_SPAN_ID) return null
|
|
45
|
+
return {
|
|
46
|
+
traceId,
|
|
47
|
+
spanId,
|
|
48
|
+
traceFlags: parseInt(flags, 16),
|
|
49
|
+
isRemote: true
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Build a non-recording span from a `traceparent` header so the caller can use it
|
|
55
|
+
* as a `parentSpan` in `startSpan(name, attrs, parentSpan)`. Returns null if the
|
|
56
|
+
* header is missing/invalid.
|
|
57
|
+
*/
|
|
58
|
+
export const remoteParentFromTraceparent = (header) => {
|
|
59
|
+
const spanContext = parseTraceparent(header)
|
|
60
|
+
if (!spanContext) return null
|
|
61
|
+
return trace.wrapSpanContext(spanContext)
|
|
62
|
+
}
|
package/src/lib/utils.js
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
export const sanitizeOutput = str => {
|
|
2
|
+
if (typeof str !== 'string') return str
|
|
3
|
+
return str.replace(/\0/g, '\uFFFD').trim()
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export const translateModelInfo = (model, infoTranslate = {}) => {
|
|
7
|
+
if (!model || typeof model !== 'object') return model
|
|
8
|
+
|
|
9
|
+
const result = { ...model }
|
|
10
|
+
|
|
11
|
+
if (infoTranslate && Object.keys(infoTranslate).length) {
|
|
12
|
+
for (const [source, target] of Object.entries(infoTranslate)) {
|
|
13
|
+
if (source in result) {
|
|
14
|
+
if (typeof target === 'function') {
|
|
15
|
+
const [realtarget, realresult] = target(result[source])
|
|
16
|
+
result[realtarget] = realresult
|
|
17
|
+
} else {
|
|
18
|
+
result[target] = result[source]
|
|
19
|
+
}
|
|
20
|
+
delete result[source]
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const labelKeys = ['display_name', 'displayName']
|
|
26
|
+
for (const key of labelKeys) {
|
|
27
|
+
if (key in result) {
|
|
28
|
+
result.label = result[key]
|
|
29
|
+
delete result[key]
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return result
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const createRealtimeDeltaBuffer = (handler, opts = {}) => {
|
|
37
|
+
const maxChars = opts.maxChars ?? 250
|
|
38
|
+
const maxMs = opts.maxMs ?? 10_000
|
|
39
|
+
let buffer = ''
|
|
40
|
+
let lastType = 'message'
|
|
41
|
+
let lastFlush = Date.now()
|
|
42
|
+
|
|
43
|
+
const flushInternal = force => {
|
|
44
|
+
if (!handler) return
|
|
45
|
+
const now = Date.now()
|
|
46
|
+
const shouldFlush = force || buffer.length >= maxChars || now - lastFlush >= maxMs
|
|
47
|
+
if (shouldFlush && buffer) {
|
|
48
|
+
handler({ type: lastType, delta: buffer })
|
|
49
|
+
buffer = ''
|
|
50
|
+
lastFlush = now
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const push = (type, delta) => {
|
|
55
|
+
if (!handler || !delta) return
|
|
56
|
+
lastType = type || lastType || 'message'
|
|
57
|
+
buffer += delta
|
|
58
|
+
flushInternal(false)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const flush = () => flushInternal(true)
|
|
62
|
+
|
|
63
|
+
return { push, flush }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export const createTimingTracker = () => {
|
|
67
|
+
const start = process.hrtime.bigint()
|
|
68
|
+
let first = null
|
|
69
|
+
|
|
70
|
+
const markFirst = () => {
|
|
71
|
+
if (!first) first = process.hrtime.bigint()
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const timestamps = () => {
|
|
75
|
+
const end = process.hrtime.bigint()
|
|
76
|
+
const firstValue = first || end
|
|
77
|
+
return {
|
|
78
|
+
start: start.toString(),
|
|
79
|
+
first: firstValue.toString(),
|
|
80
|
+
end: end.toString()
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return { markFirst, timestamps }
|
|
85
|
+
}
|