@henjicc/ai-sdk 0.6.1 → 0.6.2
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/CHANGELOG.md +8 -0
- package/README.md +16 -3
- package/dist/capabilities/client.js +103 -86
- package/dist/capabilities/speech-recognition/bailian/realtime/module.js +42 -7
- package/dist/capabilities/speech-recognition/bailian/realtime/protocol.js +57 -15
- package/dist/capabilities/speech-recognition/volcengine/realtime/module.js +42 -10
- package/dist/capabilities/translation/bailian/module.js +41 -13
- package/dist/capabilities/translation/bailian/sse.d.ts +1 -1
- package/dist/capabilities/translation/bailian/sse.js +82 -54
- package/dist/capabilities/types.d.ts +4 -0
- package/dist/llm/chat.js +6 -1
- package/dist/llm/streaming.js +131 -91
- package/dist/protocols/sse-events.d.ts +4 -0
- package/dist/protocols/sse-events.js +31 -0
- package/docs/consumers.md +119 -107
- package/docs/llm-adaptation/README.md +26 -1
- package/docs/llm-adaptation//344/276/233/345/272/224/345/225/206//346/231/272/350/260/261GLM.md +3 -1
- package/docs/model-adaptation/Fun-ASR-Realtime/Fun-ASR-Realtime_/347/231/276/347/202/274.md +22 -2
- package/docs/model-adaptation/Qwen-MT-Flash/Qwen-MT-Flash_/347/231/276/347/202/274.md +24 -3
- package/docs/model-adaptation/Qwen-MT-Lite/Qwen-MT-Lite_/347/231/276/347/202/274.md +3 -2
- package/docs/model-adaptation/Qwen-MT-Plus/Qwen-MT-Plus_/347/231/276/347/202/274.md +3 -2
- package/docs/model-adaptation/Qwen3-ASR-Flash-Realtime/Qwen3-ASR-Flash-Realtime_/347/231/276/347/202/274.md +25 -2
- 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 +7 -3
- package/examples/llm-chat/README.md +1 -1
- package/examples/llm-chat/index.ts +1 -1
- package/examples/llm-chat/package.json +1 -1
- package/package.json +1 -1
|
@@ -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
|
|
263
|
+
const original = error instanceof AiRuntimeError
|
|
252
264
|
? error
|
|
253
|
-
: new AiRuntimeError('provider_realtime_error',
|
|
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',
|
|
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
|
-
|
|
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(
|
|
113
|
-
throw
|
|
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 =
|
|
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 ${
|
|
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
|
-
|
|
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
|
|
217
|
-
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
|
20
|
-
|
|
21
|
-
|
|
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
|
|
51
|
+
throw failure('provider_response_invalid', 'SSE contains invalid JSON');
|
|
36
52
|
}
|
|
37
53
|
if (!isRecord(payload)) {
|
|
38
|
-
throw
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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>;
|
package/dist/llm/chat.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { preprocessRequestBody } from '../upload/preprocess.js';
|
|
2
2
|
import { normalizeProviderError } from '../runtime/error-classify.js';
|
|
3
|
+
import { AiRuntimeError } from '../runtime/AiRuntimeError.js';
|
|
3
4
|
import { cancelTask, clearCancelFlag, isCancelled, registerAbortController, } from '../runtime/task-registry.js';
|
|
4
5
|
import { buildOpenAiCompatiblePayload, resolveOpenAiCompatibleEndpoint, resolvePpioChatEndpoint, streamOpenAiCompatibleChat, } from './streaming.js';
|
|
5
6
|
import { countLlmInputChars } from './message-metrics.js';
|
|
@@ -109,11 +110,15 @@ export async function runLlmChatStream(request, taskId, emit, runtime, hooks = {
|
|
|
109
110
|
})
|
|
110
111
|
: error;
|
|
111
112
|
span?.end(normalizedInput);
|
|
112
|
-
|
|
113
|
+
const normalized = new Error(normalizeLlmChatError(taskId, normalizedInput, {
|
|
113
114
|
providerId: request.providerId,
|
|
114
115
|
modelId: request.modelId,
|
|
115
116
|
requestId: taskId,
|
|
116
117
|
}));
|
|
118
|
+
if (error instanceof AiRuntimeError && error.details?.protocol === 'openai-chat-sse') {
|
|
119
|
+
Object.assign(normalized, { details: error.details });
|
|
120
|
+
}
|
|
121
|
+
throw normalized;
|
|
117
122
|
}
|
|
118
123
|
finally {
|
|
119
124
|
execution.signal?.removeEventListener('abort', forwardAbort);
|