@henjicc/ai-sdk 0.6.1 → 0.7.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 (128) hide show
  1. package/CHANGELOG.md +41 -27
  2. package/README.md +59 -46
  3. package/dist/capabilities/client.js +103 -86
  4. package/dist/capabilities/speech-recognition/bailian/realtime/module.js +42 -7
  5. package/dist/capabilities/speech-recognition/bailian/realtime/protocol.js +57 -15
  6. package/dist/capabilities/speech-recognition/volcengine/realtime/module.js +42 -10
  7. package/dist/capabilities/translation/bailian/module.js +41 -13
  8. package/dist/capabilities/translation/bailian/sse.d.ts +1 -1
  9. package/dist/capabilities/translation/bailian/sse.js +82 -54
  10. package/dist/capabilities/types.d.ts +4 -0
  11. package/dist/catalog/audio/tts-common.d.ts +5 -0
  12. package/dist/catalog/audio/tts-common.js +17 -0
  13. package/dist/catalog/bailian/cosyvoice-v3.5.model.d.ts +2 -0
  14. package/dist/catalog/bailian/cosyvoice-v3.5.model.js +40 -0
  15. package/dist/catalog/bailian/minimax-speech-2.8.model.d.ts +2 -0
  16. package/dist/catalog/bailian/minimax-speech-2.8.model.js +38 -0
  17. package/dist/catalog/bailian/qwen-audio-3.1-tts-flash.model.d.ts +2 -0
  18. package/dist/catalog/bailian/qwen-audio-3.1-tts-flash.model.js +35 -0
  19. package/dist/catalog/fal/eleven-v3-tts.model.d.ts +2 -0
  20. package/dist/catalog/fal/eleven-v3-tts.model.js +31 -0
  21. package/dist/catalog/fal/minimax-speech-2.8.model.d.ts +2 -0
  22. package/dist/catalog/fal/minimax-speech-2.8.model.js +39 -0
  23. package/dist/catalog/fal/qwen3-tts-1.7b.model.d.ts +2 -0
  24. package/dist/catalog/fal/qwen3-tts-1.7b.model.js +33 -0
  25. package/dist/catalog/index.js +116 -90
  26. package/dist/catalog/kie/eleven-turbo-2.5-tts.model.d.ts +2 -0
  27. package/dist/catalog/kie/eleven-turbo-2.5-tts.model.js +29 -0
  28. package/dist/catalog/kie/eleven-v3-dialogue.model.d.ts +2 -0
  29. package/dist/catalog/kie/eleven-v3-dialogue.model.js +46 -0
  30. package/dist/catalog/kie/gemini-3.1-flash-tts.model.d.ts +2 -0
  31. package/dist/catalog/kie/gemini-3.1-flash-tts.model.js +40 -0
  32. package/dist/catalog/ppio/minimax-speech.model.js +1 -3
  33. package/dist/catalog/siliconflow/cosyvoice2-tts.model.d.ts +2 -0
  34. package/dist/catalog/siliconflow/cosyvoice2-tts.model.js +27 -0
  35. package/dist/catalog/siliconflow/moss-ttsd-0.5.model.d.ts +2 -0
  36. package/dist/catalog/siliconflow/moss-ttsd-0.5.model.js +28 -0
  37. package/dist/catalog/volcengine-speech/seed-icl-2.0.model.d.ts +2 -0
  38. package/dist/catalog/volcengine-speech/seed-icl-2.0.model.js +26 -0
  39. package/dist/catalog/volcengine-speech/seed-tts-2.0.model.d.ts +2 -0
  40. package/dist/catalog/volcengine-speech/seed-tts-2.0.model.js +25 -0
  41. package/dist/llm/chat.js +6 -1
  42. package/dist/llm/streaming.js +131 -91
  43. package/dist/packs/models/bailian/cosyvoice-v3.5.d.ts +6 -0
  44. package/dist/packs/models/bailian/cosyvoice-v3.5.js +7 -0
  45. package/dist/packs/models/bailian/minimax-speech-2.8.d.ts +6 -0
  46. package/dist/packs/models/bailian/minimax-speech-2.8.js +7 -0
  47. package/dist/packs/models/bailian/qwen-audio-3.1-tts-flash.d.ts +6 -0
  48. package/dist/packs/models/bailian/qwen-audio-3.1-tts-flash.js +7 -0
  49. package/dist/packs/models/fal/eleven-v3-tts.d.ts +6 -0
  50. package/dist/packs/models/fal/eleven-v3-tts.js +7 -0
  51. package/dist/packs/models/fal/minimax-speech-2.8.d.ts +6 -0
  52. package/dist/packs/models/fal/minimax-speech-2.8.js +7 -0
  53. package/dist/packs/models/fal/qwen3-tts-1.7b.d.ts +6 -0
  54. package/dist/packs/models/fal/qwen3-tts-1.7b.js +7 -0
  55. package/dist/packs/models/kie/eleven-turbo-2.5-tts.d.ts +6 -0
  56. package/dist/packs/models/kie/eleven-turbo-2.5-tts.js +7 -0
  57. package/dist/packs/models/kie/eleven-v3-dialogue.d.ts +6 -0
  58. package/dist/packs/models/kie/eleven-v3-dialogue.js +7 -0
  59. package/dist/packs/models/kie/gemini-3.1-flash-tts.d.ts +6 -0
  60. package/dist/packs/models/kie/gemini-3.1-flash-tts.js +7 -0
  61. package/dist/packs/models/siliconflow/cosyvoice2-tts.d.ts +6 -0
  62. package/dist/packs/models/siliconflow/cosyvoice2-tts.js +7 -0
  63. package/dist/packs/models/siliconflow/moss-ttsd-0.5.d.ts +6 -0
  64. package/dist/packs/models/siliconflow/moss-ttsd-0.5.js +7 -0
  65. package/dist/packs/models/volcengine-speech/seed-icl-2.0.d.ts +6 -0
  66. package/dist/packs/models/volcengine-speech/seed-icl-2.0.js +7 -0
  67. package/dist/packs/models/volcengine-speech/seed-tts-2.0.d.ts +6 -0
  68. package/dist/packs/models/volcengine-speech/seed-tts-2.0.js +7 -0
  69. package/dist/packs/provider-adapters/siliconflow.d.ts +3 -0
  70. package/dist/packs/provider-adapters/siliconflow.js +5 -0
  71. package/dist/packs/provider-adapters/volcengine-speech.d.ts +3 -0
  72. package/dist/packs/provider-adapters/volcengine-speech.js +5 -0
  73. package/dist/packs/provider-packs/bailian.d.ts +1 -1
  74. package/dist/packs/provider-packs/bailian.js +6 -3
  75. package/dist/packs/provider-packs/fal.d.ts +1 -1
  76. package/dist/packs/provider-packs/fal.js +41 -38
  77. package/dist/packs/provider-packs/kie.d.ts +1 -1
  78. package/dist/packs/provider-packs/kie.js +33 -30
  79. package/dist/packs/provider-packs/siliconflow.d.ts +4 -0
  80. package/dist/packs/provider-packs/siliconflow.js +7 -0
  81. package/dist/packs/provider-packs/volcengine-speech.d.ts +4 -0
  82. package/dist/packs/provider-packs/volcengine-speech.js +7 -0
  83. package/dist/protocols/sse-events.d.ts +4 -0
  84. package/dist/protocols/sse-events.js +31 -0
  85. package/dist/providers/audio-data.d.ts +2 -0
  86. package/dist/providers/audio-data.js +21 -0
  87. package/dist/providers/bailian.js +30 -2
  88. package/dist/providers/index.js +4 -0
  89. package/dist/providers/metadata.js +8 -0
  90. package/dist/providers/siliconflow.d.ts +3 -0
  91. package/dist/providers/siliconflow.js +34 -0
  92. package/dist/providers/volcengine-speech.d.ts +3 -0
  93. package/dist/providers/volcengine-speech.js +143 -0
  94. package/dist/types/model.d.ts +4 -4
  95. package/dist/types/model.js +3 -1
  96. package/docs/consumers.md +22 -5
  97. package/docs/llm-adaptation/README.md +28 -3
  98. package/docs/llm-adaptation//344/276/233/345/272/224/345/225/206/DeepSeek.md +14 -14
  99. package/docs/llm-adaptation//344/276/233/345/272/224/345/225/206/Kimi.md +3 -3
  100. package/docs/llm-adaptation//344/276/233/345/272/224/345/225/206//345/260/217/347/261/263MiMo.md +42 -42
  101. package/docs/llm-adaptation//344/276/233/345/272/224/345/225/206//346/231/272/350/260/261GLM.md +2 -0
  102. package/docs/model-adaptation/Fun-ASR-Realtime/Fun-ASR-Realtime_/347/231/276/347/202/274.md +22 -2
  103. package/docs/model-adaptation/GLM-5.3-Flash/GLM-5.3-Flash_/346/231/272/350/260/261.md +6 -6
  104. package/docs/model-adaptation/GPT-Image-2.5/GPT-Image-2.5_APIMart.md +66 -66
  105. package/docs/model-adaptation/GPT-Image-2.5/GPT-Image-2.5_Fal.md +55 -55
  106. package/docs/model-adaptation/GPT-Image-2.5/GPT-Image-2.5_Grsai.md +53 -53
  107. package/docs/model-adaptation/GPT-Image-2.5/GPT-Image-2.5_KIE.md +55 -55
  108. package/docs/model-adaptation/HappyHorse-1.1/HappyHorse-1.1_KIE.md +27 -27
  109. package/docs/model-adaptation/IC-Light-v2/IC-Light-v2_Fal.md +14 -14
  110. package/docs/model-adaptation/Qwen-MT-Flash/Qwen-MT-Flash_/347/231/276/347/202/274.md +22 -1
  111. package/docs/model-adaptation/Qwen-MT-Lite/Qwen-MT-Lite_/347/231/276/347/202/274.md +2 -1
  112. package/docs/model-adaptation/Qwen-MT-Plus/Qwen-MT-Plus_/347/231/276/347/202/274.md +2 -1
  113. package/docs/model-adaptation/Qwen3-ASR-Flash-Realtime/Qwen3-ASR-Flash-Realtime_/347/231/276/347/202/274.md +25 -2
  114. package/docs/model-adaptation/README.md +14 -10
  115. package/docs/model-adaptation/SeedASR-2.0-Realtime/SeedASR-2.0-Realtime_/347/201/253/345/261/261/345/274/225/346/223/216.md +6 -2
  116. package/docs/model-adaptation/Seedream-5.0-Flash/Seedream-5.0-Flash_/347/201/253/345/261/261/345/274/225/346/223/216.md +18 -18
  117. package/docs/model-adaptation/TTS-/345/244/232/344/276/233/345/272/224/345/225/206-2026-09.md +55 -0
  118. package/docs/model-adaptation//344/276/233/345/272/224/345/225/206//346/264/276/346/254/247/344/272/221.md +9 -9
  119. package/docs/model-adaptation//344/276/233/345/272/224/345/225/206//347/201/253/345/261/261/345/274/225/346/223/216.md +11 -11
  120. package/docs/model-adaptation//344/276/233/345/272/224/345/225/206//347/231/276/347/202/274.md +31 -31
  121. package/docs/model-adaptation//344/276/233/345/272/224/345/225/206//347/241/205/345/237/272/346/265/201/345/212/250.md +67 -67
  122. package/docs/model-adaptation//346/226/207/346/241/243/351/207/207/351/233/206/346/211/213/345/206/214.md +8 -8
  123. package/examples/form-renderer/package.json +1 -1
  124. package/examples/llm-chat/README.md +8 -8
  125. package/examples/llm-chat/index.ts +2 -2
  126. package/examples/llm-chat/package.json +1 -1
  127. package/examples/minimal-node/package.json +1 -1
  128. package/package.json +258 -258
