mohdel 3.8.0 → 3.9.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.
@@ -122,6 +122,13 @@
122
122
  * @property {('text'|'reasoning'|'image')} type
123
123
  * @property {string} [text]
124
124
  * Required on `text` and `reasoning` parts.
125
+ * @property {string} [encrypted]
126
+ * On a `reasoning` part: the provider's opaque reasoning, replayed only
127
+ * to `model` and dropped for any other.
128
+ * @property {string} [id]
129
+ * On a `reasoning` part carrying `encrypted`: the provider's item id.
130
+ * @property {string} [model]
131
+ * On a `reasoning` part carrying `encrypted`: the model that produced it.
125
132
  * @property {string} [fileUri]
126
133
  * Required on `image` parts. Same schemes and local-read rules as
127
134
  * an envelope `images` ref. Accepted on `user` and `tool` messages.
package/js/core/events.js CHANGED
@@ -101,6 +101,10 @@
101
101
  * `'insufficientOutputBudget' | 'aborted' | ...` additive union.
102
102
  * @property {ToolCall[]} [toolCalls]
103
103
  * Present when `status === 'tool_use'`.
104
+ * @property {import('./envelope.js').MessagePart[]} [reasoningParts]
105
+ * Reasoning parts carrying `encrypted`, from a provider that returns its
106
+ * reasoning to be replayed. A multi-turn caller puts them at the head
107
+ * of the assistant message it records for this result.
104
108
  * @property {number} [maxInterFrameMs]
105
109
  * Longest gap (ms) between adapter events during the call —
106
110
  * from `startedAt` to the first frame, between consecutive
@@ -262,6 +262,11 @@ async function * runStreaming (envelope, client, args, config, start, deps) {
262
262
  return
263
263
  }
264
264
 
