@codehz/ai 0.2.4 → 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.
@@ -10,7 +10,7 @@
10
10
  */
11
11
 
12
12
  import { AdapterBase } from "../helpers/adapter-base.js";
13
- import { AIProviderError, AIRequestError, AIStreamError } from "../core/errors.js";
13
+ import { AIRequestError } from "../core/errors.js";
14
14
  import {
15
15
  textBlock,
16
16
  messageItem,
@@ -18,14 +18,16 @@ import {
18
18
  toolCallItem,
19
19
  opaqueItem,
20
20
  replayFromOutput,
21
- blockToText,
22
- contentBlocksToText,
23
21
  } from "../helpers/mapping.js";
24
- import { emitMalformedStreamWarning } from "../helpers/adapter-auxiliary.js";
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 { NormalizedRequestMapper, splitSSEFrames, IncrementalStreamParser } from "../helpers/index.js";
28
- import type { ProviderProfile } from "../helpers/index.js";
24
+ import {
25
+ NormalizedRequestMapper,
26
+ createSseJsonParser,
27
+ openProviderJsonStream,
28
+ iterateProviderStreamBatches,
29
+ createCompletionGate,
30
+ } from "../helpers/index.js";
29
31
 
30
32
  import type { NormalizedRequest, AIStreamEvent, EventFactory, OutputItem, FetchFn } from "../index.js";
31
33
 
@@ -72,24 +74,7 @@ type ResponsesTool = {
72
74
  input_schema: Record<string, unknown>;
73
75
  };
74
76
 
75
- // ── ProviderProfile & Mapper ────────────────────────────────────
76
-
77
- const profile: ProviderProfile = {
78
- kind: "responses",
79
- instructionsMode: "instructions_field",
80
- supportedBlockTypes: ["text", "json"] as const,
81
- reasoningBlockTypes: ["text"] as const,
82
- capabilities: {
83
- textStreaming: "native",
84
- reasoningStreaming: "native",
85
- toolCallStreaming: "native",
86
- replay: "opaque",
87
- usage: "final",
88
- toolResultOutcomes: ["success"],
89
- },
90
- };
91
-
92
- const mapper = new NormalizedRequestMapper(profile);
77
+ const mapper = new NormalizedRequestMapper("responses");
93
78
 
94
79
  // ── SSE 事件类型 ──────────────────────────────────────────────
95
80
 
@@ -99,8 +84,8 @@ type ResponsesSSEEvent =
99
84
  | { type: "response.output_text.done"; data: { item_id: string; text: string } }
100
85
  | { type: "response.reasoning.delta"; data: { item_id: string; delta: string } }
101
86
  | { type: "response.reasoning.done"; data: { item_id: string; text: string } }
102
- | { type: "response.tool_call.delta"; data: { item_id: string; delta: { arguments?: string } } }
103
- | { type: "response.tool_call.done"; data: { item_id: string; arguments?: string; name?: string } }
87
+ | { type: "response.function_call_arguments.delta"; data: { item_id: string; delta: string } }
88
+ | { type: "response.function_call_arguments.done"; data: { item_id: string; arguments: string } }
104
89
  | { type: "response.completed"; data: { response: ResponsesAPIResponse } }
105
90
  | { type: "response.failed"; data: { response: ResponsesAPIResponse } }
106
91
  | { type: "response.incomplete"; data: { response: ResponsesAPIResponse } }
@@ -115,8 +100,6 @@ const KNOWN_RESPONSES_SSE_TYPES = new Set([
115
100
  "response.output_text.done",
116
101
  "response.reasoning.delta",
117
102
  "response.reasoning.done",
118
- "response.tool_call.delta",
119
- "response.tool_call.done",
120
103
  "response.function_call_arguments.delta",
121
104
  "response.function_call_arguments.done",
122
105
  "response.content_part.added",
@@ -187,7 +170,7 @@ function canonicalToResponsesBlock(b: import("../index.js").ContentBlock): Respo
187
170
 
188
171
  export class ResponsesAdapter extends AdapterBase {
189
172
  readonly kind = "responses" as const;
190
- readonly capabilities = profile.capabilities;
173
+ readonly isSyntheticStream = false;
191
174
 
192
175
  private apiKey: string;
193
176
  private baseUrl: string;
@@ -218,9 +201,7 @@ export class ResponsesAdapter extends AdapterBase {
218
201
  input.push({
219
202
  type: "message",
220
203
  role: item.role,
221
- content: contentBlocksToText(
222
- mapper.ensureTextBlocks(item.content, `input message (${item.role}) content`),
223
- ),
204
+ content: mapper.textFromBlocks(item.content, `input message (${item.role}) content`),
224
205
  });
225
206
  }
226
207
  break;
@@ -242,11 +223,7 @@ export class ResponsesAdapter extends AdapterBase {
242
223
  break;
243
224
  }
244
225
  case "tool_result": {
245
- mapper.assertToolResultOutcome(item.outcome);
246
- const output = mapper
247
- .ensureTextBlocks(item.content, `tool_result ${item.callId} content`)
248
- .map(blockToText)
249
- .join("\n");
226
+ const output = mapper.textFromBlocks(item.content, `tool_result ${item.callId} content`);
250
227
  input.push({
251
228
  type: "function_call_output",
252
229
  call_id: item.callId,
@@ -286,24 +263,24 @@ export class ResponsesAdapter extends AdapterBase {
286
263
  body.instructions = mapper.mapInstructions(request.instructions);
287
264
  }
288
265
 
289
- if (request.tools && request.tools.length > 0) {
290
- body.tools = request.tools.map(
291
- (t): ResponsesTool => ({
292
- type: "function",
293
- name: t.name,
294
- description: t.description,
295
- input_schema: t.inputSchema,
296
- }),
297
- );
298
- }
299
-
300
- if (request.toolChoice) {
301
- if (request.toolChoice === "auto") body.tool_choice = "auto";
302
- else if (request.toolChoice === "none") body.tool_choice = "none";
303
- else if (request.toolChoice.type === "tool") {
304
- body.tool_choice = { type: "function", name: request.toolChoice.name };
305
- }
306
- }
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
+ );
307
284
 
308
285
  if (request.temperature !== undefined) body.temperature = request.temperature;
309
286
  if (request.maxOutputTokens !== undefined) body.max_output_tokens = request.maxOutputTokens;
@@ -320,198 +297,139 @@ export class ResponsesAdapter extends AdapterBase {
320
297
  request: NormalizedRequest,
321
298
  ): AsyncIterable<AIStreamEvent> {
322
299
  const auxiliary = this.createAuxiliaryState(request);
323
- let response: Response;
324
-
325
- try {
326
- response = await this.fetchFn(`${this.baseUrl}/responses`, {
327
- method: "POST",
328
- headers: {
329
- "Content-Type": "application/json",
330
- Authorization: `Bearer ${this.apiKey}`,
331
- },
332
- body: JSON.stringify(providerRequest),
333
- signal: request.signal,
334
- });
335
- } catch (err) {
336
- throw new AIProviderError(err instanceof Error ? err.message : String(err), "PROVIDER_ERROR");
337
- }
338
-
339
- if (!response.ok) {
340
- const errorBody = await response.text().catch(() => "");
341
- throw providerHttpError(response.status, errorBody);
342
- }
343
-
344
- const reader = response.body?.getReader();
345
- if (!reader) {
346
- throw new AIStreamError("Response body is not readable", "STREAM_ERROR");
347
- }
348
-
349
- // 流式累积状态
350
- const parser = new IncrementalStreamParser<ResponsesSSEEvent>(splitSSEFrames, (frame: string) => {
351
- let eventType = "";
352
- let dataStr = "";
353
- for (const rawLine of frame.split("\n")) {
354
- const line = rawLine.trim();
355
- if (line.startsWith("event: ")) eventType = line.slice(7).trim();
356
- else if (line.startsWith("data: ")) dataStr += line.slice(6);
357
- }
358
- if (!eventType) return { status: "ignored" };
359
- try {
360
- const data = JSON.parse(dataStr);
361
- return { status: "parsed", value: { type: eventType, data } as ResponsesSSEEvent };
362
- } catch {
363
- return { status: "malformed" };
364
- }
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,
365
311
  });
366
312
 
313
+ const parser = createSseJsonParser<ResponsesSSEEvent>();
367
314
  const output: OutputItem[] = [];
368
- let streamDone = false;
369
315
  let completedResponse: ResponsesAPIResponse | undefined;
370
- let completedEmitted = false;
371
316
  let unknownEventsWarned = false;
372
317
  const messageItemsWithDelta = new Set<string>();
373
-
374
- try {
375
- while (true) {
376
- const readResult = await reader.read().catch((err: unknown) => {
377
- throw new AIStreamError(
378
- `Failed to read response stream: ${err instanceof Error ? err.message : String(err)}`,
379
- "STREAM_ERROR",
380
- );
381
- });
382
- const { done, value } = readResult;
383
- const { items: events, malformed: malformedEvents } = done ? parser.flush() : parser.feed(value as Uint8Array);
384
-
385
- const malformedWarning = emitMalformedStreamWarning(factory, {
386
- count: malformedEvents,
387
- providerLabel: "Responses",
388
- transportLabel: "SSE event(s)",
389
- });
390
- if (malformedWarning) {
391
- yield malformedWarning;
318
+ const toolCallNames = new Map<string, string>();
319
+
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;
392
335
  }
393
336
 
394
- for (const sseEvent of events) {
395
- if (sseEvent.type === "error") {
396
- const data = sseEvent.data as { message?: string; code?: string };
397
- yield factory.responseWarning(data.message ?? "Provider error event", data.code);
398
- continue;
399
- }
400
-
401
- // item 级事件
402
- if (sseEvent.type === "response.output_item.added") {
403
- const item = (sseEvent.data as { item: { id: string; type: string; [key: string]: unknown } }).item;
404
- switch (item.type) {
405
- case "message":
406
- yield factory.messageStarted(item.id);
407
- break;
408
- case "reasoning":
409
- yield factory.reasoningStarted(item.id, "full");
410
- break;
411
- case "function_call":
412
- yield factory.toolCallStarted(item.id, ((item as Record<string, unknown>).name as string) ?? "unknown");
413
- break;
414
- }
415
- continue;
416
- }
417
-
418
- if (sseEvent.type === "response.output_text.delta") {
419
- const data = sseEvent.data as { item_id: string; delta: string };
420
- yield factory.messageDelta(data.item_id, textBlock(data.delta));
421
- messageItemsWithDelta.add(data.item_id);
422
- continue;
423
- }
424
-
425
- if (sseEvent.type === "response.output_text.done") {
426
- const data = sseEvent.data as { item_id: string; text: string };
427
- if (!messageItemsWithDelta.has(data.item_id) && data.text) {
428
- yield factory.messageDelta(data.item_id, textBlock(data.text));
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;
429
351
  }
430
- yield factory.messageCompleted(data.item_id);
431
- output.push(messageItem([textBlock(data.text)], { id: data.item_id }));
432
- continue;
433
352
  }
353
+ continue;
354
+ }
434
355
 
435
- if (sseEvent.type === "response.reasoning.delta") {
436
- const data = sseEvent.data as { item_id: string; delta: string };
437
- yield factory.reasoningDelta(data.item_id, textBlock(data.delta));
438
- continue;
439
- }
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
+ }
440
362
 
441
- if (sseEvent.type === "response.reasoning.done") {
442
- const data = sseEvent.data as { item_id: string; text: string };
443
- yield factory.reasoningCompleted(data.item_id);
444
- output.push(reasoningItem([textBlock(data.text)], "full", data.item_id));
445
- 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));
446
367
  }
368
+ yield factory.messageCompleted(data.item_id);
369
+ output.push(messageItem([textBlock(data.text)], { id: data.item_id }));
370
+ continue;
371
+ }
447
372
 
448
- if (sseEvent.type === "response.tool_call.delta") {
449
- const data = sseEvent.data as { item_id: string; delta: { arguments?: string } };
450
- if (data.delta.arguments) {
451
- yield factory.toolCallDelta(data.item_id, { argumentsText: data.delta.arguments });
452
- }
453
- continue;
454
- }
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
+ }
455
378
 
456
- if (sseEvent.type === "response.tool_call.done") {
457
- const data = sseEvent.data as { item_id: string; arguments?: string; name?: string };
458
- const tcItem = toolCallItem(data.item_id, data.name ?? "unknown", data.arguments ?? "");
459
- yield factory.toolCallCompleted(data.item_id);
460
- output.push(tcItem);
461
- continue;
462
- }
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
+ }
463
385
 
464
- if (
465
- sseEvent.type === "response.completed" ||
466
- sseEvent.type === "response.failed" ||
467
- sseEvent.type === "response.incomplete"
468
- ) {
469
- const data = sseEvent.data as { response: ResponsesAPIResponse };
470
- if (completedResponse) {
471
- yield factory.responseWarning("Duplicate finish signal ignored", "DUPLICATE_FINISH");
472
- continue;
473
- }
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
+ }
474
391
 
475
- completedResponse = data.response;
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
+ }
476
399
 
477
- if (sseEvent.type === "response.failed") {
478
- yield factory.responseWarning(
479
- `Response failed: ${extractFailureMessage(data.response)}`,
480
- "PROVIDER_FAILURE",
481
- );
482
- }
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");
483
408
  continue;
484
409
  }
485
410
 
486
- if (!KNOWN_RESPONSES_SSE_TYPES.has(sseEvent.type) && !unknownEventsWarned) {
487
- unknownEventsWarned = true;
411
+ completedResponse = data.response;
412
+
413
+ if (sseEvent.type === "response.failed") {
488
414
  yield factory.responseWarning(
489
- `Responses API sent unknown event type "${sseEvent.type}"; this may indicate an incomplete integration`,
490
- "UNKNOWN_PROVIDER_EVENT",
415
+ `Response failed: ${extractFailureMessage(data.response)}`,
416
+ "PROVIDER_FAILURE",
491
417
  );
492
418
  }
419
+ continue;
493
420
  }
494
421
 
495
- if (done) {
496
- streamDone = true;
497
- break;
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
+ );
498
428
  }
