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,60 @@
1
+ /**
2
+ * Image URI loader.
3
+ *
4
+ * Three URI schemes are supported (see INTEGRATION.md §Vision):
5
+ * - `file://` → reads from local filesystem, base64-encodes
6
+ * - `https://` → passed as URL reference (where the provider accepts it)
7
+ * - `data:` → base64 data URI parsed inline
8
+ *
9
+ * Each adapter calls `loadImages(images)` to get a normalized
10
+ * intermediate shape `{mimeType, base64?, url?}`, then formats per
11
+ * provider. Errors are surfaced — file IO failures bubble up so the
12
+ * adapter can emit a typed error rather than silently skipping.
13
+ *
14
+ * @module session/adapters/_images
15
+ */
16
+
17
+ import { readFile } from 'node:fs/promises'
18
+
19
+ /**
20
+ * @typedef {object} LoadedImage
21
+ * @property {string} mimeType
22
+ * @property {string} [base64] Raw base64 (no `data:` prefix)
23
+ * @property {string} [url] Remote URL (only when source was https://)
24
+ */
25
+
26
+ /**
27
+ * @param {Array<{fileUri: string, mimeType: string}>} images
28
+ * @returns {Promise<LoadedImage[]>}
29
+ */
30
+ export async function loadImages (images) {
31
+ if (!images || !Array.isArray(images)) return []
32
+ const out = []
33
+ for (const img of images) {
34
+ if (!img?.fileUri || !img?.mimeType) continue
35
+ out.push(await loadImage(img))
36
+ }
37
+ return out
38
+ }
39
+
40
+ /**
41
+ * @param {{fileUri: string, mimeType: string}} image
42
+ * @returns {Promise<LoadedImage>}
43
+ */
44
+ export async function loadImage (image) {
45
+ const { fileUri, mimeType } = image
46
+ if (fileUri.startsWith('file://')) {
47
+ const path = fileUri.replace(/^file:\/\//, '')
48
+ const buf = await readFile(path)
49
+ return { mimeType, base64: buf.toString('base64') }
50
+ }
51
+ if (fileUri.startsWith('data:')) {
52
+ const parts = fileUri.split(',')
53
+ if (parts.length > 1) return { mimeType, base64: parts[1] }
54
+ throw new Error(`malformed data URI: ${fileUri.slice(0, 32)}…`)
55
+ }
56
+ if (fileUri.startsWith('https://') || fileUri.startsWith('http://')) {
57
+ return { mimeType, url: fileUri }
58
+ }
59
+ throw new Error(`unsupported image URI scheme: ${fileUri.slice(0, 32)}…`)
60
+ }
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Shared lazy-load-once JSON cache for config files under
3
+ * `~/.config/mohdel/`. `_catalog.js` and `_providers.js` both had
4
+ * byte-similar implementations before F62; this helper owns the
5
+ * pattern.
6
+ *
7
+ * Contract:
8
+ * - `loadSync(path?)` — synchronous read; used as the lazy
9
+ * fallback inside `get()` and by tests that want to parse an
10
+ * arbitrary file without touching the shared cache.
11
+ * - `initAsync()` — idempotent eager init from the default path.
12
+ * Called from `bin.js::main` before `drive()` so the first
13
+ * `get()` doesn't stall the event loop on a sync read.
14
+ * - `set(table)` — replace the in-memory table (tests + extension
15
+ * hook for deployments that source config from elsewhere).
16
+ * - `get(key)` — read-through; loads synchronously on first miss.
17
+ *
18
+ * A malformed / missing / non-object file resolves to the supplied
19
+ * `defaultValue` (default `{}`) so callers never have to handle
20
+ * file-absence explicitly.
21
+ *
22
+ * @module session/adapters/_lazy_json_cache
23
+ */
24
+
25
+ import fs from 'node:fs'
26
+
27
+ /**
28
+ * @template V
29
+ * @param {() => string} pathFn Resolves the default file path.
30
+ * @param {{defaultValue?: V}} [opts]
31
+ */
32
+ export function createLazyJsonFileCache (pathFn, { defaultValue = /** @type {any} */({}) } = {}) {
33
+ /** @type {V | null} */
34
+ let active = null
35
+
36
+ /** @param {unknown} parsed */
37
+ function normalize (parsed) {
38
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
39
+ return defaultValue
40
+ }
41
+ return /** @type {V} */(parsed)
42
+ }
43
+
44
+ /** @param {string} [p] */
45
+ function loadSync (p) {
46
+ const file = p ?? pathFn()
47
+ try {
48
+ return normalize(JSON.parse(fs.readFileSync(file, 'utf8')))
49
+ } catch {
50
+ return defaultValue
51
+ }
52
+ }
53
+
54
+ async function initAsync () {
55
+ if (active !== null) return
56
+ try {
57
+ const text = await fs.promises.readFile(pathFn(), 'utf8')
58
+ active = normalize(JSON.parse(text))
59
+ } catch {
60
+ active = defaultValue
61
+ }
62
+ }
63
+
64
+ /** @param {V} table */
65
+ function set (table) {
66
+ active = /** @type {V} */({ ...table })
67
+ }
68
+
69
+ /** @param {string} key */
70
+ function get (key) {
71
+ if (active === null) active = loadSync()
72
+ return active[key]
73
+ }
74
+
75
+ return { loadSync, initAsync, set, get }
76
+ }
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Cost computation from curated catalog spec.
3
+ *
4
+ * Reads `inputPrice` / `outputPrice` / `thinkingPrice` from the
5
+ * catalog spec (loaded via `_catalog.js`). Returns a single number
6
+ * (USD) written to `AnswerResult.cost`. Unknown models or specs
7
+ * without prices return `0` — graceful degradation.
8
+ *
9
+ * @module session/adapters/_pricing
10
+ */
11
+
12
+ import { getSpec, setCatalog } from './_catalog.js'
13
+
14
+ /**
15
+ * Pure cost computation from spec + usage.
16
+ *
17
+ * @param {any} spec Catalog entry (with `inputPrice`/`outputPrice`/`thinkingPrice`),
18
+ * or `undefined`.
19
+ * @param {{inputTokens?: number, outputTokens?: number, thinkingTokens?: number}} usage
20
+ * @returns {number}
21
+ */
22
+ export function computeCost (spec, usage) {
23
+ if (!spec) return 0
24
+ const ip = spec.inputPrice
25
+ const op = spec.outputPrice
26
+ if (typeof ip !== 'number' || typeof op !== 'number') return 0
27
+ const i = usage.inputTokens ?? 0
28
+ const o = usage.outputTokens ?? 0
29
+ const t = usage.thinkingTokens ?? 0
30
+ const tp = typeof spec.thinkingPrice === 'number' ? spec.thinkingPrice : op
31
+ const total = (i * ip + o * op + t * tp) / 1_000_000
32
+ return round(total)
33
+ }
34
+
35
+ /**
36
+ * @param {string} model Fully-qualified `<provider>/<model>`.
37
+ * @param {{inputTokens?: number, outputTokens?: number, thinkingTokens?: number}} usage
38
+ * @returns {number}
39
+ */
40
+ export function costFor (model, usage) {
41
+ return computeCost(getSpec(model), usage)
42
+ }
43
+
44
+ /**
45
+ * Test convenience: inject pricing-only specs by model id. Wraps
46
+ * `setCatalog` with the `{input, output, thinking?}` shape used in
47
+ * existing tests, translating to spec fields.
48
+ *
49
+ * @param {Record<string, {input: number, output: number, thinking?: number}>} table
50
+ */
51
+ export function setPricing (table) {
52
+ /** @type {Record<string, any>} */
53
+ const wrapped = {}
54
+ for (const [k, v] of Object.entries(table)) {
55
+ wrapped[k] = {
56
+ inputPrice: v.input,
57
+ outputPrice: v.output,
58
+ ...(v.thinking != null && { thinkingPrice: v.thinking })
59
+ }
60
+ }
61
+ setCatalog(wrapped)
62
+ }
63
+
64
+ /** @param {number} n */
65
+ function round (n) {
66
+ return Math.round(n * 1e6) / 1e6
67
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Provider-level configuration reader.
3
+ *
4
+ * Per-provider rate limits live in `~/.config/mohdel/providers.json`
5
+ * as `{ <provider>: { rpmLimit, tpmLimit } }`. These are
6
+ * per-account values (different plans get different limits), so
7
+ * they live in user config — not source code.
8
+ *
9
+ * Sessions load once and cache in-process.
10
+ *
11
+ * @module session/adapters/_providers
12
+ */
13
+
14
+ import envPaths from 'env-paths'
15
+
16
+ import { createLazyJsonFileCache } from './_lazy_json_cache.js'
17
+
18
+ /**
19
+ * @typedef {object} ProviderLimits
20
+ * @property {number} [rpmLimit]
21
+ * @property {number} [tpmLimit]
22
+ */
23
+
24
+ const cache = createLazyJsonFileCache(
25
+ // `{ suffix: null }` — see `_catalog.js` for the rationale (stay
26
+ // in lockstep with the CLI's `CONFIG_DIR`, avoid the `-nodejs`
27
+ // suffix env-paths appends by default).
28
+ () => `${envPaths('mohdel', { suffix: null }).config}/providers.json`
29
+ )
30
+
31
+ /**
32
+ * @param {string} path
33
+ * @returns {Record<string, ProviderLimits>}
34
+ */
35
+ export function loadProviders (path) {
36
+ return cache.loadSync(path)
37
+ }
38
+
39
+ /**
40
+ * Eager async initialization from the default providers path. Called
41
+ * from `bin.js::main` before `drive()` so `getProviderLimits` doesn't
42
+ * stall the event loop on a sync read mid-call. Idempotent; respects
43
+ * a prior `setProviders` (tests).
44
+ */
45
+ export async function initProvidersFromDefault () {
46
+ await cache.initAsync()
47
+ }
48
+
49
+ /** @param {Record<string, ProviderLimits>} table */
50
+ export function setProviders (table) {
51
+ cache.set(table)
52
+ }
53
+
54
+ /**
55
+ * @param {string} provider
56
+ * @returns {ProviderLimits | undefined}
57
+ */
58
+ export function getProviderLimits (provider) {
59
+ return cache.get(provider)
60
+ }
@@ -0,0 +1,185 @@
1
+ /**
2
+ * Tool format conversion — provider-agnostic JSON-shape converters
3
+ * used by adapters to translate between the unified `ToolSpec` /
4
+ * `ToolCall` envelope shapes and each provider's native wire format.
5
+ *
6
+ * @module session/adapters/_tools
7
+ */
8
+
9
+ const argsObj = (args) => args || {}
10
+
11
+ // Tool argument parse failures are expected (models routinely send
12
+ // malformed JSON before retrying with corrections). Fall back to
13
+ // returning the raw string — downstream adapter code handles the
14
+ // type mismatch. Logging here would create warn-level noise.
15
+ const parseArgs = (_name, args) => {
16
+ if (typeof args !== 'string') return argsObj(args)
17
+ try {
18
+ return JSON.parse(args)
19
+ } catch {
20
+ return args
21
+ }
22
+ }
23
+
24
+ /**
25
+ * Convert unified tool format to Anthropic's native format.
26
+ * @param {Array} tools Array of unified tool definitions.
27
+ * @returns {Array} Anthropic-formatted tools.
28
+ */
29
+ export const toAnthropicTools = (tools) => tools.map(t => ({
30
+ name: t.name,
31
+ description: t.description,
32
+ input_schema: t.parameters
33
+ }))
34
+
35
+ /**
36
+ * Convert unified tool format to OpenAI's native format.
37
+ * @param {Array} tools Array of unified tool definitions.
38
+ * @returns {Array} OpenAI-formatted tools.
39
+ */
40
+ export const toOpenAITools = (tools) => tools.map(t => ({
41
+ type: 'function',
42
+ name: t.name,
43
+ description: t.description,
44
+ parameters: t.parameters
45
+ }))
46
+
47
+ /**
48
+ * Convert unified tool format to Cerebras's native format (classic
49
+ * OpenAI chat completions).
50
+ * @param {Array} tools Array of unified tool definitions.
51
+ * @returns {Array} Cerebras-formatted tools.
52
+ */
53
+ export const toCerebrasTools = (tools) => tools.map(t => ({
54
+ type: 'function',
55
+ function: {
56
+ name: t.name,
57
+ description: t.description,
58
+ parameters: t.parameters
59
+ }
60
+ }))
61
+
62
+ /**
63
+ * Convert unified tool format to Gemini's native format (wrapped in
64
+ * `functionDeclarations`).
65
+ * @param {Array} tools Array of unified tool definitions.
66
+ * @returns {Array} Gemini-formatted tools.
67
+ */
68
+ export const toGeminiTools = (tools) => [{
69
+ functionDeclarations: tools.map(t => ({
70
+ name: t.name,
71
+ description: t.description,
72
+ parameters: t.parameters
73
+ }))
74
+ }]
75
+
76
+ /**
77
+ * Normalize Anthropic tool_use blocks to unified format.
78
+ * @param {Array} blocks Array of `tool_use` content blocks.
79
+ * @returns {Array} Unified toolCalls format.
80
+ */
81
+ export const fromAnthropicToolCalls = (blocks) => blocks.map(block => ({
82
+ id: block.id,
83
+ name: block.name,
84
+ arguments: block.input
85
+ }))
86
+
87
+ /**
88
+ * Normalize OpenAI function calls to unified format.
89
+ * @param {Array} calls Array of OpenAI function call outputs.
90
+ * @returns {Array} Unified toolCalls format.
91
+ */
92
+ export const fromOpenAIToolCalls = (calls) => calls.map(call => ({
93
+ id: call.call_id || call.id,
94
+ name: call.name,
95
+ arguments: parseArgs(call.name, call.arguments)
96
+ }))
97
+
98
+ /**
99
+ * Normalize Cerebras tool calls to unified format. Cerebras uses
100
+ * the classic OpenAI chat-completions shape `{id, function: {name,
101
+ * arguments}}`.
102
+ * @param {Array} calls Array of Cerebras tool_calls.
103
+ * @returns {Array} Unified toolCalls format.
104
+ */
105
+ export const fromCerebrasToolCalls = (calls) => calls.map(call => ({
106
+ id: call.id,
107
+ name: call.function.name,
108
+ arguments: parseArgs(call.function.name, call.function.arguments)
109
+ }))
110
+
111
+ /**
112
+ * Normalize Gemini function calls to unified format. Gemini doesn't
113
+ * provide IDs, so we generate them.
114
+ * @param {Array} calls Array of Gemini `functionCall` parts.
115
+ * @returns {Array} Unified toolCalls format.
116
+ */
117
+ export const fromGeminiToolCalls = (calls) => calls.map((call, index) => {
118
+ const tc = {
119
+ id: `gemini_call_${Date.now()}_${index}_${Math.random().toString(36).slice(2, 6)}`,
120
+ name: call.name,
121
+ arguments: call.args || {}
122
+ }
123
+ if (call.thoughtSignature) tc.thoughtSignature = call.thoughtSignature
124
+ return tc
125
+ })
126
+
127
+ /**
128
+ * Convert tool choice to provider-specific format.
129
+ * @param {string} provider Provider name (`'anthropic'`, `'openai'`,
130
+ * `'cerebras'`, `'mistral'`, `'gemini'`).
131
+ * @param {string|object} choice Tool choice (`'auto'`, `'required'`,
132
+ * `'none'`, or a specific tool name).
133
+ * @returns {any} Provider-formatted tool choice.
134
+ */
135
+ export const toToolChoice = (provider, choice) => {
136
+ if (!choice) return undefined
137
+
138
+ switch (provider) {
139
+ case 'anthropic':
140
+ if (choice === 'auto') return { type: 'auto' }
141
+ if (choice === 'required') return { type: 'any' }
142
+ if (choice === 'none') return { type: 'none' }
143
+ if (typeof choice === 'string') return { type: 'tool', name: choice }
144
+ return choice
145
+
146
+ case 'openai':
147
+ if (choice === 'auto') return 'auto'
148
+ if (choice === 'required') return 'required'
149
+ if (choice === 'none') return 'none'
150
+ if (typeof choice === 'string') return { type: 'function', function: { name: choice } }
151
+ return choice
152
+
153
+ case 'cerebras':
154
+ if (choice === 'auto') return 'auto'
155
+ if (choice === 'required') return 'required'
156
+ if (choice === 'none') return 'none'
157
+ if (typeof choice === 'string') return { type: 'function', function: { name: choice } }
158
+ return choice
159
+
160
+ case 'mistral':
161
+ if (choice === 'auto') return 'auto'
162
+ if (choice === 'required') return 'any'
163
+ if (choice === 'none') return 'none'
164
+ if (typeof choice === 'string') return { type: 'function', function: { name: choice } }
165
+ return choice
166
+
167
+ case 'gemini':
168
+ // Gemini uses toolConfig.functionCallingConfig
169
+ if (choice === 'auto') return { functionCallingConfig: { mode: 'AUTO' } }
170
+ if (choice === 'required') return { functionCallingConfig: { mode: 'ANY' } }
171
+ if (choice === 'none') return { functionCallingConfig: { mode: 'NONE' } }
172
+ if (typeof choice === 'string') {
173
+ return {
174
+ functionCallingConfig: {
175
+ mode: 'ANY',
176
+ allowedFunctionNames: [choice]
177
+ }
178
+ }
179
+ }
180
+ return choice
181
+
182
+ default:
183
+ return choice
184
+ }
185
+ }