265
+ if (finishReason === null) {
266
+ yield { type: 'error', error: classifyProviderError(new Error(`${config.provider} stream ended before completion`), envelope.auth?.key, { provider: config.provider }) }
267
+ return
268
+ }
269
+
265
270
  const collectedToolCalls = Object.values(toolCallAccum).map(tc => ({
266
271
  id: tc.id,
267
272
  function: { name: tc.name, arguments: tc.arguments }
@@ -557,7 +562,7 @@ function flattenText (content) {
557
562
  /** @param {string | import('#core/envelope.js').MessagePart[]} content */
558
563
  function extractReasoning (content) {
559
564
  if (typeof content === 'string' || !Array.isArray(content)) return null
560
- const parts = content.filter(p => p.type === 'reasoning' && p.text).map(p => p.text)
565
+ const parts = content.filter(p => p.type === 'reasoning' && p.text && !p.encrypted).map(p => p.text)
561
566
  return parts.length ? parts.join('\n') : null
562
567
  }
563
568
 
@@ -70,7 +70,8 @@ export async function * openai (envelope, deps = {}) {
70
70
  }
71
71
 
72
72
  const { instructions, input } = splitPrompt(envelope.prompt, imageParts, {
73
- toolResultImages: ['openai', 'chatgpt'].includes(providerOf(envelope.model))
73
+ toolResultImages: ['openai', 'chatgpt'].includes(providerOf(envelope.model)),
74
+ model: envelope.model
74
75
  })
75
76
  if (imageInputs.length) injectImageParts(input, imageInputs)
76
77
 
@@ -94,6 +95,8 @@ export async function * openai (envelope, deps = {}) {
94
95
  // Tool-call accumulation: itemId → {call_id, name, arguments}
95
96
  /** @type {Map<string, {call_id: string, name: string, arguments: string}>} */
96
97
  const toolItems = new Map()
98
+ /** @type {import('#core/envelope.js').MessagePart[]} */
99
+ const reasoningParts = []
97
100
 
98
101
  try {
99
102
  const stream = await client.responses.stream(request, { signal })
@@ -122,6 +125,18 @@ export async function * openai (envelope, deps = {}) {
122
125
  }
123
126
  break
124
127
 
128
+ case 'response.output_item.done':
129
+ if (event.item?.type === 'reasoning' && event.item.encrypted_content) {
130
+ reasoningParts.push({
131
+ type: 'reasoning',
132
+ text: (event.item.summary ?? []).map(s => s.text).join('\n'),
133
+ id: event.item.id,
134
+ encrypted: event.item.encrypted_content,
135
+ model: envelope.model
136
+ })
137
+ }
138
+ break
139
+
125
140
  case 'response.function_call_arguments.delta':
126
141
  if (event.item_id && event.delta) {
127
142
  const t = toolItems.get(event.item_id)
@@ -167,12 +182,12 @@ export async function * openai (envelope, deps = {}) {
167
182
  }
168
183
  break
169
184
 
170
- case 'response.failed':
171
- if (providerOf(envelope.model) === 'chatgpt') {
172
- yield { type: 'error', error: classifyProviderError(new Error('ChatGPT response failed'), envelope.auth?.key, { provider: 'chatgpt' }) }
173
- return
174
- }
175
- break
185
+ case 'response.failed': {
186
+ const provider = providerOf(envelope.model)
187
+ const failure = Object.assign(new Error(`${provider} response failed`), { error: event.response?.error })
188
+ yield { type: 'error', error: classifyProviderError(failure, envelope.auth?.key, { provider }) }
189
+ return
190
+ }
176
191
 
177
192
  default:
178
193
  break
@@ -194,8 +209,9 @@ export async function * openai (envelope, deps = {}) {
194
209
  }
195
210
 
196
211
  const end = String(process.hrtime.bigint())
197
- if (providerOf(envelope.model) === 'chatgpt' && !terminalResponse) {
198
- yield { type: 'error', error: classifyProviderError(new Error('ChatGPT stream ended before completion'), envelope.auth?.key, { provider: 'chatgpt' }) }
212
+ if (!terminalResponse) {
213
+ const provider = providerOf(envelope.model)
214
+ yield { type: 'error', error: classifyProviderError(new Error(`${provider} stream ended before completion`), envelope.auth?.key, { provider }) }
199
215
  return
200
216
  }
201
217
  // OpenAI Responses reports `output_tokens` INCLUDING reasoning
@@ -239,6 +255,7 @@ export async function * openai (envelope, deps = {}) {
239
255
  }
240
256
  }
241
257
  if (warning) done.result.warning = warning
258
+ if (reasoningParts.length) done.result.reasoningParts = reasoningParts
242
259
  if (toolItems.size > 0) {
243
260
  done.result.toolCalls = fromOpenAIToolCalls(Array.from(toolItems.values()))
244
261
  }
@@ -345,6 +362,7 @@ function buildRequest (envelope, input, instructions) {
345
362
  request.max_output_tokens += headroom
346
363
  }
347
364
  request.reasoning = { effort }
365
+ if (effort !== 'none') request.include = ['reasoning.encrypted_content']
348
366
  }
349
367
  }
350
368
 
@@ -391,7 +409,7 @@ function buildRequest (envelope, input, instructions) {
391
409
  *
392
410
  * @param {string | import('#core/envelope.js').Message[]} prompt
393
411
  * @param {Map<object, import('./_images.js').LoadedImage>} imageParts
394
- * @param {{toolResultImages: boolean}} opts
412
+ * @param {{toolResultImages: boolean, model: string}} opts
395
413
  */
396
414
  function splitPrompt (prompt, imageParts, opts) {
397
415
  if (typeof prompt === 'string') {
@@ -409,6 +427,18 @@ function splitPrompt (prompt, imageParts, opts) {
409
427
  }
410
428
  for (const m of prompt) {
411
429
  if (m.role !== 'tool') flushHoisted()
430
+ if (m.role === 'assistant' && Array.isArray(m.content)) {
431
+ for (const p of m.content) {
432
+ if (p.type === 'reasoning' && p.encrypted && p.model === opts.model) {
433
+ input.push({
434
+ type: 'reasoning',
435
+ id: p.id,
436
+ summary: p.text ? [{ type: 'summary_text', text: p.text }] : [],
437
+ encrypted_content: p.encrypted
438
+ })
439
+ }
440
+ }
441
+ }
412
442
  if (m.role === 'system') {
413
443
  systemParts.push(flattenText(m.content))
414
444
  } else if (m.role === 'tool') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mohdel",
3
- "version": "3.8.0",
3
+ "version": "3.9.0",
4
4
  "license": "MIT",
5
5
  "author": {
6
6
  "name": "Christophe Le Bars",
@@ -144,7 +144,7 @@
144
144
  "@opentelemetry/exporter-trace-otlp-grpc": "^0.222.0",
145
145
  "@opentelemetry/sdk-node": "^0.222.0",
146
146
  "chalk": "^6.0.1",
147
- "mohdel-thin-gate-linux-x64-gnu": "3.8.0"
147
+ "mohdel-thin-gate-linux-x64-gnu": "3.9.0"
148
148
  },
149
149
  "dependencies": {
150
150
  "@anthropic-ai/sdk": "^0.129.0",