499
429
  }
500
- } finally {
501
- try {
502
- if (!streamDone) await reader.cancel().catch(() => undefined);
503
- } finally {
504
- reader.releaseLock();
505
- }
506
- }
507
-
508
- if (parser.getRemaining().trim().length > 0) {
509
- yield factory.responseWarning("Stream ended with an incomplete Responses SSE frame", "STREAM_ERROR");
510
430
  }
511
431
 
512
- // 解析完成响应中的 usage 和 replay
513
432
  let rawResponseId: string | undefined;
514
-
515
433
  if (completedResponse) {
516
434
  rawResponseId = completedResponse.id;
517
435
  if (completedResponse.usage) {
@@ -519,47 +437,19 @@ export class ResponsesAdapter extends AdapterBase {
519
437
  }
520
438
  }
521
439
 
522
- // 构造 replay:在 output 基础上追加 opaque continuation
523
440
  const replay = [...replayFromOutput(output)];
524
-
525
- // 如果有 provider continuation id,附加 opaque replay item
526
441
  if (completedResponse?.id) {
527
442
  replay.push(opaqueItem("responses", "replay", { id: completedResponse.id }));
528
443
  }
529
444
 
530
- // 从 completedResponse 推断 stop reason
531
445
  const stopReason = completedResponse ? this.inferStopReason(completedResponse) : undefined;
532
446
 
533
- const auxiliaryResult = await auxiliary.finalize(factory);
534
- for (const event of auxiliaryResult.events) {
535
- yield event;
536
- }
537
-
538
- if (!completedEmitted) {
539
- completedEmitted = true;
540
- const finalResponse = this.buildResponse(
541
- request,
542
- {
543
- output,
544
- replay,
545
- stopReason,
546
- usage: auxiliaryResult.usage,
547
- billing: auxiliaryResult.billing,
548
- auxiliary: auxiliaryResult.auxiliary,
549
- warnings: auxiliaryResult.warnings,
550
- metadataSources: auxiliaryResult.metadataSources,
551
- rawResponseId,
552
- },
553
- factory,
554
- );
555
- yield factory.responseCompleted({
556
- replay: finalResponse.replay,
557
- stopReason: finalResponse.stopReason,
558
- trace: finalResponse.backend,
559
- usage: finalResponse.usage,
560
- billing: finalResponse.billing,
561
- auxiliary: finalResponse.auxiliary,
562
- warnings: finalResponse.warnings,
447
+ if (gate.tryComplete()) {
448
+ yield* this.emitStreamCompleted(factory, request, auxiliary, {
449
+ output,
450
+ replay,
451
+ stopReason,
452
+ rawResponseId,
563
453
  });
564
454
  }
565
455
  }
@@ -136,6 +136,25 @@ function validateInputItem(item: unknown, field: string, issues: ValidationIssue
136
136
  "TOOL_CALL_ARGUMENTS_INVALID",
137
137
  `${field}.argumentsText must be a string`,
138
138
  );
139
+ } else {
140
+ try {
141
+ const parsed: unknown = JSON.parse(item.argumentsText);
142
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
143
+ pushIssue(
144
+ issues,
145
+ `${field}.argumentsText`,
146
+ "TOOL_CALL_ARGUMENTS_INVALID",
147
+ `${field}.argumentsText must encode a JSON object`,
148
+ );
149
+ }
150
+ } catch {
151
+ pushIssue(
152
+ issues,
153
+ `${field}.argumentsText`,
154
+ "TOOL_CALL_ARGUMENTS_INVALID",
155
+ `${field}.argumentsText must encode a JSON object`,
156
+ );
157
+ }
139
158
  }
140
159
  return;
141
160
  case "tool_result":
@@ -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
  }
