@codehz/ai 0.3.0 → 0.4.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/dist/index.d.mts +68 -53
- package/dist/index.mjs +671 -813
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/adapters/chat-completions.ts +150 -244
- package/src/adapters/messages.ts +171 -274
- package/src/adapters/ollama.ts +118 -205
- package/src/adapters/responses.ts +135 -226
- package/src/helpers/adapter-auxiliary.ts +1 -23
- package/src/helpers/adapter-base.ts +38 -4
- package/src/helpers/incremental-stream-parser.ts +58 -0
- package/src/helpers/index.ts +20 -7
- package/src/helpers/mapping.ts +0 -8
- package/src/helpers/provider-stream.ts +147 -0
- package/src/helpers/request-mapper.ts +30 -1
- package/src/helpers/usage-mapping.ts +36 -39
- package/src/helpers/sse-parser.ts +0 -113
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import { AdapterBase } from "../helpers/adapter-base.js";
|
|
13
|
-
import {
|
|
13
|
+
import { AIRequestError } from "../core/errors.js";
|
|
14
14
|
import {
|
|
15
15
|
textBlock,
|
|
16
16
|
messageItem,
|
|
@@ -18,13 +18,16 @@ import {
|
|
|
18
18
|
toolCallItem,
|
|
19
19
|
opaqueItem,
|
|
20
20
|
replayFromOutput,
|
|
21
|
-
blockToText,
|
|
22
|
-
contentBlocksToText,
|
|
23
21
|
} from "../helpers/mapping.js";
|
|
24
|
-
import {
|
|
25
|
-
import { assertOpaqueReplayEnvelope, providerHttpError } from "../helpers/adapter-security.js";
|
|
22
|
+
import { assertOpaqueReplayEnvelope } from "../helpers/adapter-security.js";
|
|
26
23
|
import { usageFromOpenAIResponses } from "../helpers/usage-mapping.js";
|
|
27
|
-
import {
|
|
24
|
+
import {
|
|
25
|
+
NormalizedRequestMapper,
|
|
26
|
+
createSseJsonParser,
|
|
27
|
+
openProviderJsonStream,
|
|
28
|
+
iterateProviderStreamBatches,
|
|
29
|
+
createCompletionGate,
|
|
30
|
+
} from "../helpers/index.js";
|
|
28
31
|
|
|
29
32
|
import type { NormalizedRequest, AIStreamEvent, EventFactory, OutputItem, FetchFn } from "../index.js";
|
|
30
33
|
|
|
@@ -198,9 +201,7 @@ export class ResponsesAdapter extends AdapterBase {
|
|
|
198
201
|
input.push({
|
|
199
202
|
type: "message",
|
|
200
203
|
role: item.role,
|
|
201
|
-
content:
|
|
202
|
-
mapper.ensureTextBlocks(item.content, `input message (${item.role}) content`),
|
|
203
|
-
),
|
|
204
|
+
content: mapper.textFromBlocks(item.content, `input message (${item.role}) content`),
|
|
204
205
|
});
|
|
205
206
|
}
|
|
206
207
|
break;
|
|
@@ -222,10 +223,7 @@ export class ResponsesAdapter extends AdapterBase {
|
|
|
222
223
|
break;
|
|
223
224
|
}
|
|
224
225
|
case "tool_result": {
|
|
225
|
-
const output = mapper
|
|
226
|
-
.ensureTextBlocks(item.content, `tool_result ${item.callId} content`)
|
|
227
|
-
.map(blockToText)
|
|
228
|
-
.join("\n");
|
|
226
|
+
const output = mapper.textFromBlocks(item.content, `tool_result ${item.callId} content`);
|
|
229
227
|
input.push({
|
|
230
228
|
type: "function_call_output",
|
|
231
229
|
call_id: item.callId,
|
|
@@ -265,24 +263,24 @@ export class ResponsesAdapter extends AdapterBase {
|
|
|
265
263
|
body.instructions = mapper.mapInstructions(request.instructions);
|
|
266
264
|
}
|
|
267
265
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
}
|
|
285
|
-
|
|
266
|
+
body.tools = mapper.mapToolsIfPresent(
|
|
267
|
+
request.tools,
|
|
268
|
+
(t): ResponsesTool => ({
|
|
269
|
+
type: "function",
|
|
270
|
+
name: t.name,
|
|
271
|
+
description: t.description,
|
|
272
|
+
input_schema: t.inputSchema,
|
|
273
|
+
}),
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
body.tool_choice = mapper.mapToolChoice<Exclude<ResponsesAPIRequest["tool_choice"], undefined>>(
|
|
277
|
+
request.toolChoice,
|
|
278
|
+
{
|
|
279
|
+
auto: "auto",
|
|
280
|
+
none: "none",
|
|
281
|
+
tool: (name) => ({ type: "function" as const, name }),
|
|
282
|
+
},
|
|
283
|
+
);
|
|
286
284
|
|
|
287
285
|
if (request.temperature !== undefined) body.temperature = request.temperature;
|
|
288
286
|
if (request.maxOutputTokens !== undefined) body.max_output_tokens = request.maxOutputTokens;
|
|
@@ -299,200 +297,139 @@ export class ResponsesAdapter extends AdapterBase {
|
|
|
299
297
|
request: NormalizedRequest,
|
|
300
298
|
): AsyncIterable<AIStreamEvent> {
|
|
301
299
|
const auxiliary = this.createAuxiliaryState(request);
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
});
|
|
314
|
-
} catch (err) {
|
|
315
|
-
throw new AIProviderError(err instanceof Error ? err.message : String(err), "PROVIDER_ERROR");
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
if (!response.ok) {
|
|
319
|
-
const errorBody = await response.text().catch(() => "");
|
|
320
|
-
throw providerHttpError(response.status, errorBody);
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
const reader = response.body?.getReader();
|
|
324
|
-
if (!reader) {
|
|
325
|
-
throw new AIStreamError("Response body is not readable", "STREAM_ERROR");
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
// 流式累积状态
|
|
329
|
-
const parser = new IncrementalStreamParser<ResponsesSSEEvent>(splitSSEFrames, (frame: string) => {
|
|
330
|
-
let eventType = "";
|
|
331
|
-
let dataStr = "";
|
|
332
|
-
for (const rawLine of frame.split("\n")) {
|
|
333
|
-
const line = rawLine.trim();
|
|
334
|
-
if (line.startsWith("event: ")) eventType = line.slice(7).trim();
|
|
335
|
-
else if (line.startsWith("data: ")) dataStr += line.slice(6);
|
|
336
|
-
}
|
|
337
|
-
if (!eventType) return { status: "ignored" };
|
|
338
|
-
try {
|
|
339
|
-
const data = JSON.parse(dataStr);
|
|
340
|
-
return { status: "parsed", value: { type: eventType, data } as ResponsesSSEEvent };
|
|
341
|
-
} catch {
|
|
342
|
-
return { status: "malformed" };
|
|
343
|
-
}
|
|
300
|
+
const gate = createCompletionGate();
|
|
301
|
+
|
|
302
|
+
const { reader } = await openProviderJsonStream({
|
|
303
|
+
fetchFn: this.fetchFn,
|
|
304
|
+
url: `${this.baseUrl}/responses`,
|
|
305
|
+
headers: {
|
|
306
|
+
"Content-Type": "application/json",
|
|
307
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
308
|
+
},
|
|
309
|
+
body: providerRequest,
|
|
310
|
+
signal: request.signal,
|
|
344
311
|
});
|
|
345
312
|
|
|
313
|
+
const parser = createSseJsonParser<ResponsesSSEEvent>();
|
|
346
314
|
const output: OutputItem[] = [];
|
|
347
|
-
let streamDone = false;
|
|
348
315
|
let completedResponse: ResponsesAPIResponse | undefined;
|
|
349
|
-
let completedEmitted = false;
|
|
350
316
|
let unknownEventsWarned = false;
|
|
351
317
|
const messageItemsWithDelta = new Set<string>();
|
|
352
318
|
const toolCallNames = new Map<string, string>();
|
|
353
319
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
});
|
|
370
|
-
if (malformedWarning) {
|
|
371
|
-
yield malformedWarning;
|
|
320
|
+
for await (const batch of iterateProviderStreamBatches({
|
|
321
|
+
reader,
|
|
322
|
+
parser,
|
|
323
|
+
factory,
|
|
324
|
+
providerLabel: "Responses",
|
|
325
|
+
transportLabel: "SSE event(s)",
|
|
326
|
+
incompleteMessage: "Stream ended with an incomplete Responses SSE frame",
|
|
327
|
+
})) {
|
|
328
|
+
for (const warning of batch.warnings) yield warning;
|
|
329
|
+
|
|
330
|
+
for (const sseEvent of batch.items) {
|
|
331
|
+
if (sseEvent.type === "error") {
|
|
332
|
+
const data = sseEvent.data as { message?: string; code?: string };
|
|
333
|
+
yield factory.responseWarning(data.message ?? "Provider error event", data.code);
|
|
334
|
+
continue;
|
|
372
335
|
}
|
|
373
336
|
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
case "reasoning":
|
|
389
|
-
yield factory.reasoningStarted(item.id, "full");
|
|
390
|
-
break;
|
|
391
|
-
case "function_call": {
|
|
392
|
-
const name = typeof item.name === "string" ? item.name : "unknown";
|
|
393
|
-
toolCallNames.set(item.id, name);
|
|
394
|
-
yield factory.toolCallStarted(item.id, name);
|
|
395
|
-
break;
|
|
396
|
-
}
|
|
337
|
+
if (sseEvent.type === "response.output_item.added") {
|
|
338
|
+
const item = (sseEvent.data as { item: { id: string; type: string; [key: string]: unknown } }).item;
|
|
339
|
+
switch (item.type) {
|
|
340
|
+
case "message":
|
|
341
|
+
yield factory.messageStarted(item.id);
|
|
342
|
+
break;
|
|
343
|
+
case "reasoning":
|
|
344
|
+
yield factory.reasoningStarted(item.id, "full");
|
|
345
|
+
break;
|
|
346
|
+
case "function_call": {
|
|
347
|
+
const name = typeof item.name === "string" ? item.name : "unknown";
|
|
348
|
+
toolCallNames.set(item.id, name);
|
|
349
|
+
yield factory.toolCallStarted(item.id, name);
|
|
350
|
+
break;
|
|
397
351
|
}
|
|
398
|
-
continue;
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
if (sseEvent.type === "response.output_text.delta") {
|
|
402
|
-
const data = sseEvent.data as { item_id: string; delta: string };
|
|
403
|
-
yield factory.messageDelta(data.item_id, textBlock(data.delta));
|
|
404
|
-
messageItemsWithDelta.add(data.item_id);
|
|
405
|
-
continue;
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
if (sseEvent.type === "response.output_text.done") {
|
|
409
|
-
const data = sseEvent.data as { item_id: string; text: string };
|
|
410
|
-
if (!messageItemsWithDelta.has(data.item_id) && data.text) {
|
|
411
|
-
yield factory.messageDelta(data.item_id, textBlock(data.text));
|
|
412
|
-
}
|
|
413
|
-
yield factory.messageCompleted(data.item_id);
|
|
414
|
-
output.push(messageItem([textBlock(data.text)], { id: data.item_id }));
|
|
415
|
-
continue;
|
|
416
352
|
}
|
|
353
|
+
continue;
|
|
354
|
+
}
|
|
417
355
|
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
356
|
+
if (sseEvent.type === "response.output_text.delta") {
|
|
357
|
+
const data = sseEvent.data as { item_id: string; delta: string };
|
|
358
|
+
yield factory.messageDelta(data.item_id, textBlock(data.delta));
|
|
359
|
+
messageItemsWithDelta.add(data.item_id);
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
423
362
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
continue;
|
|
363
|
+
if (sseEvent.type === "response.output_text.done") {
|
|
364
|
+
const data = sseEvent.data as { item_id: string; text: string };
|
|
365
|
+
if (!messageItemsWithDelta.has(data.item_id) && data.text) {
|
|
366
|
+
yield factory.messageDelta(data.item_id, textBlock(data.text));
|
|
429
367
|
}
|
|
368
|
+
yield factory.messageCompleted(data.item_id);
|
|
369
|
+
output.push(messageItem([textBlock(data.text)], { id: data.item_id }));
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
430
372
|
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
373
|
+
if (sseEvent.type === "response.reasoning.delta") {
|
|
374
|
+
const data = sseEvent.data as { item_id: string; delta: string };
|
|
375
|
+
yield factory.reasoningDelta(data.item_id, textBlock(data.delta));
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
436
378
|
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
}
|
|
379
|
+
if (sseEvent.type === "response.reasoning.done") {
|
|
380
|
+
const data = sseEvent.data as { item_id: string; text: string };
|
|
381
|
+
yield factory.reasoningCompleted(data.item_id);
|
|
382
|
+
output.push(reasoningItem([textBlock(data.text)], "full", data.item_id));
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
444
385
|
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
const data = sseEvent.data as { response: ResponsesAPIResponse };
|
|
451
|
-
if (completedResponse) {
|
|
452
|
-
yield factory.responseWarning("Duplicate finish signal ignored", "DUPLICATE_FINISH");
|
|
453
|
-
continue;
|
|
454
|
-
}
|
|
386
|
+
if (sseEvent.type === "response.function_call_arguments.delta") {
|
|
387
|
+
const data = sseEvent.data as { item_id: string; delta: string };
|
|
388
|
+
if (data.delta) yield factory.toolCallDelta(data.item_id, { argumentsText: data.delta });
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
455
391
|
|
|
456
|
-
|
|
392
|
+
if (sseEvent.type === "response.function_call_arguments.done") {
|
|
393
|
+
const data = sseEvent.data as { item_id: string; arguments: string };
|
|
394
|
+
const tcItem = toolCallItem(data.item_id, toolCallNames.get(data.item_id) ?? "unknown", data.arguments);
|
|
395
|
+
yield factory.toolCallCompleted(data.item_id);
|
|
396
|
+
output.push(tcItem);
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
457
399
|
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
400
|
+
if (
|
|
401
|
+
sseEvent.type === "response.completed" ||
|
|
402
|
+
sseEvent.type === "response.failed" ||
|
|
403
|
+
sseEvent.type === "response.incomplete"
|
|
404
|
+
) {
|
|
405
|
+
const data = sseEvent.data as { response: ResponsesAPIResponse };
|
|
406
|
+
if (completedResponse) {
|
|
407
|
+
yield factory.responseWarning("Duplicate finish signal ignored", "DUPLICATE_FINISH");
|
|
464
408
|
continue;
|
|
465
409
|
}
|
|
466
410
|
|
|
467
|
-
|
|
468
|
-
|
|
411
|
+
completedResponse = data.response;
|
|
412
|
+
|
|
413
|
+
if (sseEvent.type === "response.failed") {
|
|
469
414
|
yield factory.responseWarning(
|
|
470
|
-
`
|
|
471
|
-
"
|
|
415
|
+
`Response failed: ${extractFailureMessage(data.response)}`,
|
|
416
|
+
"PROVIDER_FAILURE",
|
|
472
417
|
);
|
|
473
418
|
}
|
|
419
|
+
continue;
|
|
474
420
|
}
|
|
475
421
|
|
|
476
|
-
if (
|
|
477
|
-
|
|
478
|
-
|
|
422
|
+
if (!KNOWN_RESPONSES_SSE_TYPES.has(sseEvent.type) && !unknownEventsWarned) {
|
|
423
|
+
unknownEventsWarned = true;
|
|
424
|
+
yield factory.responseWarning(
|
|
425
|
+
`Responses API sent unknown event type "${sseEvent.type}"; this may indicate an incomplete integration`,
|
|
426
|
+
"UNKNOWN_PROVIDER_EVENT",
|
|
427
|
+
);
|
|
479
428
|
}
|
|
480
429
|
}
|
|
481
|
-
} finally {
|
|
482
|
-
try {
|
|
483
|
-
if (!streamDone) await reader.cancel().catch(() => undefined);
|
|
484
|
-
} finally {
|
|
485
|
-
reader.releaseLock();
|
|
486
|
-
}
|
|
487
430
|
}
|
|
488
431
|
|
|
489
|
-
if (parser.getRemaining().trim().length > 0) {
|
|
490
|
-
yield factory.responseWarning("Stream ended with an incomplete Responses SSE frame", "STREAM_ERROR");
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
// 解析完成响应中的 usage 和 replay
|
|
494
432
|
let rawResponseId: string | undefined;
|
|
495
|
-
|
|
496
433
|
if (completedResponse) {
|
|
497
434
|
rawResponseId = completedResponse.id;
|
|
498
435
|
if (completedResponse.usage) {
|
|
@@ -500,47 +437,19 @@ export class ResponsesAdapter extends AdapterBase {
|
|
|
500
437
|
}
|
|
501
438
|
}
|
|
502
439
|
|
|
503
|
-
// 构造 replay:在 output 基础上追加 opaque continuation
|
|
504
440
|
const replay = [...replayFromOutput(output)];
|
|
505
|
-
|
|
506
|
-
// 如果有 provider continuation id,附加 opaque replay item
|
|
507
441
|
if (completedResponse?.id) {
|
|
508
442
|
replay.push(opaqueItem("responses", "replay", { id: completedResponse.id }));
|
|
509
443
|
}
|
|
510
444
|
|
|
511
|
-
// 从 completedResponse 推断 stop reason
|
|
512
445
|
const stopReason = completedResponse ? this.inferStopReason(completedResponse) : undefined;
|
|
513
446
|
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
completedEmitted = true;
|
|
521
|
-
const finalResponse = this.buildResponse(
|
|
522
|
-
request,
|
|
523
|
-
{
|
|
524
|
-
output,
|
|
525
|
-
replay,
|
|
526
|
-
stopReason,
|
|
527
|
-
usage: auxiliaryResult.usage,
|
|
528
|
-
billing: auxiliaryResult.billing,
|
|
529
|
-
auxiliary: auxiliaryResult.auxiliary,
|
|
530
|
-
warnings: auxiliaryResult.warnings,
|
|
531
|
-
metadataSources: auxiliaryResult.metadataSources,
|
|
532
|
-
rawResponseId,
|
|
533
|
-
},
|
|
534
|
-
factory,
|
|
535
|
-
);
|
|
536
|
-
yield factory.responseCompleted({
|
|
537
|
-
replay: finalResponse.replay,
|
|
538
|
-
stopReason: finalResponse.stopReason,
|
|
539
|
-
trace: finalResponse.backend,
|
|
540
|
-
usage: finalResponse.usage,
|
|
541
|
-
billing: finalResponse.billing,
|
|
542
|
-
auxiliary: finalResponse.auxiliary,
|
|
543
|
-
warnings: finalResponse.warnings,
|
|
447
|
+
if (gate.tryComplete()) {
|
|
448
|
+
yield* this.emitStreamCompleted(factory, request, auxiliary, {
|
|
449
|
+
output,
|
|
450
|
+
replay,
|
|
451
|
+
stopReason,
|
|
452
|
+
rawResponseId,
|
|
544
453
|
});
|
|
545
454
|
}
|
|
546
455
|
}
|
|
@@ -1,13 +1,6 @@
|
|
|
1
1
|
import { WarningCode } from "../core/errors.js";
|
|
2
2
|
import type { EventFactory } from "../core/event-factory.js";
|
|
3
|
-
import type {
|
|
4
|
-
AIStreamEvent,
|
|
5
|
-
BillingInfo,
|
|
6
|
-
NormalizedRequest,
|
|
7
|
-
Usage,
|
|
8
|
-
AuxiliaryInfo,
|
|
9
|
-
BackendTrace,
|
|
10
|
-
} from "../types/index.js";
|
|
3
|
+
import type { AIStreamEvent, BillingInfo, NormalizedRequest, Usage, AuxiliaryInfo } from "../types/index.js";
|
|
11
4
|
import { AuxiliaryCollector, type BillingSource, type LookupResult, type UsageSource } from "./auxiliary-collector.js";
|
|
12
5
|
|
|
13
6
|
type MaybePromise<T> = T | Promise<T>;
|
|
@@ -157,21 +150,6 @@ export function emitMalformedStreamWarning(
|
|
|
157
150
|
);
|
|
158
151
|
}
|
|
159
152
|
|
|
160
|
-
export function metadataSourceList(
|
|
161
|
-
...groups: Array<Array<NonNullable<BackendTrace["metadataSources"]>[number]> | undefined>
|
|
162
|
-
): string[] | undefined {
|
|
163
|
-
const sources = new Set<string>();
|
|
164
|
-
|
|
165
|
-
for (const group of groups) {
|
|
166
|
-
if (!group) continue;
|
|
167
|
-
for (const source of group) {
|
|
168
|
-
sources.add(source);
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
return sources.size > 0 ? [...sources] : undefined;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
153
|
function isEmptyRecord(value: object): boolean {
|
|
176
154
|
return Object.keys(value).length === 0;
|
|
177
155
|
}
|
|
@@ -129,7 +129,7 @@ export abstract class AdapterBase implements BackendAdapter {
|
|
|
129
129
|
* 子类可在返回前自定义覆盖。
|
|
130
130
|
*/
|
|
131
131
|
protected buildResponse(request: NormalizedRequest, result: StreamResult, _factory: EventFactory): AIResponse {
|
|
132
|
-
const text =
|
|
132
|
+
const text = extractText(result.output);
|
|
133
133
|
const warnings = mergeWarnings(result.warnings, _factory.warnings);
|
|
134
134
|
const auxiliary = mergeAuxiliary(
|
|
135
135
|
result.auxiliary,
|
|
@@ -158,9 +158,43 @@ export abstract class AdapterBase implements BackendAdapter {
|
|
|
158
158
|
};
|
|
159
159
|
}
|
|
160
160
|
|
|
161
|
-
/**
|
|
162
|
-
|
|
163
|
-
|
|
161
|
+
/**
|
|
162
|
+
* 统一 finalize auxiliary → response.completed。
|
|
163
|
+
* adapter 在调用前组装 output / replay / stopReason 等业务字段。
|
|
164
|
+
*/
|
|
165
|
+
protected async *emitStreamCompleted(
|
|
166
|
+
factory: EventFactory,
|
|
167
|
+
request: NormalizedRequest,
|
|
168
|
+
auxiliary: AdapterAuxiliaryState,
|
|
169
|
+
result: StreamResult,
|
|
170
|
+
): AsyncIterable<AIStreamEvent> {
|
|
171
|
+
const auxiliaryResult = await auxiliary.finalize(factory);
|
|
172
|
+
for (const event of auxiliaryResult.events) {
|
|
173
|
+
yield event;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const finalResponse = this.buildResponse(
|
|
177
|
+
request,
|
|
178
|
+
{
|
|
179
|
+
...result,
|
|
180
|
+
usage: result.usage ?? auxiliaryResult.usage,
|
|
181
|
+
billing: result.billing ?? auxiliaryResult.billing,
|
|
182
|
+
auxiliary: mergeAuxiliary(result.auxiliary, auxiliaryResult.auxiliary),
|
|
183
|
+
warnings: mergeWarnings(result.warnings, auxiliaryResult.warnings),
|
|
184
|
+
metadataSources: result.metadataSources ?? auxiliaryResult.metadataSources,
|
|
185
|
+
},
|
|
186
|
+
factory,
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
yield factory.responseCompleted({
|
|
190
|
+
replay: finalResponse.replay,
|
|
191
|
+
stopReason: finalResponse.stopReason,
|
|
192
|
+
trace: finalResponse.backend,
|
|
193
|
+
usage: finalResponse.usage,
|
|
194
|
+
billing: finalResponse.billing,
|
|
195
|
+
auxiliary: finalResponse.auxiliary,
|
|
196
|
+
warnings: finalResponse.warnings,
|
|
197
|
+
});
|
|
164
198
|
}
|
|
165
199
|
|
|
166
200
|
protected createAuxiliaryState(request: NormalizedRequest): AdapterAuxiliaryState {
|
|
@@ -82,3 +82,61 @@ export function splitSSEFrames(buffer: string, allowEOF: boolean): StreamSplitRe
|
|
|
82
82
|
|
|
83
83
|
return { items, rest: normalized.slice(cursor) };
|
|
84
84
|
}
|
|
85
|
+
|
|
86
|
+
// ── 常用 parse 工厂 ───────────────────────────────────────────
|
|
87
|
+
|
|
88
|
+
export type SseJsonEvent = { type: string; data: unknown };
|
|
89
|
+
|
|
90
|
+
/** 解析标准 SSE frame(event: + data:),用于 Messages / Responses。 */
|
|
91
|
+
export function parseSseJsonFrame(frame: string): StreamParseResult<SseJsonEvent> {
|
|
92
|
+
let eventType = "";
|
|
93
|
+
let dataStr = "";
|
|
94
|
+
for (const rawLine of frame.split("\n")) {
|
|
95
|
+
const line = rawLine.trim();
|
|
96
|
+
if (line.startsWith("event: ")) eventType = line.slice(7).trim();
|
|
97
|
+
else if (line.startsWith("data: ")) dataStr += line.slice(6);
|
|
98
|
+
}
|
|
99
|
+
if (!eventType) return { status: "ignored" };
|
|
100
|
+
try {
|
|
101
|
+
const data: unknown = JSON.parse(dataStr);
|
|
102
|
+
return { status: "parsed", value: { type: eventType, data } };
|
|
103
|
+
} catch {
|
|
104
|
+
return { status: "malformed" };
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function createSseJsonParser<T extends SseJsonEvent = SseJsonEvent>(): IncrementalStreamParser<T> {
|
|
109
|
+
return new IncrementalStreamParser(splitSSEFrames, (frame) => parseSseJsonFrame(frame) as StreamParseResult<T>);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** OpenAI Chat Completions 简化 SSE:仅 `data: ...` 行,忽略 `[DONE]`。 */
|
|
113
|
+
export function parseChatCompletionsDataLine(item: string): StreamParseResult<unknown> {
|
|
114
|
+
const trimmed = item.trim();
|
|
115
|
+
if (!trimmed.startsWith("data: ")) return { status: "ignored" };
|
|
116
|
+
const data = trimmed.slice(6).trim();
|
|
117
|
+
if (data === "[DONE]") return { status: "ignored" };
|
|
118
|
+
try {
|
|
119
|
+
return { status: "parsed", value: JSON.parse(data) as unknown };
|
|
120
|
+
} catch {
|
|
121
|
+
return { status: "malformed" };
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function createChatCompletionsSseParser<T>(): IncrementalStreamParser<T> {
|
|
126
|
+
return new IncrementalStreamParser(splitLines, (item) => parseChatCompletionsDataLine(item) as StreamParseResult<T>);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** NDJSON 行解析(Ollama 等):空行忽略,JSON 失败为 malformed。 */
|
|
130
|
+
export function createNdjsonLineParser<T>(isValid: (value: unknown) => value is T): IncrementalStreamParser<T> {
|
|
131
|
+
return new IncrementalStreamParser<T>(splitLines, (item: string): StreamParseResult<T> => {
|
|
132
|
+
const trimmed = item.trim();
|
|
133
|
+
if (!trimmed) return { status: "ignored" };
|
|
134
|
+
try {
|
|
135
|
+
const parsed: unknown = JSON.parse(trimmed);
|
|
136
|
+
if (isValid(parsed)) return { status: "parsed", value: parsed };
|
|
137
|
+
return { status: "malformed" };
|
|
138
|
+
} catch {
|
|
139
|
+
return { status: "malformed" };
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
}
|
package/src/helpers/index.ts
CHANGED
|
@@ -13,7 +13,6 @@ export {
|
|
|
13
13
|
opaqueBlock,
|
|
14
14
|
blockToText,
|
|
15
15
|
contentBlocksToText,
|
|
16
|
-
instructionsToText,
|
|
17
16
|
extractText,
|
|
18
17
|
messageItem,
|
|
19
18
|
reasoningItem,
|
|
@@ -23,12 +22,9 @@ export {
|
|
|
23
22
|
replayFromOutput,
|
|
24
23
|
} from "./mapping.js";
|
|
25
24
|
|
|
26
|
-
export { parseSSEEvents } from "./sse-parser.js";
|
|
27
|
-
export type { SSEEvent } from "./sse-parser.js";
|
|
28
|
-
|
|
29
25
|
export { AdapterBase } from "./adapter-base.js";
|
|
30
26
|
export type { StreamResult } from "./adapter-base.js";
|
|
31
|
-
export { AdapterAuxiliaryState, emitMalformedStreamWarning
|
|
27
|
+
export { AdapterAuxiliaryState, emitMalformedStreamWarning } from "./adapter-auxiliary.js";
|
|
32
28
|
export type { AuxiliaryFinalizeOptions, AuxiliaryFinalizeResult, BillingPostprocessHook } from "./adapter-auxiliary.js";
|
|
33
29
|
export { syntheticStream } from "./synthetic-stream.js";
|
|
34
30
|
export type { SyntheticStreamOptions } from "./synthetic-stream.js";
|
|
@@ -54,7 +50,24 @@ export {
|
|
|
54
50
|
} from "./adapter-security.js";
|
|
55
51
|
export type { OpaqueEnvelopeResult } from "./adapter-security.js";
|
|
56
52
|
|
|
57
|
-
export {
|
|
58
|
-
|
|
53
|
+
export {
|
|
54
|
+
IncrementalStreamParser,
|
|
55
|
+
splitLines,
|
|
56
|
+
splitSSEFrames,
|
|
57
|
+
parseSseJsonFrame,
|
|
58
|
+
createSseJsonParser,
|
|
59
|
+
parseChatCompletionsDataLine,
|
|
60
|
+
createChatCompletionsSseParser,
|
|
61
|
+
createNdjsonLineParser,
|
|
62
|
+
} from "./incremental-stream-parser.js";
|
|
63
|
+
export type { StreamSplitResult, StreamParseResult, SseJsonEvent } from "./incremental-stream-parser.js";
|
|
64
|
+
|
|
65
|
+
export { openProviderJsonStream, iterateProviderStreamBatches, createCompletionGate } from "./provider-stream.js";
|
|
66
|
+
export type {
|
|
67
|
+
OpenProviderJsonStreamOptions,
|
|
68
|
+
OpenedProviderStream,
|
|
69
|
+
ProviderStreamBatch,
|
|
70
|
+
ProviderStreamBatchOptions,
|
|
71
|
+
} from "./provider-stream.js";
|
|
59
72
|
|
|
60
73
|
export { NormalizedRequestMapper } from "./request-mapper.js";
|