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,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
+ }
@@ -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
+ }