mohdel 1.4.0 → 1.5.1
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/js/client/coalesce.js +42 -0
- package/js/client/index.js +1 -0
- package/js/session/driver.js +8 -0
- package/js/session/run.js +37 -3
- package/js/session/run_info.js +58 -0
- package/package.json +5 -5
- package/src/lib/utils.js +3 -1
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Coalesce the delta events of a `call()` stream, with the facade's
|
|
3
|
+
* `bufferOpts` rules (`createRealtimeDeltaBuffer`): deltas of one kind
|
|
4
|
+
* accumulate until `maxChars` is reached or `maxMs` has passed since the
|
|
5
|
+
* last flush, checked as each delta arrives — there is no timer. A change
|
|
6
|
+
* of kind, any other event, and the end of the stream flush first, so
|
|
7
|
+
* order is kept and the terminal event is never held back.
|
|
8
|
+
*
|
|
9
|
+
* @module client/coalesce
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @param {AsyncIterable<import('#core/events.js').Event>} events
|
|
14
|
+
* @param {{maxChars?: number, maxMs?: number}} [bufferOpts]
|
|
15
|
+
* @returns {AsyncGenerator<import('#core/events.js').Event>}
|
|
16
|
+
*/
|
|
17
|
+
export async function * coalesce (events, { maxChars = 250, maxMs = 10_000 } = {}) {
|
|
18
|
+
let buffer = ''
|
|
19
|
+
let kind = 'message'
|
|
20
|
+
let lastFlush = Date.now()
|
|
21
|
+
|
|
22
|
+
const flush = () => {
|
|
23
|
+
const ev = { type: 'delta', delta: { type: kind, delta: buffer } }
|
|
24
|
+
buffer = ''
|
|
25
|
+
lastFlush = Date.now()
|
|
26
|
+
return ev
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
for await (const ev of events) {
|
|
30
|
+
if (ev.type !== 'delta') {
|
|
31
|
+
if (buffer) yield flush()
|
|
32
|
+
yield ev
|
|
33
|
+
continue
|
|
34
|
+
}
|
|
35
|
+
if (!ev.delta.delta) continue
|
|
36
|
+
if (buffer && ev.delta.type !== kind) yield flush()
|
|
37
|
+
kind = ev.delta.type
|
|
38
|
+
buffer += ev.delta.delta
|
|
39
|
+
if (buffer.length >= maxChars || Date.now() - lastFlush >= maxMs) yield flush()
|
|
40
|
+
}
|
|
41
|
+
if (buffer) yield flush()
|
|
42
|
+
}
|
package/js/client/index.js
CHANGED
package/js/session/driver.js
CHANGED
|
@@ -19,6 +19,7 @@ import { run } from './run.js'
|
|
|
19
19
|
import { runImage } from './run_image.js'
|
|
20
20
|
import { runTranscription } from './run_transcription.js'
|
|
21
21
|
import { runEmbedding } from './run_embedding.js'
|
|
22
|
+
import { runInfo } from './run_info.js'
|
|
22
23
|
import { setCatalog } from './adapters/_catalog.js'
|
|
23
24
|
|
|
24
25
|
// Bounded memory for pre-dequeue cancels. Hostile/buggy supervisors
|
|
@@ -232,6 +233,13 @@ export async function drive (stdin, stdout) {
|
|
|
232
233
|
} else {
|
|
233
234
|
await writeLine({ type: 'error', error: out.error })
|
|
234
235
|
}
|
|
236
|
+
} else if (envelope.op === 'info') {
|
|
237
|
+
const out = await runInfo(envelope)
|
|
238
|
+
if (out.ok) {
|
|
239
|
+
await writeLine({ type: 'info_done', result: out.result })
|
|
240
|
+
} else {
|
|
241
|
+
await writeLine({ type: 'error', error: out.error })
|
|
242
|
+
}
|
|
235
243
|
} else {
|
|
236
244
|
for await (const ev of run(envelope, { signal: controller.signal })) {
|
|
237
245
|
await writeLine(ev)
|
package/js/session/run.js
CHANGED
|
@@ -61,7 +61,7 @@ import { STATUS_INCOMPLETE, WARNING_CANCELLED } from '#core/status.js'
|
|
|
61
61
|
// name is checked against the known list before it reaches the import path —
|
|
62
62
|
// it comes off the envelope, and a computed specifier must never take an
|
|
63
63
|
// arbitrary string.
|
|
64
|
-
const loadAdapter = async (provider) => {
|
|
64
|
+
export const loadAdapter = async (provider) => {
|
|
65
65
|
if (!ADAPTER_NAMES.includes(provider)) throw new Error(`unknown provider: ${provider}`)
|
|
66
66
|
const module = await import(`./adapters/${provider}.js`)
|
|
67
67
|
return module[provider]
|
|
@@ -85,6 +85,9 @@ export async function * run (envelope, {
|
|
|
85
85
|
if (norm.error) { yield norm.error; return }
|
|
86
86
|
envelope = norm.envelope
|
|
87
87
|
|
|
88
|
+
const cacheErr = cacheMarkerError(envelope.prompt)
|
|
89
|
+
if (cacheErr) { yield cacheErr; return }
|
|
90
|
+
|
|
88
91
|
const provider = providerOf(envelope.model)
|
|
89
92
|
const span = openSpan(envelope)
|
|
90
93
|
const log = scopedLogger(logger, envelope, span)
|
|
@@ -312,7 +315,7 @@ export async function * run (envelope, {
|
|
|
312
315
|
* error?: import('#core/events.js').ErrorEvent
|
|
313
316
|
* }}
|
|
314
317
|
*/
|
|
315
|
-
function normalizeModelId (envelope, resolveSpec) {
|
|
318
|
+
export function normalizeModelId (envelope, resolveSpec) {
|
|
316
319
|
// A bare id that itself contains `:` or `@` is a catalog key in its
|
|
317
320
|
// own right; resolving the whole string first stops it being split
|
|
318
321
|
// into a base plus a suffix that was never meant as one.
|
|
@@ -357,6 +360,37 @@ function normalizeModelId (envelope, resolveSpec) {
|
|
|
357
360
|
return { envelope: next, key: base, spec: baseSpec }
|
|
358
361
|
}
|
|
359
362
|
|
|
363
|
+
/**
|
|
364
|
+
* A `cache` marker is honoured on `text` parts only, with a TTL the
|
|
365
|
+
* adapters know. The adapters would ignore any other marker, so it is
|
|
366
|
+
* refused rather than dropped.
|
|
367
|
+
*
|
|
368
|
+
* @param {import('#core/envelope.js').CallEnvelope['prompt']} prompt
|
|
369
|
+
* @returns {import('#core/events.js').ErrorEvent | undefined}
|
|
370
|
+
*/
|
|
371
|
+
function cacheMarkerError (prompt) {
|
|
372
|
+
if (!Array.isArray(prompt)) return undefined
|
|
373
|
+
for (const m of prompt) {
|
|
374
|
+
if (!Array.isArray(m?.content)) continue
|
|
375
|
+
for (const p of m.content) {
|
|
376
|
+
if (p?.cache == null) continue
|
|
377
|
+
if (p.type !== 'text') {
|
|
378
|
+
return errorEvent(
|
|
379
|
+
`\`cache\` is accepted on text parts only, not on a ${p.type} part (${m.role} message)`,
|
|
380
|
+
'SESSION_INVALID_PROMPT'
|
|
381
|
+
)
|
|
382
|
+
}
|
|
383
|
+
if (p.cache !== '5m' && p.cache !== '1h') {
|
|
384
|
+
return errorEvent(
|
|
385
|
+
`\`cache\` on a text part must be '5m' or '1h' (got ${JSON.stringify(p.cache)})`,
|
|
386
|
+
'SESSION_INVALID_PROMPT'
|
|
387
|
+
)
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
return undefined
|
|
392
|
+
}
|
|
393
|
+
|
|
360
394
|
/**
|
|
361
395
|
* The two lane guards, in caller-then-internal order. Returns an
|
|
362
396
|
* error event, or `undefined` when the lane is usable.
|
|
@@ -374,7 +408,7 @@ function normalizeModelId (envelope, resolveSpec) {
|
|
|
374
408
|
* @param {any} adapter
|
|
375
409
|
* @returns {import('#core/events.js').ErrorEvent | undefined}
|
|
376
410
|
*/
|
|
377
|
-
function speedError (key, speed, spec, provider, adapter) {
|
|
411
|
+
export function speedError (key, speed, spec, provider, adapter) {
|
|
378
412
|
if (!hasSpeed(spec, speed)) {
|
|
379
413
|
const available = speedNames(spec)
|
|
380
414
|
const detail = available.length
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Catalog entry lookup for a model id, answered by the session that
|
|
3
|
+
* holds the catalog. An embedder of the gate supplies or loads the
|
|
4
|
+
* catalog but never reads it back; this is how it asks.
|
|
5
|
+
*
|
|
6
|
+
* The answer is the entry a call with that `model` would run on: the
|
|
7
|
+
* same lookup and the same effort and speed-lane checks as `run.js`,
|
|
8
|
+
* so an id this accepts is one a call accepts. Shape follows the
|
|
9
|
+
* facade's `info()`: the entry, plus `speed` when a lane is named.
|
|
10
|
+
*
|
|
11
|
+
* @module session/run_info
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { getSpec } from './adapters/_catalog.js'
|
|
15
|
+
import { providerOf } from '#core/model-id.js'
|
|
16
|
+
import { loadAdapter, normalizeModelId, speedError } from './run.js'
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @param {{model: string}} request
|
|
20
|
+
* @param {{
|
|
21
|
+
* resolveSpec?: (key: string) => any,
|
|
22
|
+
* resolveAdapter?: (provider: string) => Promise<any>
|
|
23
|
+
* }} [options]
|
|
24
|
+
* @returns {Promise<
|
|
25
|
+
* | {ok: true, result: Record<string, any> | null}
|
|
26
|
+
* | {ok: false, error: import('#core/errors.js').TypedError}
|
|
27
|
+
* >}
|
|
28
|
+
*/
|
|
29
|
+
export async function runInfo ({ model }, {
|
|
30
|
+
resolveSpec = getSpec,
|
|
31
|
+
resolveAdapter = loadAdapter
|
|
32
|
+
} = {}) {
|
|
33
|
+
const norm = normalizeModelId(/** @type {any} */({ model }), resolveSpec)
|
|
34
|
+
if (norm.error) return { ok: false, error: norm.error.error }
|
|
35
|
+
if (!norm.spec) return { ok: true, result: null }
|
|
36
|
+
|
|
37
|
+
const speed = norm.envelope.speed
|
|
38
|
+
if (!speed) return { ok: true, result: { ...norm.spec } }
|
|
39
|
+
|
|
40
|
+
const provider = providerOf(norm.key)
|
|
41
|
+
let adapter
|
|
42
|
+
try {
|
|
43
|
+
adapter = await resolveAdapter(provider)
|
|
44
|
+
} catch (e) {
|
|
45
|
+
return {
|
|
46
|
+
ok: false,
|
|
47
|
+
error: {
|
|
48
|
+
message: e instanceof Error ? e.message : String(e),
|
|
49
|
+
severity: 'error',
|
|
50
|
+
retryable: false,
|
|
51
|
+
type: 'SESSION_UNKNOWN_PROVIDER'
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const denied = speedError(norm.key, speed, norm.spec, provider, adapter)
|
|
56
|
+
if (denied) return { ok: false, error: denied.error }
|
|
57
|
+
return { ok: true, result: { ...norm.spec, speed } }
|
|
58
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mohdel",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.1",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Christophe Le Bars",
|
|
@@ -136,17 +136,17 @@
|
|
|
136
136
|
"@opentelemetry/exporter-trace-otlp-grpc": "^0.222.0",
|
|
137
137
|
"@opentelemetry/sdk-node": "^0.222.0",
|
|
138
138
|
"chalk": "^6.0.0",
|
|
139
|
-
"mohdel-thin-gate-linux-x64-gnu": "1.
|
|
139
|
+
"mohdel-thin-gate-linux-x64-gnu": "1.5.1"
|
|
140
140
|
},
|
|
141
141
|
"dependencies": {
|
|
142
|
-
"@anthropic-ai/sdk": "^0.
|
|
142
|
+
"@anthropic-ai/sdk": "^0.128.0",
|
|
143
143
|
"@cerebras/cerebras_cloud_sdk": "^1.91.0",
|
|
144
144
|
"@clack/prompts": "^1.8.1",
|
|
145
|
-
"@google/genai": "^2.
|
|
145
|
+
"@google/genai": "^2.24.0",
|
|
146
146
|
"@opentelemetry/api": "^1.9.1",
|
|
147
147
|
"env-paths": "^4.0.0",
|
|
148
148
|
"groq-sdk": "^1.6.0",
|
|
149
|
-
"openai": "^7.
|
|
149
|
+
"openai": "^7.23.0",
|
|
150
150
|
"undici": "^7.29.0"
|
|
151
151
|
},
|
|
152
152
|
"lint-staged": {
|
package/src/lib/utils.js
CHANGED
|
@@ -64,7 +64,9 @@ export const createRealtimeDeltaBuffer = (handler, opts = {}) => {
|
|
|
64
64
|
|
|
65
65
|
const push = (type, delta) => {
|
|
66
66
|
if (!handler || !delta) return
|
|
67
|
-
|
|
67
|
+
const kind = type || lastType
|
|
68
|
+
if (buffer && kind !== lastType) flushInternal(true)
|
|
69
|
+
lastType = kind
|
|
68
70
|
buffer += delta
|
|
69
71
|
flushInternal(false)
|
|
70
72
|
}
|