@@ -56,7 +56,7 @@ export type StreamResult = {
56
56
 
57
57
  export abstract class AdapterBase implements BackendAdapter {
58
58
  abstract readonly kind: "chat-completions" | "messages" | "responses" | "ollama" | "mock";
59
- abstract readonly capabilities: import("../types/index.js").AdapterCapabilities;
59
+ abstract readonly isSyntheticStream: boolean;
60
60
 
61
61
  /**
62
62
  * stream 模板方法:
@@ -70,7 +70,7 @@ export abstract class AdapterBase implements BackendAdapter {
70
70
 
71
71
  const factory = createEventFactory({
72
72
  responseId: request.requestId,
73
- backend: { kind: this.kind, isSynthetic: this.capabilities.textStreaming === "synthetic" },
73
+ backend: { kind: this.kind, isSynthetic: this.isSyntheticStream },
74
74
  });
75
75
 
76
76
  yield factory.responseStarted(request.model);
@@ -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 = this.extractText(result.output);
132
+ const text = extractText(result.output);
133
133
  const warnings = mergeWarnings(result.warnings, _factory.warnings);
134
134
  const auxiliary = mergeAuxiliary(
135
135
  result.auxiliary,
@@ -151,16 +151,50 @@ export abstract class AdapterBase implements BackendAdapter {
151
151
  requestId: request.requestId,
152
152
  rawResponseId: result.rawResponseId,
153
153
  adapter: this.kind,
154
- isSyntheticStream: this.capabilities.textStreaming === "synthetic",
154
+ isSyntheticStream: this.isSyntheticStream,
155
155
  metadataSources: result.metadataSources,
156
156
  warnings,
157
157
  },
158
158
  };
159
159
  }
160
160
 
161
- /** 从 output items 中提取文本内容。 */
162
- protected extractText(output: OutputItem[]): string {
163
- return extractText(output);
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 {