@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.
@@ -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,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 { 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";
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: contentBlocksToText(
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
- if (request.tools && request.tools.length > 0) {
269
- body.tools = request.tools.map(
270
- (t): ResponsesTool => ({
271
- type: "function",
272
- name: t.name,
273
- description: t.description,
274
- input_schema: t.inputSchema,
275
- }),
276
- );
277
- }
278
-
279
- if (request.toolChoice) {
280
- if (request.toolChoice === "auto") body.tool_choice = "auto";
281
- else if (request.toolChoice === "none") body.tool_choice = "none";
282
- else if (request.toolChoice.type === "tool") {
283
- body.tool_choice = { type: "function", name: request.toolChoice.name };
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
- let response: Response;
303
-
304
- try {
305
- response = await this.fetchFn(`${this.baseUrl}/responses`, {
306
- method: "POST",
307
- headers: {
308
- "Content-Type": "application/json",
309
- Authorization: `Bearer ${this.apiKey}`,
310
- },
311
- body: JSON.stringify(providerRequest),
312
- signal: request.signal,
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
- try {
355
- while (true) {
356
- const readResult = await reader.read().catch((err: unknown) => {
357
- throw new AIStreamError(
358
- `Failed to read response stream: ${err instanceof Error ? err.message : String(err)}`,
359
- "STREAM_ERROR",
360
- );
361
- });
362
- const { done, value } = readResult;
363
- const { items: events, malformed: malformedEvents } = done ? parser.flush() : parser.feed(value as Uint8Array);
364
-
365
- const malformedWarning = emitMalformedStreamWarning(factory, {
366
- count: malformedEvents,
367
- providerLabel: "Responses",
368
- transportLabel: "SSE event(s)",
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
- for (const sseEvent of events) {
375
- if (sseEvent.type === "error") {
376
- const data = sseEvent.data as { message?: string; code?: string };
377
- yield factory.responseWarning(data.message ?? "Provider error event", data.code);
378
- continue;
379
- }
380
-
381
- // item 级事件
382
- if (sseEvent.type === "response.output_item.added") {
383
- const item = (sseEvent.data as { item: { id: string; type: string; [key: string]: unknown } }).item;
384
- switch (item.type) {
385
- case "message":
386
- yield factory.messageStarted(item.id);
387
- break;
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
- if (sseEvent.type === "response.reasoning.delta") {
419
- const data = sseEvent.data as { item_id: string; delta: string };
420
- yield factory.reasoningDelta(data.item_id, textBlock(data.delta));
421
- continue;
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
- if (sseEvent.type === "response.reasoning.done") {
425
- const data = sseEvent.data as { item_id: string; text: string };
426
- yield factory.reasoningCompleted(data.item_id);
427
- output.push(reasoningItem([textBlock(data.text)], "full", data.item_id));
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
- if (sseEvent.type === "response.function_call_arguments.delta") {
432
- const data = sseEvent.data as { item_id: string; delta: string };
433
- if (data.delta) yield factory.toolCallDelta(data.item_id, { argumentsText: data.delta });
434
- continue;
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
- if (sseEvent.type === "response.function_call_arguments.done") {
438
- const data = sseEvent.data as { item_id: string; arguments: string };
439
- const tcItem = toolCallItem(data.item_id, toolCallNames.get(data.item_id) ?? "unknown", data.arguments);
440
- yield factory.toolCallCompleted(data.item_id);
441
- output.push(tcItem);
442
- continue;
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
- if (
446
- sseEvent.type === "response.completed" ||
447
- sseEvent.type === "response.failed" ||
448
- sseEvent.type === "response.incomplete"
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
- 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
+ }
457
399
 
458
- if (sseEvent.type === "response.failed") {
459
- yield factory.responseWarning(
460
- `Response failed: ${extractFailureMessage(data.response)}`,
461
- "PROVIDER_FAILURE",
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
- if (!KNOWN_RESPONSES_SSE_TYPES.has(sseEvent.type) && !unknownEventsWarned) {
468
- unknownEventsWarned = true;
411
+ completedResponse = data.response;
412
+
413
+ if (sseEvent.type === "response.failed") {
469
414
  yield factory.responseWarning(
470
- `Responses API sent unknown event type "${sseEvent.type}"; this may indicate an incomplete integration`,
471
- "UNKNOWN_PROVIDER_EVENT",
415
+ `Response failed: ${extractFailureMessage(data.response)}`,
416
+ "PROVIDER_FAILURE",
472
417
  );
473
418
  }
419
+ continue;
474
420
  }
475
421
 
476
- if (done) {
477
- streamDone = true;
478
- 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
+ );
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
- const auxiliaryResult = await auxiliary.finalize(factory);
515
- for (const event of auxiliaryResult.events) {
516
- yield event;
517
- }
518
-
519
- if (!completedEmitted) {
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 = 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,
@@ -158,9 +158,43 @@ export abstract class AdapterBase implements BackendAdapter {
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 {
@@ -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
+ }
@@ -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, metadataSourceList } from "./adapter-auxiliary.js";
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 { IncrementalStreamParser, splitLines, splitSSEFrames } from "./incremental-stream-parser.js";
58
- export type { StreamSplitResult, StreamParseResult } from "./incremental-stream-parser.js";
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";