@@ -116,17 +116,33 @@ export function qwenUsesManualCommit(input) {
116
116
  return inputOptions(input).turnDetection === 'manual';
117
117
  }
118
118
  export function parseRealtimeMessage(preset, payload) {
119
- let parsed;
119
+ let eventType;
120
120
  try {
121
- parsed = JSON.parse(payload);
121
+ let parsed;
122
+ try {
123
+ parsed = JSON.parse(payload);
124
+ }
125
+ catch {
126
+ throw new AiRuntimeError('invalid_response', 'Server returned invalid JSON');
127
+ }
128
+ const root = record(parsed);
129
+ if (!root)
130
+ throw new AiRuntimeError('invalid_response', 'Server returned a non-object event');
131
+ const discriminator = preset.protocol === 'fun-duplex' ? record(root.header)?.event : root.type;
132
+ if (typeof discriminator !== 'string' || !discriminator.trim()) {
133
+ throw new AiRuntimeError('invalid_response', 'Server event has no event type');
134
+ }
135
+ eventType = /^[a-zA-Z][\w.-]{0,127}$/.test(discriminator) ? discriminator : 'unrecognized-event';
136
+ return preset.protocol === 'fun-duplex' ? parseFunEvent(root) : parseQwenEvent(root);
122
137
  }
123
- catch {
124
- throw new AiRuntimeError('invalid_response', 'Bailian realtime server returned invalid JSON');
138
+ catch (error) {
139
+ if (!(error instanceof AiRuntimeError))
140
+ throw error;
141
+ throw new AiRuntimeError(error.code, `${preset.modelId} (${preset.protocol}): ${error.message.replace(`[${error.code}] `, '')}`, {
142
+ providerId: 'bailian', modelId: preset.modelId, protocol: preset.protocol,
143
+ stage: 'parse', eventType,
144
+ });
125
145
  }
126
- const root = record(parsed);
127
- if (!root)
128
- throw new AiRuntimeError('invalid_response', 'Bailian realtime server returned a non-object event');
129
- return preset.protocol === 'fun-duplex' ? parseFunEvent(root) : parseQwenEvent(root);
130
146
  }
131
147
  function parseFunEvent(root) {
132
148
  const header = record(root.header);
@@ -147,6 +163,9 @@ function parseFunEvent(root) {
147
163
  const sentence = record(record(record(root.payload)?.output)?.sentence);
148
164
  if (sentence?.heartbeat === true)
149
165
  return { kind: 'ignored', eventType: 'heartbeat' };
166
+ if (typeof sentence?.text !== 'string' || typeof sentence.sentence_end !== 'boolean') {
167
+ throw new AiRuntimeError('invalid_response', 'Result requires text and boolean sentence_end');
168
+ }
150
169
  const segment = parseBailianSentence(sentence);
151
170
  const transcript = segment?.text ?? string(sentence?.text);
152
171
  if (!transcript) {
@@ -156,16 +175,16 @@ function parseFunEvent(root) {
156
175
  return { kind: 'ignored', eventType: sentence.sentence_begin === true ? 'empty-sentence-begin' : 'empty-intermediate' };
157
176
  }
158
177
  if (sentence?.sentence_end === true) {
159
- throw new AiRuntimeError('invalid_response', 'Bailian Fun-ASR final result has no text');
178
+ throw new AiRuntimeError('invalid_response', 'Final result has no text');
160
179
  }
161
- throw new AiRuntimeError('invalid_response', 'Bailian Fun-ASR result has no text or sentence state');
180
+ throw new AiRuntimeError('invalid_response', 'Result has no text or sentence state');
162
181
  }
163
182
  const duration = number((record(root.usage) ?? record(record(root.payload)?.usage))?.duration);
164
183
  return sentence?.sentence_end === true
165
184
  ? { kind: 'final', text: transcript, segment, durationMs: duration === undefined ? undefined : duration * 1_000 }
166
185
  : { kind: 'partial', text: transcript, segment };
167
186
  }
168
- return { kind: 'unknown', eventType: eventType || 'missing-event' };
187
+ return { kind: 'unknown', eventType: /^[a-zA-Z][\w.-]{0,127}$/.test(eventType) ? eventType : 'unrecognized-event' };
169
188
  }
170
189
  function parseQwenEvent(root) {
171
190
  const eventType = string(root.type) ?? '';
@@ -176,19 +195,42 @@ function parseQwenEvent(root) {
176
195
  return { kind: 'ready', sessionId: string(record(root.session)?.id) ?? string(root.session_id) };
177
196
  }
178
197
  if (eventType === 'conversation.item.input_audio_transcription.text') {
179
- const transcript = `${string(root.text) ?? ''}${string(root.stash) ?? ''}`.trim();
180
- if (!transcript)
198
+ if (typeof root.text !== 'string' || typeof root.stash !== 'string') {
199
+ throw new AiRuntimeError('invalid_response', 'Partial event requires string text and stash');
200
+ }
201
+ const transcript = root.text + root.stash;
202
+ if (!transcript.trim())
181
203
  throw new AiRuntimeError('invalid_response', 'Bailian Qwen realtime partial event has no text');
182
204
  return { kind: 'partial', text: transcript };
183
205
  }
184
206
  if (eventType === 'conversation.item.input_audio_transcription.completed') {
185
- const transcript = string(root.transcript) ?? string(root.text);
207
+ const transcript = string(root.transcript);
186
208
  if (!transcript)
187
209
  throw new AiRuntimeError('invalid_response', 'Bailian Qwen realtime final event has no transcript');
188
210
  return { kind: 'final', text: transcript };
189
211
  }
190
212
  if (eventType === 'session.finished')
191
213
  return { kind: 'finished' };
214
+ if (eventType === 'input_audio_buffer.speech_started' || eventType === 'input_audio_buffer.speech_stopped') {
215
+ const timestamp = root[eventType.endsWith('speech_started') ? 'audio_start_ms' : 'audio_end_ms'];
216
+ if (!string(root.item_id) || typeof timestamp !== 'number' || !Number.isInteger(timestamp) || timestamp < 0) {
217
+ throw new AiRuntimeError('invalid_response', 'Invalid speech activity notification');
218
+ }
219
+ return { kind: 'ignored', eventType };
220
+ }
221
+ if (eventType === 'input_audio_buffer.committed') {
222
+ if (!string(root.item_id))
223
+ throw new AiRuntimeError('invalid_response', 'Commit notification has no item_id');
224
+ return { kind: 'ignored', eventType };
225
+ }
226
+ if (eventType === 'conversation.item.created') {
227
+ const item = record(root.item);
228
+ if (!string(item?.id) || !Array.isArray(item?.content)
229
+ || !item.content.some(value => record(value)?.type === 'input_audio' && record(value)?.transcript === null)) {
230
+ throw new AiRuntimeError('invalid_response', 'Invalid audio item notification');
231
+ }
232
+ return { kind: 'ignored', eventType };
233
+ }
192
234
  if (eventType === 'error' || eventType === 'conversation.item.input_audio_transcription.failed') {
193
235
  const error = record(root.error);
194
236
  return {
@@ -196,5 +238,5 @@ function parseQwenEvent(root) {
196
238
  message: string(error?.message) ?? string(root.message) ?? 'Bailian Qwen realtime task failed',
197
239
  };
198
240
  }
199
- return { kind: 'unknown', eventType: eventType || 'missing-type' };
241
+ return { kind: 'unknown', eventType: /^[a-zA-Z][\w.-]{0,127}$/.test(eventType) ? eventType : 'unrecognized-event' };
200
242
  }
@@ -233,6 +233,7 @@ async function openDriver(preset, input, requestId, connection, context, openTim
233
233
  let closePromise;
234
234
  let finishPromise;
235
235
  let terminalError;
236
+ let lastFrame;
236
237
  let lastPartial = '';
237
238
  let durationMs;
238
239
  let logId;
@@ -240,17 +241,34 @@ async function openDriver(preset, input, requestId, connection, context, openTim
240
241
  const segments = [];
241
242
  const ready = deferred();
242
243
  const finished = deferred();
244
+ const onAbort = () => fail(cancelledError(context.requestId));
245
+ const checkStopped = () => {
246
+ if (context.signal.aborted)
247
+ throw cancelledError(context.requestId);
248
+ if (terminalError)
249
+ throw terminalError;
250
+ if (phase === 'closed')
251
+ throw new AiRuntimeError('realtime_session_closed', 'Volcengine realtime session was closed');
252
+ };
243
253
  const closeConnection = () => {
254
+ context.signal.removeEventListener('abort', onAbort);
244
255
  closePromise ??= Promise.resolve().then(async () => await connection.close(1000, 'session complete'));
245
256
  return closePromise;
246
257
  };
247
- const fail = (error) => {
258
+ const fail = (error, operation = 'receive') => {
248
259
  if (phase === 'finished' || phase === 'closed' || phase === 'failed')
249
260
  return;
261
+ const failedPhase = phase;
250
262
  phase = 'failed';
251
- const normalized = error instanceof AiRuntimeError
263
+ const original = error instanceof AiRuntimeError
252
264
  ? error
253
- : new AiRuntimeError('provider_realtime_error', error instanceof Error ? error.message : 'Volcengine realtime ASR failed');
265
+ : new AiRuntimeError('provider_realtime_error', 'Realtime transport or event handler failed');
266
+ const normalized = new AiRuntimeError(original.code, `${preset.modelId} (volcengine-binary-v1): ${original.message.replace(`[${original.code}] `, '')}`, {
267
+ providerId: 'volcengine', modelId: preset.modelId, protocol: 'volcengine-binary-v1',
268
+ stage: failedPhase, operation,
269
+ providerCode: original.details?.providerCode,
270
+ lastSequence: lastFrame?.sequence, lastEventNumber: lastFrame?.eventNumber, lastFrame: lastFrame?.last,
271
+ });
254
272
  terminalError = normalized;
255
273
  ready.reject(normalized);
256
274
  finished.reject(normalized);
@@ -277,6 +295,7 @@ async function openDriver(preset, input, requestId, connection, context, openTim
277
295
  },
278
296
  };
279
297
  await context.emit({ type: 'completed', output });
298
+ checkStopped();
280
299
  phase = 'finished';
281
300
  finished.resolve(output);
282
301
  await closeConnection();
@@ -302,6 +321,7 @@ async function openDriver(preset, input, requestId, connection, context, openTim
302
321
  }
303
322
  phase = 'active';
304
323
  await context.emit({ type: 'started', sessionId: requestId });
324
+ checkStopped();
305
325
  ready.resolve();
306
326
  }
307
327
  else if (phase !== 'active' && phase !== 'finishing') {
@@ -315,6 +335,7 @@ async function openDriver(preset, input, requestId, connection, context, openTim
315
335
  ? (({ definite: _definite, ...segment }) => segment)(candidate)
316
336
  : undefined;
317
337
  await context.emit({ type: 'partial', text: parsed.text, segment: partial });
338
+ checkStopped();
318
339
  }
319
340
  }
320
341
  for (const utterance of parsed.utterances) {
@@ -323,6 +344,7 @@ async function openDriver(preset, input, requestId, connection, context, openTim
323
344
  const { definite: _definite, ...segment } = utterance;
324
345
  if (upsertFinal(segments, segment)) {
325
346
  await context.emit({ type: 'final', text: segment.text, segment });
347
+ checkStopped();
326
348
  }
327
349
  }
328
350
  if (frame.last)
@@ -333,12 +355,14 @@ async function openDriver(preset, input, requestId, connection, context, openTim
333
355
  for await (const message of connection.messages) {
334
356
  if (context.signal.aborted)
335
357
  throw cancelledError(context.requestId);
358
+ lastFrame = undefined;
336
359
  if (typeof message.data === 'string') {
337
360
  throw new AiRuntimeError('invalid_response', 'Volcengine realtime server returned a text WebSocket frame');
338
361
  }
339
362
  const frame = parseVolcengineServerFrame(message.data);
363
+ lastFrame = { sequence: frame.sequence, eventNumber: frame.event, last: frame.last };
340
364
  if (frame.kind === 'error') {
341
- throw new AiRuntimeError('provider_task_failed', `${frame.code}: ${frame.message}`);
365
+ throw new AiRuntimeError('provider_task_failed', `Server reported a realtime task failure (${frame.code})`, { providerCode: frame.code });
342
366
  }
343
367
  await handle(frame);
344
368
  }
@@ -351,7 +375,6 @@ async function openDriver(preset, input, requestId, connection, context, openTim
351
375
  }
352
376
  };
353
377
  void consume();
354
- const onAbort = () => fail(cancelledError(context.requestId));
355
378
  context.signal.addEventListener('abort', onAbort, { once: true });
356
379
  let openTimer;
357
380
  try {
@@ -369,8 +392,16 @@ async function openDriver(preset, input, requestId, connection, context, openTim
369
392
  ]);
370
393
  }
371
394
  catch (error) {
372
- fail(error);
373
- await closeConnection();
395
+ fail(error, 'open');
396
+ try {
397
+ await closeConnection();
398
+ }
399
+ catch {
400
+ const primary = terminalError;
401
+ throw new AiRuntimeError(primary.code, primary.message.replace(`[${primary.code}] `, ''), {
402
+ ...primary.details, cleanupFailed: true,
403
+ });
404
+ }
374
405
  throw terminalError ?? error;
375
406
  }
376
407
  finally {
@@ -378,6 +409,7 @@ async function openDriver(preset, input, requestId, connection, context, openTim
378
409
  clearTimeout(openTimer);
379
410
  }
380
411
  return {
412
+ result: finished.promise,
381
413
  send: (chunk) => {
382
414
  if (terminalError)
383
415
  return Promise.reject(terminalError);
@@ -402,7 +434,7 @@ async function openDriver(preset, input, requestId, connection, context, openTim
402
434
  });
403
435
  sendQueue = operation.catch(() => undefined);
404
436
  return operation.catch((error) => {
405
- fail(error);
437
+ fail(error, 'send');
406
438
  throw terminalError ?? error;
407
439
  });
408
440
  },
@@ -428,12 +460,12 @@ async function openDriver(preset, input, requestId, connection, context, openTim
428
460
  await connection.send(encodeVolcengineAudioRequest(finalAudio, nextSequence, true));
429
461
  }
430
462
  catch (error) {
431
- fail(error);
463
+ fail(error, 'finish');
432
464
  throw terminalError ?? error;
433
465
  }
434
466
  return await finished.promise;
435
467
  })();
436
- void finishPromise.catch((error) => fail(error));
468
+ void finishPromise.catch((error) => fail(error, 'finish'));
437
469
  return finishPromise;
438
470
  },
439
471
  close: async () => {
@@ -24,11 +24,16 @@ export function createBailianQwenMtTranslationModule(modelId, config = {}) {
24
24
  executionModes: ['request-response', 'event-stream'],
25
25
  },
26
26
  execute: async (input, context) => {
27
+ const checkAbort = () => {
28
+ if (context.signal.aborted)
29
+ throw new AiRuntimeError('cancelled', 'Translation request cancelled');
30
+ };
27
31
  const source = normalizeSource(input);
28
32
  const options = normalizeOptions(input.options, defaultStream);
29
33
  const targetLanguage = normalizeBailianQwenMtLanguage(requireText(input.targetLanguage, 'targetLanguage'));
30
34
  const sourceLanguage = normalizeBailianQwenMtLanguage(input.sourceLanguage?.trim() || 'auto');
31
35
  const apiKey = await context.runtime.credentials.get('translation', 'bailian');
36
+ checkAbort();
32
37
  if (!apiKey?.trim()) {
33
38
  throw new AiRuntimeError('api_key_missing', 'Bailian translation API key is not configured');
34
39
  }
@@ -37,6 +42,7 @@ export function createBailianQwenMtTranslationModule(modelId, config = {}) {
37
42
  const metadata = [];
38
43
  let usage;
39
44
  for (let index = 0; index < source.length; index += 1) {
45
+ checkAbort();
40
46
  const entry = source[index];
41
47
  const result = entry.text.length === 0
42
48
  ? { item: { text: '', sourceText: entry.text, id: entry.id }, metadata: {} }
@@ -44,11 +50,13 @@ export function createBailianQwenMtTranslationModule(modelId, config = {}) {
44
50
  apiKey: apiKey.trim(), endpoint, preset, input, options,
45
51
  sourceLanguage, targetLanguage, entry, index, context,
46
52
  });
53
+ checkAbort();
47
54
  translations.push(result.item);
48
55
  metadata.push(result.metadata);
49
56
  usage = addUsage(usage, result.usage);
50
57
  await context.emit({ type: 'item', index, item: result.item });
51
58
  }
59
+ checkAbort();
52
60
  const output = {
53
61
  translations,
54
62
  usage,
@@ -80,6 +88,7 @@ async function executeItem(input) {
80
88
  event: 'bailian.translation.request.start', requestId: context.requestId,
81
89
  providerId: 'bailian', modelId: preset.modelId, context: { itemIndex: index },
82
90
  });
91
+ let stage = 'request';
83
92
  try {
84
93
  const response = await fetchProvider('Bailian Qwen-MT', input.endpoint, {
85
94
  method: 'POST',
@@ -91,11 +100,15 @@ async function executeItem(input) {
91
100
  body: JSON.stringify(buildRequestBody(input)),
92
101
  signal: context.signal,
93
102
  }, { transport: context.runtime.transport, retryPreconnectOnce: true });
103
+ stage = 'http';
94
104
  if (!response.ok)
95
105
  throw await createHttpError(response);
106
+ stage = 'parse';
96
107
  const result = input.options.stream
97
108
  ? await parseStreamingResponse(response, input)
98
109
  : await parseCompleteResponse(response, entry);
110
+ if (context.signal.aborted)
111
+ throw new AiRuntimeError('cancelled', 'Translation request cancelled');
99
112
  context.runtime.logger.info('百炼翻译请求完成', {
100
113
  event: 'bailian.translation.request.completed', requestId: context.requestId,
101
114
  providerId: 'bailian', modelId: preset.modelId,
@@ -105,12 +118,21 @@ async function executeItem(input) {
105
118
  return result;
106
119
  }
107
120
  catch (error) {
121
+ const normalized = error instanceof AiRuntimeError && error.details?.protocol === 'qwen-mt-sse'
122
+ ? error
123
+ : new AiRuntimeError(error instanceof AiRuntimeError ? error.code : 'provider_request_failed', error instanceof AiRuntimeError
124
+ ? `Bailian ${preset.modelId}: ${error.message.replace(/^\[[^\]]+\]\s*/, '')}`
125
+ : `Bailian ${preset.modelId}: Translation ${stage} failed`, {
126
+ ...(error instanceof AiRuntimeError ? error.details : {}),
127
+ providerId: 'bailian', modelId: preset.modelId,
128
+ protocol: input.options.stream ? 'qwen-mt-sse' : 'qwen-mt-json', stage,
129
+ });
108
130
  context.runtime.logger.error('百炼翻译请求失败', {
109
131
  event: 'bailian.translation.request.failed', requestId: context.requestId,
110
- providerId: 'bailian', modelId: preset.modelId, context: { itemIndex: index }, error,
132
+ providerId: 'bailian', modelId: preset.modelId, context: { itemIndex: index }, error: normalized,
111
133
  });
112
- span.end(error);
113
- throw error;
134
+ span.end(normalized);
135
+ throw normalized;
114
136
  }
115
137
  }
116
138
  function buildRequestBody(input) {
@@ -147,7 +169,7 @@ async function parseStreamingResponse(response, input) {
147
169
  onUsage: async (usage) => await input.context.emit({
148
170
  type: 'usage', index: input.index, id: input.entry.id, usage,
149
171
  }),
150
- });
172
+ }, input.preset.modelId);
151
173
  return {
152
174
  item: { text: result.text, sourceText: input.entry.text, id: input.entry.id },
153
175
  usage: result.usage,
@@ -169,6 +191,12 @@ async function parseCompleteResponse(response, entry) {
169
191
  if (text === undefined) {
170
192
  throw new AiRuntimeError('empty_result', 'Bailian Qwen-MT response has no translated text');
171
193
  }
194
+ if (choice?.finish_reason === 'length') {
195
+ throw new AiRuntimeError('provider_task_failed', 'Bailian Qwen-MT translation was truncated by the output limit', { finishReason: 'length' });
196
+ }
197
+ if (choice?.finish_reason !== 'stop') {
198
+ throw new AiRuntimeError('provider_response_invalid', 'Bailian Qwen-MT response has no valid finish_reason');
199
+ }
172
200
  return {
173
201
  item: { text, sourceText: entry.text, id: entry.id },
174
202
  usage: readUsage(payload.usage),
@@ -182,9 +210,7 @@ async function parseCompleteResponse(response, entry) {
182
210
  async function createHttpError(response) {
183
211
  const payload = await readJson(response, true);
184
212
  const error = isRecord(payload.error) ? payload.error : undefined;
185
- const providerCode = readNonEmptyString(error?.code) ?? readNonEmptyString(payload.code);
186
- const message = readNonEmptyString(error?.message) ?? readNonEmptyString(payload.message)
187
- ?? `HTTP ${response.status}`;
213
+ const providerCode = readProviderCode(error?.code ?? payload.code);
188
214
  const code = response.status === 401 || response.status === 403
189
215
  ? 'provider_auth_error'
190
216
  : response.status === 429
@@ -192,7 +218,7 @@ async function createHttpError(response) {
192
218
  : response.status >= 500
193
219
  ? 'provider_http_error'
194
220
  : 'provider_request_failed';
195
- return new AiRuntimeError(code, `Bailian Qwen-MT ${providerCode ? `${providerCode}: ` : ''}${message}`);
221
+ return new AiRuntimeError(code, `Bailian Qwen-MT HTTP ${response.status}`, { providerCode, statusCode: response.status });
196
222
  }
197
223
  async function readJson(response, allowInvalid = false) {
198
224
  try {
@@ -209,12 +235,14 @@ async function readJson(response, allowInvalid = false) {
209
235
  throw new AiRuntimeError('provider_response_invalid', 'Bailian Qwen-MT response is not valid JSON');
210
236
  }
211
237
  function throwPayloadError(payload) {
212
- const error = isRecord(payload.error) ? payload.error : undefined;
213
- const code = readNonEmptyString(error?.code) ?? readNonEmptyString(payload.code);
214
- if (!code)
238
+ if (payload.error == null && payload.code == null)
215
239
  return;
216
- const message = readNonEmptyString(error?.message) ?? readNonEmptyString(payload.message) ?? code;
217
- throw new AiRuntimeError('provider_task_failed', `Bailian Qwen-MT ${code}: ${message}`);
240
+ const error = isRecord(payload.error) ? payload.error : undefined;
241
+ const providerCode = readProviderCode(error?.code ?? payload.code);
242
+ throw new AiRuntimeError('provider_task_failed', 'Bailian Qwen-MT server rejected translation', { providerCode });
243
+ }
244
+ function readProviderCode(value) {
245
+ return typeof value === 'string' && /^[\w.-]{1,128}$/.test(value) ? value : undefined;
218
246
  }
219
247
  function normalizeSource(input) {
220
248
  if (typeof input.source === 'string')
@@ -15,5 +15,5 @@ interface QwenMtStreamCallbacks {
15
15
  }): Promise<void>;
16
16
  onUsage(usage: TranslationUsage): Promise<void>;
17
17
  }
18
- export declare function readQwenMtSse(body: ReadableStream<Uint8Array>, streamingContent: BailianQwenMtStreamingContent, signal: AbortSignal, callbacks: QwenMtStreamCallbacks): Promise<QwenMtStreamResult>;
18
+ export declare function readQwenMtSse(body: ReadableStream<Uint8Array>, streamingContent: BailianQwenMtStreamingContent, signal: AbortSignal, callbacks: QwenMtStreamCallbacks, modelId: string): Promise<QwenMtStreamResult>;
19
19
  export {};
@@ -1,6 +1,7 @@
1
1
  import { createUtf8StreamDecoder } from '../../../protocols/utf8-stream-decoder.js';
2
+ import { drainSseEvents } from '../../../protocols/sse-events.js';
2
3
  import { AiRuntimeError } from '../../../runtime/AiRuntimeError.js';
3
- export async function readQwenMtSse(body, streamingContent, signal, callbacks) {
4
+ export async function readQwenMtSse(body, streamingContent, signal, callbacks, modelId) {
4
5
  const reader = body.getReader();
5
6
  const decoder = createUtf8StreamDecoder();
6
7
  let pending = '';
@@ -10,18 +11,33 @@ export async function readQwenMtSse(body, streamingContent, signal, callbacks) {
10
11
  let responseModel;
11
12
  let finishReason;
12
13
  let ended = false;
13
- const cancelReader = () => { void reader.cancel(); };
14
- if (signal.aborted)
15
- cancelReader();
16
- else
17
- signal.addEventListener('abort', cancelReader, { once: true });
14
+ let eof = false;
15
+ let stage = 'read';
16
+ let cancellation;
17
+ const failure = (code, reason, details) => new AiRuntimeError(code, `Bailian ${modelId}: ${reason}`, {
18
+ providerId: 'bailian', modelId, protocol: 'qwen-mt-sse', stage,
19
+ receivedFinish: finishReason !== undefined, ...details,
20
+ });
21
+ const checkAbort = () => {
22
+ if (signal.aborted)
23
+ throw failure('cancelled', 'Translation stream cancelled');
24
+ };
25
+ // A host cleanup rejection must not replace the original failure or escape
26
+ // as an unhandled rejection; never wait indefinitely for host cancellation.
27
+ const cancelReader = () => { cancellation ??= reader.cancel().catch(() => undefined); };
28
+ signal.addEventListener('abort', cancelReader, { once: true });
18
29
  const consume = async (event) => {
19
- const data = event
20
- .split(/\r?\n/)
21
- .map((line) => line.trim())
30
+ const lines = event.split(/\r\n|\r|\n/);
31
+ const eventName = lines.filter(line => line.startsWith('event:')).at(-1)?.slice(6).trim();
32
+ const data = lines
22
33
  .filter((line) => line.startsWith('data:'))
23
34
  .map((line) => line.slice(5).trim())
24
35
  .join('\n');
36
+ if (eventName === 'error')
37
+ throw failure('provider_task_failed', 'Server sent an error event');
38
+ // SSE extension notifications are not translation results or completion evidence.
39
+ if (eventName && eventName !== 'message')
40
+ return;
25
41
  if (!data || data === '[DONE]') {
26
42
  if (data === '[DONE]')
27
43
  ended = true;
@@ -32,26 +48,54 @@ export async function readQwenMtSse(body, streamingContent, signal, callbacks) {
32
48
  payload = JSON.parse(data);
33
49
  }
34
50
  catch {
35
- throw new AiRuntimeError('provider_response_invalid', 'Bailian Qwen-MT SSE contains invalid JSON');
51
+ throw failure('provider_response_invalid', 'SSE contains invalid JSON');
36
52
  }
37
53
  if (!isRecord(payload)) {
38
- throw new AiRuntimeError('provider_response_invalid', 'Bailian Qwen-MT SSE payload is not an object');
54
+ throw failure('provider_response_invalid', 'SSE payload is not an object');
55
+ }
56
+ if (payload.error != null || payload.code != null) {
57
+ const error = isRecord(payload.error) ? payload.error : undefined;
58
+ const code = error?.code ?? payload.code;
59
+ const providerCode = typeof code === 'string' && /^[\w.-]{1,128}$/.test(code) ? code : undefined;
60
+ throw failure('provider_task_failed', 'Server rejected translation', { providerCode });
61
+ }
62
+ if (!Array.isArray(payload.choices)) {
63
+ throw failure('provider_response_invalid', 'SSE payload is missing choices');
39
64
  }
40
- throwPayloadError(payload);
41
65
  requestId = readNonEmptyString(payload.id) ?? requestId;
42
66
  responseModel = readNonEmptyString(payload.model) ?? responseModel;
43
67
  const nextUsage = readUsage(payload.usage);
68
+ if (payload.choices.length === 0 && !nextUsage) {
69
+ throw failure('provider_response_invalid', 'Empty choices requires usage');
70
+ }
44
71
  if (nextUsage) {
45
72
  usage = nextUsage;
73
+ stage = 'callback';
46
74
  await callbacks.onUsage(nextUsage);
75
+ checkAbort();
76
+ stage = 'parse';
47
77
  }
48
- const choice = Array.isArray(payload.choices) && isRecord(payload.choices[0])
49
- ? payload.choices[0]
50
- : undefined;
51
- finishReason = readNonEmptyString(choice?.finish_reason) ?? finishReason;
52
- const delta = isRecord(choice?.delta) ? readString(choice.delta.content) : undefined;
53
- if (delta === undefined || delta.length === 0)
78
+ if (payload.choices.length === 0)
54
79
  return;
80
+ const choice = payload.choices[0];
81
+ if (!isRecord(choice) || !isRecord(choice.delta) || typeof choice.delta.content !== 'string') {
82
+ throw failure('provider_response_invalid', 'SSE choice requires delta.content');
83
+ }
84
+ const nextFinish = choice.finish_reason;
85
+ if (nextFinish !== null && nextFinish !== 'stop' && nextFinish !== 'length') {
86
+ throw failure('provider_response_invalid', 'SSE choice has invalid finish_reason');
87
+ }
88
+ if (nextFinish === 'length') {
89
+ throw failure('provider_task_failed', 'Translation was truncated by the output limit', { finishReason: 'length' });
90
+ }
91
+ const delta = choice.delta.content;
92
+ if (finishReason && (nextFinish !== finishReason || (streamingContent === 'incremental' ? delta !== '' : delta !== text))) {
93
+ throw failure('provider_response_invalid', 'Translation changed after finish_reason');
94
+ }
95
+ finishReason = nextFinish ?? finishReason;
96
+ if (delta.length === 0)
97
+ return;
98
+ stage = 'callback';
55
99
  if (streamingContent === 'incremental') {
56
100
  text += delta;
57
101
  await callbacks.onDelta({ mode: 'append', text: delta, accumulatedText: text });
@@ -70,49 +114,44 @@ export async function readQwenMtSse(body, streamingContent, signal, callbacks) {
70
114
  await callbacks.onDelta({ mode: 'replace', text, accumulatedText: text });
71
115
  };
72
116
  try {
117
+ checkAbort();
73
118
  while (!ended) {
119
+ stage = 'read';
74
120
  const result = await reader.read();
75
- if (result.done)
121
+ checkAbort();
122
+ if (result.done) {
123
+ eof = true;
76
124
  break;
125
+ }
77
126
  pending += decoder.decode(result.value, { stream: true });
78
- const drained = drainEvents(pending);
127
+ const drained = drainSseEvents(pending);
79
128
  pending = drained.remaining;
80
129
  for (const event of drained.events) {
130
+ checkAbort();
131
+ stage = 'parse';
81
132
  await consume(event);
82
133
  if (ended)
83
134
  break;
84
135
  }
85
136
  }
86
- pending += decoder.decode();
87
- const drained = drainEvents(`${pending}\n\n`);
88
- for (const event of drained.events)
89
- await consume(event);
137
+ stage = 'completion';
138
+ checkAbort();
139
+ if (!finishReason)
140
+ throw failure('provider_response_invalid', 'Translation stream ended without finish_reason');
141
+ }
142
+ catch (error) {
143
+ if (error instanceof AiRuntimeError && error.details?.protocol === 'qwen-mt-sse')
144
+ throw error;
145
+ throw failure(stage === 'callback' ? 'capability_execution_failed' : 'provider_response_invalid', stage === 'callback' ? 'Translation event callback failed' : 'Translation stream read failed');
90
146
  }
91
147
  finally {
92
148
  signal.removeEventListener('abort', cancelReader);
149
+ if (!eof)
150
+ cancelReader();
93
151
  reader.releaseLock();
94
152
  }
95
153
  return { text, usage, requestId, responseModel, finishReason };
96
154
  }
97
- function drainEvents(input) {
98
- const events = [];
99
- let remaining = input;
100
- let hasSeparator = true;
101
- while (hasSeparator) {
102
- const crlf = remaining.indexOf('\r\n\r\n');
103
- const lf = remaining.indexOf('\n\n');
104
- if (crlf === -1 && lf === -1) {
105
- hasSeparator = false;
106
- continue;
107
- }
108
- const useCrlf = crlf !== -1 && (lf === -1 || crlf < lf);
109
- const index = useCrlf ? crlf : lf;
110
- const length = useCrlf ? 4 : 2;
111
- events.push(remaining.slice(0, index));
112
- remaining = remaining.slice(index + length);
113
- }
114
- return { events, remaining };
115
- }
116
155
  function readUsage(value) {
117
156
  if (!isRecord(value))
118
157
  return undefined;
@@ -123,23 +162,12 @@ function readUsage(value) {
123
162
  return undefined;
124
163
  return { inputTokens, outputTokens, totalTokens };
125
164
  }
126
- function throwPayloadError(payload) {
127
- const error = isRecord(payload.error) ? payload.error : undefined;
128
- const code = readNonEmptyString(error?.code) ?? readNonEmptyString(payload.code);
129
- if (!code)
130
- return;
131
- const message = readNonEmptyString(error?.message) ?? readNonEmptyString(payload.message) ?? code;
132
- throw new AiRuntimeError('provider_task_failed', `Bailian Qwen-MT ${code}: ${message}`);
133
- }
134
165
  function readTokenCount(value) {
135
166
  return typeof value === 'number' && Number.isFinite(value) && value >= 0 ? value : undefined;
136
167
  }
137
168
  function readNonEmptyString(value) {
138
169
  return typeof value === 'string' && value.length > 0 ? value : undefined;
139
170
  }
140
- function readString(value) {
141
- return typeof value === 'string' ? value : undefined;
142
- }
143
171
  function isRecord(value) {
144
172
  return typeof value === 'object' && value !== null && !Array.isArray(value);
145
173
  }
@@ -61,6 +61,8 @@ export interface CapabilityHandle<TInput, TOutput, TEvent = never> {
61
61
  execute(input: TInput, options?: CapabilityExecuteOptions<TEvent>): Promise<TOutput>;
62
62
  }
63
63
  export interface CapabilityRealtimeSessionDriver<TInput, TOutput> {
64
+ /** 后台终态:成功返回最终结果,致命错误必须拒绝;可恢复的 send 输入错误不拒绝它。旧驱动可省略。 */
65
+ readonly result?: Promise<TOutput>;
64
66
  send(input: TInput): void | Promise<void>;
65
67
  finish(): Promise<TOutput>;
66
68
  close?(): void | Promise<void>;
@@ -73,6 +75,8 @@ export interface CapabilityRealtimeModule<TStart, TInput, TEvent, TOutput> {
73
75
  export interface CapabilityRealtimeSession<TInput, TOutput> {
74
76
  readonly requestId: string;
75
77
  readonly descriptor: CapabilityDescriptor;
78
+ /** client 创建的会话始终提供;终态并释放资源后结算。不主动触发 finish。可选以兼容旧会话实现。 */
79
+ readonly result?: Promise<TOutput>;
76
80
  send(input: TInput): Promise<void>;
77
81
  finish(): Promise<TOutput>;
78
82
  close(): Promise<void>;