mohdel 0.124.0 → 0.125.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/README.md
CHANGED
|
@@ -233,7 +233,7 @@ With no session-bin configured, thin-gate runs in demo mode: `POST /v1/call` ret
|
|
|
233
233
|
|
|
234
234
|
### Calling from JS
|
|
235
235
|
|
|
236
|
-
The client snippet under [Library Usage](#library-usage) above is the full surface: `call(envelope, { socketPath, signal? })` returns an async iterable of events. Pass an `AbortSignal` to cancel in flight; thin-gate forwards a cancel control message to the session and reuses it on the pool. The envelope is the flat `answer(prompt, options)` surface plus transport metadata (`callId`, `authId`, `auth.key`, optional `traceparent`); see [`js/core/envelope.js`](js/core/envelope.js) for the full field list.
|
|
236
|
+
The client snippet under [Library Usage](#library-usage) above is the full surface: `call(envelope, { socketPath, signal? })` returns an async iterable of events. Pass an `AbortSignal` to cancel in flight; thin-gate forwards a cancel control message to the session and reuses it on the pool, and the client ends the stream with the same cancelled `done` the in-process path returns (status `incomplete`, warning `cancelled`, the partial output it relayed, zero tokens). The envelope is the flat `answer(prompt, options)` surface plus transport metadata (`callId`, `authId`, `auth.key`, optional `traceparent`); see [`js/core/envelope.js`](js/core/envelope.js) for the full field list.
|
|
237
237
|
|
|
238
238
|
### Other languages
|
|
239
239
|
|
package/js/client/call.js
CHANGED
|
@@ -1,17 +1,42 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Send a CallEnvelope to thin-gate; returns an async iterable of Events.
|
|
3
3
|
*
|
|
4
|
-
* Cancellation: pass an AbortSignal. Aborting
|
|
5
|
-
* thin-gate infers cancel from connection close and
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* Cancellation: pass an AbortSignal. Aborting closes the HTTP request;
|
|
5
|
+
* thin-gate infers cancel from connection close and sends the session a
|
|
6
|
+
* cancel control message. The session's own cancelled terminal is drained
|
|
7
|
+
* gate-side, so the client synthesizes the same cancelled `done` here —
|
|
8
|
+
* partial output from the deltas it relayed, zero tokens, zero cost —
|
|
9
|
+
* giving a caller the same terminal on this path as on the in-process one.
|
|
8
10
|
* @module client/call
|
|
9
11
|
*/
|
|
10
12
|
|
|
11
13
|
import { requestUnix } from './transport.js'
|
|
12
14
|
import { readAll, parseErrorBody } from './response.js'
|
|
13
15
|
import { parseNDJSON } from './ndjson.js'
|
|
14
|
-
import { isEvent, MohdelError } from '#core'
|
|
16
|
+
import { isEvent, MohdelError, STATUS_INCOMPLETE, WARNING_CANCELLED } from '#core'
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @param {string} start
|
|
20
|
+
* @param {string | null} first
|
|
21
|
+
* @param {string} output
|
|
22
|
+
* @returns {import('#core/events.js').DoneEvent}
|
|
23
|
+
*/
|
|
24
|
+
function cancelledDone (start, first, output) {
|
|
25
|
+
const end = String(process.hrtime.bigint())
|
|
26
|
+
return {
|
|
27
|
+
type: 'done',
|
|
28
|
+
result: {
|
|
29
|
+
status: STATUS_INCOMPLETE,
|
|
30
|
+
output: output || null,
|
|
31
|
+
inputTokens: 0,
|
|
32
|
+
outputTokens: 0,
|
|
33
|
+
thinkingTokens: 0,
|
|
34
|
+
cost: 0,
|
|
35
|
+
timestamps: { start, first: first ?? end, end },
|
|
36
|
+
warning: WARNING_CANCELLED
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
15
40
|
|
|
16
41
|
/**
|
|
17
42
|
* @param {import('#core/envelope.js').CallEnvelope} envelope
|
|
@@ -22,26 +47,58 @@ import { isEvent, MohdelError } from '#core'
|
|
|
22
47
|
* @returns {AsyncGenerator<import('#core/events.js').Event>}
|
|
23
48
|
*/
|
|
24
49
|
export async function * call (envelope, { socketPath, signal, path = '/v1/call' }) {
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
50
|
+
const start = String(process.hrtime.bigint())
|
|
51
|
+
if (signal?.aborted) {
|
|
52
|
+
yield cancelledDone(start, null, '')
|
|
53
|
+
return
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
let res
|
|
57
|
+
try {
|
|
58
|
+
res = await requestUnix({
|
|
59
|
+
socketPath,
|
|
60
|
+
path,
|
|
61
|
+
method: 'POST',
|
|
62
|
+
body: envelope,
|
|
63
|
+
signal
|
|
64
|
+
})
|
|
65
|
+
} catch (e) {
|
|
66
|
+
if (signal?.aborted) {
|
|
67
|
+
yield cancelledDone(start, null, '')
|
|
68
|
+
return
|
|
69
|
+
}
|
|
70
|
+
throw e
|
|
71
|
+
}
|
|
32
72
|
|
|
33
73
|
if (res.statusCode !== 200) {
|
|
34
74
|
const body = await readAll(res)
|
|
35
75
|
throw MohdelError.fromJSON(parseErrorBody(body, res.statusCode ?? 0))
|
|
36
76
|
}
|
|
37
77
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
)
|
|
78
|
+
const outputParts = []
|
|
79
|
+
let first = null
|
|
80
|
+
let sawTerminal = false
|
|
81
|
+
try {
|
|
82
|
+
for await (const obj of parseNDJSON(res)) {
|
|
83
|
+
if (!isEvent(obj)) {
|
|
84
|
+
throw new MohdelError(
|
|
85
|
+
'received non-Event object from thin-gate',
|
|
86
|
+
{ type: 'PROTOCOL_INVALID_EVENT', retryable: false }
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
if (obj.type === 'delta') {
|
|
90
|
+
if (first === null) first = String(process.hrtime.bigint())
|
|
91
|
+
if (obj.delta?.type === 'message') outputParts.push(obj.delta.delta)
|
|
92
|
+
} else if (obj.type === 'done' || obj.type === 'error') {
|
|
93
|
+
sawTerminal = true
|
|
94
|
+
}
|
|
95
|
+
yield /** @type {import('#core/events.js').Event} */(obj)
|
|
44
96
|
}
|
|
45
|
-
|
|
97
|
+
} catch (e) {
|
|
98
|
+
if (!signal?.aborted) throw e
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (signal?.aborted && !sawTerminal) {
|
|
102
|
+
yield cancelledDone(start, first, outputParts.join(''))
|
|
46
103
|
}
|
|
47
104
|
}
|
|
@@ -2,12 +2,6 @@
|
|
|
2
2
|
* Shared `cancelledDone` helper for adapters that need to synthesize
|
|
3
3
|
* a terminal `done` event on `signal.aborted` mid-stream.
|
|
4
4
|
*
|
|
5
|
-
* Three adapters (openai, anthropic, gemini) had byte-identical
|
|
6
|
-
* copies of this; consolidated here. `_chat_completions.js`
|
|
7
|
-
* and `run.js` have their own cancel paths — don't migrate them
|
|
8
|
-
* here unless you're certain the shape matches (thinkingTokens,
|
|
9
|
-
* cost, tool_calls semantics can all differ).
|
|
10
|
-
*
|
|
11
5
|
* @module session/adapters/_cancelled
|
|
12
6
|
*/
|
|
13
7
|
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
import { getSpec } from './_catalog.js'
|
|
20
20
|
import { classifyProviderError } from './_errors.js'
|
|
21
21
|
import { costFor } from './_pricing.js'
|
|
22
|
+
import { cancelledDone } from './_cancelled.js'
|
|
22
23
|
import { catalogKey, bareOf } from '#core/model-id.js'
|
|
23
24
|
import {
|
|
24
25
|
STATUS_COMPLETED,
|
|
@@ -98,6 +99,10 @@ export async function * runChatCompletions (envelope, client, config, deps = {})
|
|
|
98
99
|
try {
|
|
99
100
|
response = await client.chat.completions.create(args, { signal: deps.signal })
|
|
100
101
|
} catch (e) {
|
|
102
|
+
if (deps.signal?.aborted) {
|
|
103
|
+
yield cancelledDone(start, null, envelope, '', 0, 0)
|
|
104
|
+
return
|
|
105
|
+
}
|
|
101
106
|
deps.log?.warn({ err: e }, `[mohdel:${config.provider}] request failed`)
|
|
102
107
|
yield { type: 'error', error: classifyProviderError(e, envelope.auth?.key, { provider: config.provider }) }
|
|
103
108
|
return
|
|
@@ -161,6 +166,10 @@ async function * runStreaming (envelope, client, args, config, start, deps) {
|
|
|
161
166
|
try {
|
|
162
167
|
stream = await client.chat.completions.create(args, { signal: deps.signal })
|
|
163
168
|
} catch (e) {
|
|
169
|
+
if (deps.signal?.aborted) {
|
|
170
|
+
yield cancelledDone(start, null, envelope, '', 0, 0)
|
|
171
|
+
return
|
|
172
|
+
}
|
|
164
173
|
deps.log?.warn({ err: e }, `[mohdel:${config.provider}] request failed`)
|
|
165
174
|
yield { type: 'error', error: classifyProviderError(e, envelope.auth?.key, { provider: config.provider }) }
|
|
166
175
|
return
|
|
@@ -168,6 +177,10 @@ async function * runStreaming (envelope, client, args, config, start, deps) {
|
|
|
168
177
|
|
|
169
178
|
try {
|
|
170
179
|
for await (const chunk of stream) {
|
|
180
|
+
if (deps.signal?.aborted) {
|
|
181
|
+
yield cancelledDone(start, first, envelope, contentParts.join(''), 0, 0)
|
|
182
|
+
return
|
|
183
|
+
}
|
|
171
184
|
const choice = chunk.choices?.[0]
|
|
172
185
|
// DeepSeek V4 / deepseek-reasoner / Cerebras reasoning models emit
|
|
173
186
|
// `delta.reasoning_content` chunks before visible content. Capture
|
|
@@ -225,11 +238,20 @@ async function * runStreaming (envelope, client, args, config, start, deps) {
|
|
|
225
238
|
if (chunk.usage) usage = chunk.usage
|
|
226
239
|
}
|
|
227
240
|
} catch (e) {
|
|
241
|
+
if (deps.signal?.aborted) {
|
|
242
|
+
yield cancelledDone(start, first, envelope, contentParts.join(''), 0, 0)
|
|
243
|
+
return
|
|
244
|
+
}
|
|
228
245
|
deps.log?.warn({ err: e }, `[mohdel:${config.provider}] stream failed`)
|
|
229
246
|
yield { type: 'error', error: classifyProviderError(e, envelope.auth?.key, { provider: config.provider }) }
|
|
230
247
|
return
|
|
231
248
|
}
|
|
232
249
|
|
|
250
|
+
if (deps.signal?.aborted) {
|
|
251
|
+
yield cancelledDone(start, first, envelope, contentParts.join(''), 0, 0)
|
|
252
|
+
return
|
|
253
|
+
}
|
|
254
|
+
|
|
233
255
|
const collectedToolCalls = Object.values(toolCallAccum).map(tc => ({
|
|
234
256
|
id: tc.id,
|
|
235
257
|
function: { name: tc.name, arguments: tc.arguments }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mohdel",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.125.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Christophe Le Bars",
|
|
@@ -108,12 +108,12 @@
|
|
|
108
108
|
"@opentelemetry/exporter-trace-otlp-grpc": "^0.222.0",
|
|
109
109
|
"@opentelemetry/sdk-node": "^0.222.0",
|
|
110
110
|
"chalk": "^6.0.0",
|
|
111
|
-
"mohdel-thin-gate-linux-x64-gnu": "0.
|
|
111
|
+
"mohdel-thin-gate-linux-x64-gnu": "0.125.0"
|
|
112
112
|
},
|
|
113
113
|
"dependencies": {
|
|
114
114
|
"@anthropic-ai/sdk": "^0.123.0",
|
|
115
115
|
"@cerebras/cerebras_cloud_sdk": "^1.91.0",
|
|
116
|
-
"@google/genai": "^2.
|
|
116
|
+
"@google/genai": "^2.21.0",
|
|
117
117
|
"@opentelemetry/api": "^1.9.1",
|
|
118
118
|
"env-paths": "^4.0.0",
|
|
119
119
|
"groq-sdk": "^1.6.0",
|