@codehz/ai 0.2.0 → 0.2.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/dist/index.mjs CHANGED
@@ -11,8 +11,10 @@ var AIError = class extends Error {
11
11
  };
12
12
  /** 请求构造失败 — 参数校验不通过。在进入 adapter 前同步抛错。 */
13
13
  var AIRequestError = class extends AIError {
14
- constructor(message, code) {
14
+ issues;
15
+ constructor(message, code, issues) {
15
16
  super(message, code, "AIRequestError");
17
+ this.issues = issues;
16
18
  }
17
19
  };
18
20
  /** Provider 调用失败 — HTTP 非 2xx、网络错误。由 AdapterBase 捕获转为 warning。 */
@@ -59,7 +61,9 @@ const WarningCode = {
59
61
  /** 能力降级 */
60
62
  CAPABILITY_DOWNGRADE: "CAPABILITY_DOWNGRADE",
61
63
  /** 模拟流式 */
62
- SYNTHETIC_STREAM: "SYNTHETIC_STREAM"
64
+ SYNTHETIC_STREAM: "SYNTHETIC_STREAM",
65
+ /** 工具调用以批量方式到达(非 token 级流式) */
66
+ TOOL_CALL_BATCHED: "TOOL_CALL_BATCHED"
63
67
  };
64
68
  //#endregion
65
69
  //#region src/core/validation.ts
@@ -194,6 +198,16 @@ function validateToolChoice(toolChoice, issues) {
194
198
  if (toolChoice === "auto" || toolChoice === "none") return;
195
199
  if (!isRecord(toolChoice) || toolChoice.type !== "tool" || typeof toolChoice.name !== "string" || toolChoice.name.length === 0) pushIssue(issues, "toolChoice", "TOOL_CHOICE_INVALID", "toolChoice must be auto, none, or { type: \"tool\", name }");
196
200
  }
201
+ /** Validate include settings, appending issues to the given array. */
202
+ function validateInclude(include, issues) {
203
+ if (!isRecord(include)) {
204
+ pushIssue(issues, "include", "INCLUDE_INVALID", "include must be an object");
205
+ return;
206
+ }
207
+ if (include.usage !== void 0 && (typeof include.usage !== "string" || !INCLUDE_MODES.has(include.usage))) pushIssue(issues, "include.usage", "INCLUDE_USAGE_INVALID", "include.usage must be off or best_effort");
208
+ if (include.billing !== void 0 && (typeof include.billing !== "string" || !INCLUDE_MODES.has(include.billing))) pushIssue(issues, "include.billing", "INCLUDE_BILLING_INVALID", "include.billing must be off or best_effort");
209
+ if (include.providerMetadata !== void 0 && (typeof include.providerMetadata !== "string" || !INCLUDE_MODES.has(include.providerMetadata))) pushIssue(issues, "include.providerMetadata", "INCLUDE_PROVIDER_METADATA_INVALID", "include.providerMetadata must be off or best_effort");
210
+ }
197
211
  /**
198
212
  * 校验 AIRequest,返回校验问题列表。
199
213
  * 空数组表示无问题。
@@ -205,7 +219,7 @@ function validateRequest(request) {
205
219
  if (!Array.isArray(request.input) || request.input.length === 0) pushIssue(issues, "input", "INPUT_EMPTY", "input must be a non-empty array");
206
220
  if (Array.isArray(request.input)) for (let i = 0; i < request.input.length; i++) validateInputItem(request.input[i], `input[${i}]`, issues);
207
221
  if (request.temperature !== void 0) {
208
- if (typeof request.temperature !== "number" || isNaN(request.temperature)) issues.push({
222
+ if (typeof request.temperature !== "number" || !Number.isFinite(request.temperature)) issues.push({
209
223
  field: "temperature",
210
224
  code: "TEMPERATURE_NOT_NUMBER",
211
225
  message: "temperature must be a number"
@@ -217,7 +231,7 @@ function validateRequest(request) {
217
231
  });
218
232
  }
219
233
  if (request.maxOutputTokens !== void 0) {
220
- if (typeof request.maxOutputTokens !== "number" || isNaN(request.maxOutputTokens)) issues.push({
234
+ if (typeof request.maxOutputTokens !== "number" || !Number.isFinite(request.maxOutputTokens)) issues.push({
221
235
  field: "maxOutputTokens",
222
236
  code: "MAX_OUTPUT_TOKENS_NOT_NUMBER",
223
237
  message: "maxOutputTokens must be a number"
@@ -228,12 +242,7 @@ function validateRequest(request) {
228
242
  message: "maxOutputTokens must be a positive integer"
229
243
  });
230
244
  }
231
- if (request.include !== void 0) if (!isRecord(request.include)) pushIssue(issues, "include", "INCLUDE_INVALID", "include must be an object");
232
- else {
233
- if (request.include.usage !== void 0 && !INCLUDE_MODES.has(request.include.usage)) pushIssue(issues, "include.usage", "INCLUDE_USAGE_INVALID", "include.usage must be off or best_effort");
234
- if (request.include.billing !== void 0 && !INCLUDE_MODES.has(request.include.billing)) pushIssue(issues, "include.billing", "INCLUDE_BILLING_INVALID", "include.billing must be off or best_effort");
235
- if (request.include.providerMetadata !== void 0 && !INCLUDE_MODES.has(request.include.providerMetadata)) pushIssue(issues, "include.providerMetadata", "INCLUDE_PROVIDER_METADATA_INVALID", "include.providerMetadata must be off or best_effort");
236
- }
245
+ if (request.include !== void 0) validateInclude(request.include, issues);
237
246
  if (request.metadata !== void 0) {
238
247
  if (!isRecord(request.metadata)) pushIssue(issues, "metadata", "METADATA_INVALID", "metadata must be an object");
239
248
  else for (const [key, value] of Object.entries(request.metadata)) if (typeof value !== "string") pushIssue(issues, `metadata.${key}`, "METADATA_VALUE_INVALID", `metadata.${key} must be a string`);
@@ -260,8 +269,9 @@ function validateRequest(request) {
260
269
  * 适用于客户端入口的快速失败检查。
261
270
  */
262
271
  function assertValidRequest(request) {
263
- const first = validateRequest(request)[0];
264
- if (first) throw new AIRequestError(first.message, first.code);
272
+ const issues = validateRequest(request);
273
+ const first = issues[0];
274
+ if (first) throw new AIRequestError(first.message, first.code, issues);
265
275
  }
266
276
  //#endregion
267
277
  //#region src/core/normalize.ts
@@ -279,6 +289,11 @@ const DEFAULT_INCLUDE = {
279
289
  */
280
290
  function normalizeRequest(request, options) {
281
291
  const { model, defaults } = options;
292
+ const earlyIncludeIssues = [];
293
+ if (request.include !== void 0) validateInclude(request.include, earlyIncludeIssues);
294
+ if (defaults?.include !== void 0) validateInclude(defaults.include, earlyIncludeIssues);
295
+ const firstIncludeIssue = earlyIncludeIssues[0];
296
+ if (firstIncludeIssue) throw new AIRequestError(firstIncludeIssue.message, firstIncludeIssue.code, earlyIncludeIssues);
282
297
  const merged = {
283
298
  ...defaults,
284
299
  ...request,
@@ -350,11 +365,11 @@ function createEventFactory(state) {
350
365
  ...data
351
366
  };
352
367
  },
353
- responseCompleted(response) {
368
+ responseCompleted(completion) {
354
369
  return {
355
370
  ...base(),
356
371
  type: "response.completed",
357
- response
372
+ ...completion
358
373
  };
359
374
  },
360
375
  messageStarted(id) {
@@ -367,22 +382,19 @@ function createEventFactory(state) {
367
382
  }
368
383
  };
369
384
  },
370
- messageDelta(itemId, text) {
385
+ messageDelta(itemId, delta) {
371
386
  return {
372
387
  ...base(),
373
388
  type: "message.delta",
374
389
  itemId,
375
- delta: {
376
- type: "text",
377
- text
378
- }
390
+ delta
379
391
  };
380
392
  },
381
- messageCompleted(item) {
393
+ messageCompleted(itemId) {
382
394
  return {
383
395
  ...base(),
384
396
  type: "message.completed",
385
- item
397
+ itemId
386
398
  };
387
399
  },
388
400
  reasoningStarted(id, visibility) {
@@ -403,11 +415,11 @@ function createEventFactory(state) {
403
415
  delta
404
416
  };
405
417
  },
406
- reasoningCompleted(item) {
418
+ reasoningCompleted(itemId) {
407
419
  return {
408
420
  ...base(),
409
421
  type: "reasoning.completed",
410
- item
422
+ itemId
411
423
  };
412
424
  },
413
425
  toolCallStarted(id, name) {
@@ -428,11 +440,11 @@ function createEventFactory(state) {
428
440
  delta
429
441
  };
430
442
  },
431
- toolCallCompleted(item) {
443
+ toolCallCompleted(itemId) {
432
444
  return {
433
445
  ...base(),
434
446
  type: "tool_call.completed",
435
- item
447
+ itemId
436
448
  };
437
449
  },
438
450
  /** 返回当前已发出的 sequence 计数(用于断言) */
@@ -446,6 +458,20 @@ function createEventFactory(state) {
446
458
  };
447
459
  }
448
460
  //#endregion
461
+ //#region src/core/merge-auxiliary.ts
462
+ function mergeAuxiliary(base, patch) {
463
+ if (!base && !patch) return void 0;
464
+ const merged = {
465
+ ...base,
466
+ ...patch
467
+ };
468
+ if (base?.providerMetadata || patch?.providerMetadata) merged.providerMetadata = {
469
+ ...base?.providerMetadata,
470
+ ...patch?.providerMetadata
471
+ };
472
+ return merged;
473
+ }
474
+ //#endregion
449
475
  //#region src/core/aggregator.ts
450
476
  function createAggregatorState() {
451
477
  return {
@@ -454,10 +480,47 @@ function createAggregatorState() {
454
480
  warningSet: /* @__PURE__ */ new Set(),
455
481
  output: [],
456
482
  textParts: [],
457
- toolCalls: []
483
+ toolCalls: [],
484
+ started: false,
485
+ completed: false,
486
+ activeItems: /* @__PURE__ */ new Map(),
487
+ itemOrder: [],
488
+ completedItems: /* @__PURE__ */ new Map()
489
+ };
490
+ }
491
+ function getActiveItem(state, itemId, expectedType) {
492
+ const item = state.activeItems.get(itemId);
493
+ if (!item) throw streamProtocolError(`Received ${expectedType} delta/completed for unknown item: ${itemId}`);
494
+ if (item.type !== expectedType) throw streamProtocolError(`Item ${itemId} started as ${item.type} but received ${expectedType} event`);
495
+ return item;
496
+ }
497
+ function finalizeMessage(active) {
498
+ return {
499
+ type: "message",
500
+ id: active.id,
501
+ role: active.role,
502
+ content: active.content
503
+ };
504
+ }
505
+ function finalizeReasoning(active) {
506
+ return {
507
+ type: "reasoning",
508
+ id: active.id,
509
+ visibility: active.visibility,
510
+ content: active.content
511
+ };
512
+ }
513
+ function finalizeToolCall(active) {
514
+ return {
515
+ type: "tool_call",
516
+ id: active.id,
517
+ name: active.name,
518
+ argumentsText: active.argumentsText
458
519
  };
459
520
  }
460
521
  function handleResponseStarted(state, event) {
522
+ if (state.started) throw streamProtocolError("Stream must contain exactly one response.started event");
523
+ state.started = true;
461
524
  state.responseId = event.responseId;
462
525
  state.model = event.model;
463
526
  state.backendInfo = event.backend;
@@ -474,48 +537,106 @@ function handleResponseAuxiliary(state, event) {
474
537
  ...state.billing,
475
538
  ...event.billing
476
539
  };
477
- if (event.auxiliary) state.auxiliary = mergeAuxiliary$1(state.auxiliary, event.auxiliary);
540
+ if (event.auxiliary) state.auxiliary = mergeAuxiliary(state.auxiliary, event.auxiliary) ?? {};
541
+ }
542
+ function handleMessageStarted(state, event) {
543
+ const id = event.item.id;
544
+ if (state.activeItems.has(id)) throw streamProtocolError(`Item with id ${id} is already active`);
545
+ state.activeItems.set(id, {
546
+ type: "message",
547
+ id,
548
+ role: event.item.role,
549
+ content: []
550
+ });
551
+ state.itemOrder.push(id);
552
+ }
553
+ function handleMessageDelta(state, event) {
554
+ getActiveItem(state, event.itemId, "message").content.push(event.delta);
478
555
  }
479
556
  function handleMessageCompleted(state, event) {
480
- state.output.push(event.item);
481
- pushMessageText(state, event.item);
557
+ const active = getActiveItem(state, event.itemId, "message");
558
+ state.activeItems.delete(event.itemId);
559
+ const item = finalizeMessage(active);
560
+ state.completedItems.set(event.itemId, item);
561
+ pushMessageText(state, item);
562
+ }
563
+ function handleReasoningStarted(state, event) {
564
+ const id = event.item.id;
565
+ if (state.activeItems.has(id)) throw streamProtocolError(`Item with id ${id} is already active`);
566
+ state.activeItems.set(id, {
567
+ type: "reasoning",
568
+ id,
569
+ visibility: event.item.visibility,
570
+ content: []
571
+ });
572
+ state.itemOrder.push(id);
573
+ }
574
+ function handleReasoningDelta(state, event) {
575
+ getActiveItem(state, event.itemId, "reasoning").content.push(event.delta);
482
576
  }
483
577
  function handleReasoningCompleted(state, event) {
484
- state.output.push(event.item);
578
+ const active = getActiveItem(state, event.itemId, "reasoning");
579
+ state.activeItems.delete(event.itemId);
580
+ state.completedItems.set(event.itemId, finalizeReasoning(active));
581
+ }
582
+ function handleToolCallStarted(state, event) {
583
+ const id = event.item.id;
584
+ if (state.activeItems.has(id)) throw streamProtocolError(`Item with id ${id} is already active`);
585
+ state.activeItems.set(id, {
586
+ type: "tool_call",
587
+ id,
588
+ name: event.item.name,
589
+ argumentsText: ""
590
+ });
591
+ state.itemOrder.push(id);
592
+ }
593
+ function handleToolCallDelta(state, event) {
594
+ const active = getActiveItem(state, event.itemId, "tool_call");
595
+ if (event.delta.argumentsText) active.argumentsText += event.delta.argumentsText;
485
596
  }
486
597
  function handleToolCallCompleted(state, event) {
487
- state.output.push(event.item);
488
- state.toolCalls.push(event.item);
598
+ const active = getActiveItem(state, event.itemId, "tool_call");
599
+ state.activeItems.delete(event.itemId);
600
+ const item = finalizeToolCall(active);
601
+ state.completedItems.set(event.itemId, item);
602
+ state.toolCalls.push(item);
489
603
  }
490
604
  function handleResponseCompleted(state, event) {
491
- state.replayFromAdapter = event.response.replay;
492
- state.responseIdFromAdapter = event.response.id;
493
- state.stopReasonFromAdapter = event.response.stopReason;
494
- state.backendFromAdapter = event.response.backend;
495
- if (event.response.usage) state.usage = {
605
+ if (state.activeItems.size > 0) throw streamProtocolError("response.completed received while active items still pending");
606
+ state.completed = true;
607
+ state.replayFromAdapter = event.replay;
608
+ state.stopReasonFromAdapter = event.stopReason;
609
+ state.backendFromAdapter = event.trace;
610
+ if (event.usage) state.usage = {
496
611
  ...state.usage,
497
- ...event.response.usage
612
+ ...event.usage
498
613
  };
499
- if (event.response.billing) state.billing = {
614
+ if (event.billing) state.billing = {
500
615
  ...state.billing,
501
- ...event.response.billing
616
+ ...event.billing
502
617
  };
503
- if (event.response.auxiliary) state.auxiliary = mergeAuxiliary$1(state.auxiliary, event.response.auxiliary);
504
- if (event.response.warnings) pushWarnings(state, event.response.warnings);
618
+ if (event.auxiliary) state.auxiliary = mergeAuxiliary(state.auxiliary, event.auxiliary) ?? {};
619
+ if (event.warnings) pushWarnings(state, event.warnings);
620
+ if (event.opaqueOutput) state.output.push(...event.opaqueOutput);
505
621
  }
506
622
  function buildResponse(state) {
507
- const backendFromResponse = state.backendFromAdapter;
623
+ const backendFromCompleted = state.backendFromAdapter;
508
624
  const backend = {
509
- adapter: backendFromResponse?.adapter ?? state.backendInfo?.kind ?? "unknown",
510
- isSyntheticStream: backendFromResponse?.isSyntheticStream ?? state.backendInfo?.isSynthetic ?? false,
511
- requestId: backendFromResponse?.requestId ?? state.responseId,
512
- rawResponseId: backendFromResponse?.rawResponseId,
513
- metadataSources: backendFromResponse?.metadataSources,
514
- warnings: backendFromResponse?.warnings
625
+ adapter: backendFromCompleted?.adapter ?? state.backendInfo?.kind ?? "unknown",
626
+ isSyntheticStream: backendFromCompleted?.isSyntheticStream ?? state.backendInfo?.isSynthetic ?? false,
627
+ requestId: backendFromCompleted?.requestId ?? state.responseId,
628
+ rawResponseId: backendFromCompleted?.rawResponseId,
629
+ metadataSources: backendFromCompleted?.metadataSources,
630
+ warnings: backendFromCompleted?.warnings
515
631
  };
632
+ const orderedOutput = state.itemOrder.map((id) => {
633
+ const item = state.completedItems.get(id);
634
+ if (!item) throw streamProtocolError(`Item ${id} was started but not completed`);
635
+ return item;
636
+ });
516
637
  return {
517
- id: state.responseIdFromAdapter ?? state.responseId,
518
- output: state.output,
638
+ id: state.responseId,
639
+ output: [...orderedOutput, ...state.output],
519
640
  replay: state.replayFromAdapter ?? [],
520
641
  text: state.textParts.join(""),
521
642
  toolCalls: state.toolCalls,
@@ -537,6 +658,7 @@ function aggregateEvents(events) {
537
658
  return finalizeAggregation(state);
538
659
  }
539
660
  function aggregateEvent(state, event) {
661
+ validateEventEnvelope(state, event);
540
662
  state.lastEventType = event.type;
541
663
  switch (event.type) {
542
664
  case "response.started":
@@ -549,17 +671,29 @@ function aggregateEvent(state, event) {
549
671
  handleResponseAuxiliary(state, event);
550
672
  break;
551
673
  case "message.started":
674
+ handleMessageStarted(state, event);
675
+ break;
552
676
  case "message.delta":
553
- case "reasoning.started":
554
- case "reasoning.delta":
555
- case "tool_call.started":
556
- case "tool_call.delta": break;
677
+ handleMessageDelta(state, event);
678
+ break;
557
679
  case "message.completed":
558
680
  handleMessageCompleted(state, event);
559
681
  break;
682
+ case "reasoning.started":
683
+ handleReasoningStarted(state, event);
684
+ break;
685
+ case "reasoning.delta":
686
+ handleReasoningDelta(state, event);
687
+ break;
560
688
  case "reasoning.completed":
561
689
  handleReasoningCompleted(state, event);
562
690
  break;
691
+ case "tool_call.started":
692
+ handleToolCallStarted(state, event);
693
+ break;
694
+ case "tool_call.delta":
695
+ handleToolCallDelta(state, event);
696
+ break;
563
697
  case "tool_call.completed":
564
698
  handleToolCallCompleted(state, event);
565
699
  break;
@@ -569,20 +703,10 @@ function aggregateEvent(state, event) {
569
703
  }
570
704
  }
571
705
  function finalizeAggregation(state) {
572
- if (state.lastEventType !== "response.completed") throw new Error("Stream must end with response.completed event to produce a valid AIResponse");
706
+ if (!state.started) throw streamProtocolError("Stream must start with response.started event");
707
+ if (!state.completed || state.lastEventType !== "response.completed") throw streamProtocolError("Stream must end with response.completed event to produce a valid AIResponse");
573
708
  return buildResponse(state);
574
709
  }
575
- function mergeAuxiliary$1(base, patch) {
576
- const merged = {
577
- ...base,
578
- ...patch
579
- };
580
- if (base.providerMetadata || patch.providerMetadata) merged.providerMetadata = {
581
- ...base.providerMetadata,
582
- ...patch.providerMetadata
583
- };
584
- return merged;
585
- }
586
710
  function pushWarnings(state, warnings) {
587
711
  for (const warning of warnings) if (!state.warningSet.has(warning)) {
588
712
  state.warningSet.add(warning);
@@ -592,6 +716,16 @@ function pushWarnings(state, warnings) {
592
716
  function pushMessageText(state, item) {
593
717
  for (const block of item.content) if (block.type === "text") state.textParts.push(block.text);
594
718
  }
719
+ function validateEventEnvelope(state, event) {
720
+ if (state.completed) throw streamProtocolError("response.completed must be the final stream event");
721
+ if (!state.started && event.type !== "response.started") throw streamProtocolError("Stream must start with response.started event");
722
+ if (state.responseId !== void 0 && event.responseId !== state.responseId) throw streamProtocolError("All stream events must use the same responseId");
723
+ if (state.nextSequence !== void 0 && event.sequence !== state.nextSequence) throw streamProtocolError(`Expected event sequence ${state.nextSequence}, received ${event.sequence}`);
724
+ state.nextSequence = event.sequence + 1;
725
+ }
726
+ function streamProtocolError(message) {
727
+ return new AIStreamError(message, "STREAM_PROTOCOL_ERROR");
728
+ }
595
729
  //#endregion
596
730
  //#region src/core/collect-stream.ts
597
731
  async function collectStream(stream) {
@@ -937,7 +1071,7 @@ var AdapterBase = class {
937
1071
  responseId: request.requestId,
938
1072
  backend: {
939
1073
  kind: this.kind,
940
- isSynthetic: !this.nativeStreaming
1074
+ isSynthetic: this.capabilities.textStreaming === "synthetic"
941
1075
  }
942
1076
  });
943
1077
  yield factory.responseStarted(request.model);
@@ -945,12 +1079,25 @@ var AdapterBase = class {
945
1079
  const providerRequest = await this.buildRequest(request);
946
1080
  yield* this.runStream(providerRequest, factory, request);
947
1081
  } catch (err) {
948
- if (err instanceof AIRequestError || err instanceof AIStreamError || err instanceof AIMappingError) throw err;
949
- yield factory.responseWarning(err instanceof Error ? err.message : String(err), "PROVIDER_ERROR");
950
- yield factory.responseCompleted(this.buildResponse(request, {
951
- output: [],
952
- replay: []
953
- }, factory));
1082
+ if (err instanceof AIRequestError || err instanceof AIProviderError || err instanceof AIStreamError) throw err;
1083
+ if (err instanceof AIMappingError) {
1084
+ yield factory.responseWarning(err.message, "MAPPING_ERROR");
1085
+ const errorResp = this.buildResponse(request, {
1086
+ output: [],
1087
+ replay: []
1088
+ }, factory);
1089
+ yield factory.responseCompleted({
1090
+ replay: errorResp.replay,
1091
+ stopReason: errorResp.stopReason,
1092
+ trace: errorResp.backend,
1093
+ usage: errorResp.usage,
1094
+ billing: errorResp.billing,
1095
+ auxiliary: errorResp.auxiliary,
1096
+ warnings: errorResp.warnings
1097
+ });
1098
+ return;
1099
+ }
1100
+ throw err;
954
1101
  }
955
1102
  }
956
1103
  /**
@@ -976,7 +1123,7 @@ var AdapterBase = class {
976
1123
  requestId: request.requestId,
977
1124
  rawResponseId: result.rawResponseId,
978
1125
  adapter: this.kind,
979
- isSyntheticStream: !this.nativeStreaming,
1126
+ isSyntheticStream: this.capabilities.textStreaming === "synthetic",
980
1127
  metadataSources: result.metadataSources,
981
1128
  warnings
982
1129
  }
@@ -990,18 +1137,6 @@ var AdapterBase = class {
990
1137
  return new AdapterAuxiliaryState(request);
991
1138
  }
992
1139
  };
993
- function mergeAuxiliary(base, patch) {
994
- if (!base && !patch) return void 0;
995
- const merged = {
996
- ...base,
997
- ...patch
998
- };
999
- if (base?.providerMetadata || patch?.providerMetadata) merged.providerMetadata = {
1000
- ...base?.providerMetadata,
1001
- ...patch?.providerMetadata
1002
- };
1003
- return merged;
1004
- }
1005
1140
  function mergeWarnings(...groups) {
1006
1141
  const merged = [];
1007
1142
  for (const group of groups) {
@@ -1011,6 +1146,91 @@ function mergeWarnings(...groups) {
1011
1146
  return merged.length > 0 ? merged : void 0;
1012
1147
  }
1013
1148
  //#endregion
1149
+ //#region src/helpers/adapter-security.ts
1150
+ /**
1151
+ * Adapter 边界安全辅助
1152
+ *
1153
+ * - opaque replay 入站 envelope(大小 / 深度)
1154
+ * - provider HTTP 错误 body 出站脱敏
1155
+ */
1156
+ const MAX_OPAQUE_PAYLOAD_BYTES = 65536;
1157
+ const MAX_OPAQUE_JSON_DEPTH = 8;
1158
+ const PROVIDER_ERROR_MESSAGE_MAX_LEN = 500;
1159
+ const PROVIDER_ERROR_RAW_BODY_THRESHOLD = 200;
1160
+ /** 测量 JSON 值嵌套深度(对象/数组);循环引用按已访问节点深度计。 */
1161
+ function measureJsonDepth(value, seen = /* @__PURE__ */ new WeakSet()) {
1162
+ if (value === null || typeof value !== "object") return 0;
1163
+ if (seen.has(value)) return 0;
1164
+ seen.add(value);
1165
+ let maxChild = 0;
1166
+ if (Array.isArray(value)) for (const item of value) maxChild = Math.max(maxChild, measureJsonDepth(item, seen));
1167
+ else for (const key of Object.keys(value)) maxChild = Math.max(maxChild, measureJsonDepth(value[key], seen));
1168
+ return 1 + maxChild;
1169
+ }
1170
+ /**
1171
+ * Opaque replay 通用 envelope:必须是 object、体积 ≤ 64KB、深度 ≤ 8。
1172
+ * 不校验 adapter 专用字段形状。
1173
+ */
1174
+ function validateOpaqueReplayEnvelope(payload) {
1175
+ if (typeof payload !== "object" || payload === null) return {
1176
+ ok: false,
1177
+ reason: "payload must be an object"
1178
+ };
1179
+ let raw;
1180
+ try {
1181
+ raw = JSON.stringify(payload);
1182
+ } catch {
1183
+ return {
1184
+ ok: false,
1185
+ reason: "payload is not JSON-serializable"
1186
+ };
1187
+ }
1188
+ if (raw === void 0) return {
1189
+ ok: false,
1190
+ reason: "payload is not JSON-serializable"
1191
+ };
1192
+ if (raw.length > 65536) return {
1193
+ ok: false,
1194
+ reason: `opaque payload exceeds max size (${raw.length} > ${MAX_OPAQUE_PAYLOAD_BYTES})`
1195
+ };
1196
+ const depth = measureJsonDepth(payload);
1197
+ if (depth > 8) return {
1198
+ ok: false,
1199
+ reason: `opaque payload nesting depth (${depth}) exceeds max (8)`
1200
+ };
1201
+ return { ok: true };
1202
+ }
1203
+ /** envelope 失败时抛 AIRequestError。 */
1204
+ function assertOpaqueReplayEnvelope(payload) {
1205
+ const result = validateOpaqueReplayEnvelope(payload);
1206
+ if (!result.ok) throw new AIRequestError(`Invalid opaque replay payload: ${result.reason}`, "INVALID_OPAQUE_REPLAY");
1207
+ }
1208
+ /**
1209
+ * 从 provider HTTP 错误 body 提取可对外暴露的短消息,避免泄漏 HTML / 内部路径等。
1210
+ */
1211
+ function extractProviderErrorMessage(body, status) {
1212
+ if (!body) return `HTTP ${status}`;
1213
+ try {
1214
+ const parsed = JSON.parse(body);
1215
+ if (parsed && typeof parsed === "object") {
1216
+ const record = parsed;
1217
+ const errorField = record.error;
1218
+ let msg;
1219
+ if (errorField && typeof errorField === "object" && errorField !== null) msg = errorField.message;
1220
+ if (typeof msg !== "string") msg = typeof errorField === "string" ? errorField : record.message;
1221
+ if (typeof msg === "string" && msg.length > 0) return msg.slice(0, 500);
1222
+ }
1223
+ } catch {}
1224
+ const trimmed = body.trimStart();
1225
+ if (trimmed.startsWith("<!") || trimmed.startsWith("<html") || body.length > 200) return `HTTP ${status}. Body omitted (${body.length} bytes)`;
1226
+ return body.slice(0, 500);
1227
+ }
1228
+ /** 统一构造脱敏后的 AIProviderError。 */
1229
+ function providerHttpError(status, body) {
1230
+ const safe = extractProviderErrorMessage(body, status);
1231
+ return new AIProviderError(`Provider returned ${status}: ${safe}`, "PROVIDER_ERROR", status, safe);
1232
+ }
1233
+ //#endregion
1014
1234
  //#region src/helpers/usage-mapping.ts
1015
1235
  function num(value) {
1016
1236
  return typeof value === "number" && Number.isFinite(value) ? value : void 0;
@@ -1090,43 +1310,55 @@ function usageFromOllama(raw) {
1090
1310
  * - 未完成的行保留在 rest 中,等待下次 chunk 补全
1091
1311
  * - 支持跨 chunk 的 event 分片
1092
1312
  */
1093
- function parseSSEEvents(chunk) {
1313
+ function parseSSEEvents(chunk, options = {}) {
1094
1314
  const events = [];
1095
1315
  let eventType = "";
1096
1316
  let dataLines = [];
1097
1317
  let consumedUntil = 0;
1098
1318
  let cursor = 0;
1099
1319
  let malformedEvents = 0;
1320
+ const emitEvent = (consumedCursor) => {
1321
+ const dataStr = dataLines.join("\n");
1322
+ if (dataStr === "[DONE]") {
1323
+ eventType = "";
1324
+ dataLines = [];
1325
+ consumedUntil = consumedCursor;
1326
+ return;
1327
+ }
1328
+ try {
1329
+ const data = JSON.parse(dataStr);
1330
+ events.push({
1331
+ type: eventType,
1332
+ data
1333
+ });
1334
+ } catch {
1335
+ malformedEvents++;
1336
+ }
1337
+ eventType = "";
1338
+ dataLines = [];
1339
+ consumedUntil = consumedCursor;
1340
+ };
1341
+ const consumeLine = (line, consumedCursor) => {
1342
+ if (line.startsWith("event: ")) eventType = line.slice(7).trim();
1343
+ else if (line.startsWith("data: ")) dataLines.push(line.slice(6));
1344
+ else if (line === "" && eventType && dataLines.length > 0) emitEvent(consumedCursor);
1345
+ else if (line === "" && !eventType && dataLines.length === 0) consumedUntil = consumedCursor;
1346
+ };
1100
1347
  while (cursor < chunk.length) {
1101
1348
  const lineEnd = chunk.indexOf("\n", cursor);
1102
1349
  if (lineEnd === -1) break;
1103
1350
  let line = chunk.slice(cursor, lineEnd);
1104
1351
  cursor = lineEnd + 1;
1105
1352
  if (line.endsWith("\r")) line = line.slice(0, -1);
1106
- if (line.startsWith("event: ")) eventType = line.slice(7).trim();
1107
- else if (line.startsWith("data: ")) dataLines.push(line.slice(6));
1108
- else if (line === "" && eventType && dataLines.length > 0) {
1109
- const dataStr = dataLines.join("\n");
1110
- if (dataStr === "[DONE]") {
1111
- eventType = "";
1112
- dataLines = [];
1113
- consumedUntil = cursor;
1114
- continue;
1115
- }
1116
- try {
1117
- const data = JSON.parse(dataStr);
1118
- events.push({
1119
- type: eventType,
1120
- data
1121
- });
1122
- } catch {
1123
- malformedEvents++;
1124
- }
1125
- eventType = "";
1126
- dataLines = [];
1127
- consumedUntil = cursor;
1128
- } else if (line === "" && !eventType && dataLines.length === 0) consumedUntil = cursor;
1353
+ consumeLine(line, cursor);
1354
+ }
1355
+ if (options.allowEOF && cursor < chunk.length) {
1356
+ let line = chunk.slice(cursor);
1357
+ if (line.endsWith("\r")) line = line.slice(0, -1);
1358
+ consumeLine(line, chunk.length);
1359
+ cursor = chunk.length;
1129
1360
  }
1361
+ if (options.allowEOF && eventType && dataLines.length > 0) emitEvent(chunk.length);
1130
1362
  return {
1131
1363
  events,
1132
1364
  rest: chunk.slice(consumedUntil),
@@ -1134,70 +1366,287 @@ function parseSSEEvents(chunk) {
1134
1366
  };
1135
1367
  }
1136
1368
  //#endregion
1137
- //#region src/adapters/responses.ts
1369
+ //#region src/helpers/synthetic-stream.ts
1138
1370
  /**
1139
- * Responses Adapter
1371
+ * 模拟流式 (Synthetic Streaming)
1140
1372
  *
1141
- * 接入 OpenAI Responses API (responses 端点)。
1142
- * 职责分层:
1143
- * 1. buildRequest — 将 NormalizedRequest 转换为 Responses API 请求
1144
- * 2. runStream — 调用 API、解析 SSE、发射 canonical 事件
1373
+ * 将一组已解析的 canonical OutputItem 包装为规范事件流。
1374
+ * 适用于非原生流式后端:adapter 拿到完整响应后,调用此函数
1375
+ * 即可产出一致的事件序列,无需自己逐事件组装。
1145
1376
  *
1146
- * 支持消息流 / reasoning 流 / tool_call 流及高保真 replay。
1377
+ * 约束:
1378
+ * - 每个 item 只发一块完整 delta(不模拟逐 token)
1379
+ * - 保持 item 边界
1380
+ * - 保持后端原始顺序
1381
+ * - 不发明 reasoning
1382
+ * - 不改写工具参数
1383
+ */
1384
+ /**
1385
+ * 将已解析的 output items 包装为完整规范事件流。
1386
+ *
1387
+ * 用法示例(在 adapter 的 runStream 中):
1388
+ * ```ts
1389
+ * const result = parseNonStreamingResponse(data);
1390
+ * yield* syntheticStream({
1391
+ * model: request.model,
1392
+ * responseId: request.requestId,
1393
+ * backend: { kind: "chat-completions" },
1394
+ * output: result.output,
1395
+ * stopReason: result.stopReason,
1396
+ * usage: result.usage,
1397
+ * });
1398
+ * ```
1147
1399
  */
1148
- function ensureResponsesTextBlocks(blocks, field) {
1149
- for (let i = 0; i < blocks.length; i++) {
1150
- const block = blocks[i];
1151
- if (!block) continue;
1152
- if (block.type !== "text" && block.type !== "json") throw new AIRequestError(`responses does not support ${field}[${i}] of type "${block.type}"; only text/json blocks are supported`, "UNSUPPORTED_CONTENT_BLOCK");
1153
- }
1154
- return blocks;
1155
- }
1156
- function ensureResponsesReasoningBlocks(blocks, field) {
1157
- return blocks.map((block, index) => {
1158
- if (block.type !== "text") throw new AIRequestError(`responses does not support ${field}[${index}] of type "${block.type}"; reasoning only supports text blocks`, "UNSUPPORTED_CONTENT_BLOCK");
1159
- return block;
1400
+ async function* syntheticStream(options) {
1401
+ const { model, responseId, backend, output, replay, stopReason, usage, billing, providerMetadata, rawResponseId, warnings: extraWarnings } = options;
1402
+ const factory = createEventFactory({
1403
+ responseId,
1404
+ backend: {
1405
+ kind: backend.kind,
1406
+ isSynthetic: true
1407
+ }
1408
+ });
1409
+ yield factory.responseStarted(model);
1410
+ for (const item of output) yield* emitItemEvents(item, factory);
1411
+ if (usage || billing) yield factory.responseAuxiliary({
1412
+ usage,
1413
+ billing
1414
+ });
1415
+ const finalReplay = replay ?? replayFromOutput(output);
1416
+ const allWarnings = [];
1417
+ allWarnings.push("Response is synthetically streamed; delta granularity may differ from native streaming");
1418
+ if (extraWarnings) allWarnings.push(...extraWarnings);
1419
+ yield factory.responseCompleted({
1420
+ replay: finalReplay,
1421
+ stopReason,
1422
+ usage,
1423
+ billing,
1424
+ auxiliary: providerMetadata ? { providerMetadata } : void 0,
1425
+ opaqueOutput: output.filter((item) => item.type === "opaque"),
1426
+ warnings: allWarnings.length > 0 ? allWarnings : void 0,
1427
+ trace: {
1428
+ requestId: responseId,
1429
+ rawResponseId,
1430
+ adapter: backend.kind,
1431
+ isSyntheticStream: true,
1432
+ warnings: allWarnings.length > 0 ? allWarnings : void 0
1433
+ }
1160
1434
  });
1161
1435
  }
1162
- function instructionsToResponsesText(instructions) {
1163
- return typeof instructions === "string" ? instructions : contentBlocksToText(ensureResponsesTextBlocks(instructions, "instructions"));
1436
+ function* emitItemEvents(item, factory) {
1437
+ switch (item.type) {
1438
+ case "message":
1439
+ yield* emitMessageEvents(item, factory);
1440
+ break;
1441
+ case "reasoning":
1442
+ yield* emitReasoningEvents(item, factory);
1443
+ break;
1444
+ case "tool_call":
1445
+ yield* emitToolCallEvents(item, factory);
1446
+ break;
1447
+ case "opaque": break;
1448
+ }
1164
1449
  }
1165
- function assertResponsesToolResultOutcome(outcome) {
1166
- if (outcome !== "success") throw new AIRequestError(`responses does not preserve tool_result outcome "${outcome}"; only "success" is supported`, "UNSUPPORTED_TOOL_RESULT_OUTCOME");
1450
+ function* emitMessageEvents(item, factory) {
1451
+ const id = item.id ?? `syn-msg-${crypto.randomUUID()}`;
1452
+ yield factory.messageStarted(id);
1453
+ for (const block of item.content) yield factory.messageDelta(id, block);
1454
+ yield factory.messageCompleted(id);
1167
1455
  }
1168
- function parseSSE(chunk) {
1169
- const result = parseSSEEvents(chunk);
1170
- return {
1171
- events: result.events,
1172
- rest: result.rest,
1173
- malformedEvents: result.malformedEvents
1174
- };
1456
+ function* emitReasoningEvents(item, factory) {
1457
+ const id = item.id ?? `syn-reason-${crypto.randomUUID()}`;
1458
+ yield factory.reasoningStarted(id, item.visibility);
1459
+ for (const block of item.content) yield factory.reasoningDelta(id, block);
1460
+ yield factory.reasoningCompleted(id);
1175
1461
  }
1176
- function isReplayCanonicalInput(item) {
1177
- return item.type === "message" && item.role === "assistant" || item.type === "reasoning" || item.type === "function_call";
1462
+ function* emitToolCallEvents(item, factory) {
1463
+ yield factory.toolCallStarted(item.id, item.name);
1464
+ if (item.argumentsText) yield factory.toolCallDelta(item.id, { argumentsText: item.argumentsText });
1465
+ yield factory.toolCallCompleted(item.id);
1178
1466
  }
1179
- function rollbackTrailingReplayCanonicalItems(input) {
1180
- while (input.length > 0) {
1181
- const last = input[input.length - 1];
1182
- if (!last || !isReplayCanonicalInput(last)) break;
1183
- input.pop();
1467
+ //#endregion
1468
+ //#region src/helpers/incremental-stream-parser.ts
1469
+ var IncrementalStreamParser = class {
1470
+ split;
1471
+ parse;
1472
+ buffer = "";
1473
+ decoder = new TextDecoder();
1474
+ constructor(split, parse) {
1475
+ this.split = split;
1476
+ this.parse = parse;
1477
+ }
1478
+ feed(value) {
1479
+ this.buffer += this.decoder.decode(value, { stream: true });
1480
+ return this.consume(false);
1481
+ }
1482
+ flush() {
1483
+ this.buffer += this.decoder.decode();
1484
+ return this.consume(true);
1485
+ }
1486
+ getRemaining() {
1487
+ return this.buffer;
1488
+ }
1489
+ consume(allowEOF) {
1490
+ const split = this.split(this.buffer, allowEOF);
1491
+ this.buffer = split.rest;
1492
+ const items = [];
1493
+ let malformed = 0;
1494
+ for (const rawItem of split.items) {
1495
+ const result = this.parse(rawItem);
1496
+ if (result.status === "parsed") items.push(result.value);
1497
+ else if (result.status === "malformed") malformed++;
1498
+ }
1499
+ return {
1500
+ items,
1501
+ malformed
1502
+ };
1184
1503
  }
1185
- }
1186
- function canonicalToResponsesBlock(b) {
1187
- if (b.type === "text") return {
1188
- type: "text",
1189
- text: b.text
1504
+ };
1505
+ function splitLines(buffer, allowEOF) {
1506
+ const items = [];
1507
+ let cursor = 0;
1508
+ while (true) {
1509
+ const lineEnd = buffer.indexOf("\n", cursor);
1510
+ if (lineEnd === -1) break;
1511
+ items.push(buffer.slice(cursor, lineEnd));
1512
+ cursor = lineEnd + 1;
1513
+ }
1514
+ if (allowEOF && cursor < buffer.length) {
1515
+ items.push(buffer.slice(cursor));
1516
+ cursor = buffer.length;
1517
+ }
1518
+ return {
1519
+ items,
1520
+ rest: buffer.slice(cursor)
1190
1521
  };
1191
- if (b.type === "json") return {
1192
- type: "text",
1193
- text: JSON.stringify(b.json)
1522
+ }
1523
+ function splitSSEFrames(buffer, allowEOF) {
1524
+ const normalized = buffer.replaceAll("\r\n", "\n");
1525
+ const items = [];
1526
+ let cursor = 0;
1527
+ while (true) {
1528
+ const frameEnd = normalized.indexOf("\n\n", cursor);
1529
+ if (frameEnd === -1) break;
1530
+ items.push(normalized.slice(cursor, frameEnd));
1531
+ cursor = frameEnd + 2;
1532
+ }
1533
+ if (allowEOF && cursor < normalized.length) {
1534
+ items.push(normalized.slice(cursor));
1535
+ cursor = normalized.length;
1536
+ }
1537
+ return {
1538
+ items,
1539
+ rest: normalized.slice(cursor)
1194
1540
  };
1195
- throw new AIRequestError(`responses does not support content block type "${b.type}" in canonical mapping`, "UNSUPPORTED_CONTENT_BLOCK");
1196
1541
  }
1197
- var ResponsesAdapter = class extends AdapterBase {
1198
- kind = "responses";
1199
- nativeStreaming = true;
1200
- apiKey;
1542
+ //#endregion
1543
+ //#region src/helpers/request-mapper.ts
1544
+ var NormalizedRequestMapper = class {
1545
+ profile;
1546
+ constructor(profile) {
1547
+ this.profile = profile;
1548
+ }
1549
+ mapInstructions(instructions) {
1550
+ return typeof instructions === "string" ? instructions : contentBlocksToText(this.ensureTextBlocks(instructions, "instructions"));
1551
+ }
1552
+ ensureTextBlocks(blocks, field) {
1553
+ return this.ensureBlocks(blocks, field, this.profile.supportedBlockTypes, "only text/json blocks are supported");
1554
+ }
1555
+ ensureReasoningBlocks(blocks, field) {
1556
+ return this.ensureBlocks(blocks, field, this.profile.reasoningBlockTypes, "reasoning only supports text blocks");
1557
+ }
1558
+ assertToolResultOutcome(outcome) {
1559
+ if (this.profile.capabilities.toolResultOutcomes.includes(outcome)) return;
1560
+ const outcomes = this.profile.capabilities.toolResultOutcomes;
1561
+ const supported = outcomes.map((value) => `"${value}"`).join(" and ");
1562
+ const verb = outcomes.length > 1 ? "are" : "is";
1563
+ throw new AIRequestError(`${this.profile.kind} does not preserve tool_result outcome "${outcome}"; only ${supported} ${verb} supported`, "UNSUPPORTED_TOOL_RESULT_OUTCOME");
1564
+ }
1565
+ rollbackTrailingAssistantMessages(messages) {
1566
+ while (messages.length > 0 && messages[messages.length - 1]?.role === "assistant") messages.pop();
1567
+ }
1568
+ ensureBlocks(blocks, field, supportedTypes, description) {
1569
+ for (let i = 0; i < blocks.length; i++) {
1570
+ const block = blocks[i];
1571
+ if (block && !supportedTypes.includes(block.type)) throw new AIRequestError(`${this.profile.kind} does not support ${field}[${i}] of type "${block.type}"; ${description}`, "UNSUPPORTED_CONTENT_BLOCK");
1572
+ }
1573
+ return blocks;
1574
+ }
1575
+ };
1576
+ //#endregion
1577
+ //#region src/adapters/responses.ts
1578
+ /**
1579
+ * Responses Adapter
1580
+ *
1581
+ * 接入 OpenAI Responses API (responses 端点)。
1582
+ * 职责分层:
1583
+ * 1. buildRequest — 将 NormalizedRequest 转换为 Responses API 请求
1584
+ * 2. runStream — 调用 API、解析 SSE、发射 canonical 事件
1585
+ *
1586
+ * 支持消息流 / reasoning 流 / tool_call 流及高保真 replay。
1587
+ */
1588
+ const profile$3 = {
1589
+ kind: "responses",
1590
+ instructionsMode: "instructions_field",
1591
+ supportedBlockTypes: ["text", "json"],
1592
+ reasoningBlockTypes: ["text"],
1593
+ capabilities: {
1594
+ textStreaming: "native",
1595
+ reasoningStreaming: "native",
1596
+ toolCallStreaming: "native",
1597
+ replay: "opaque",
1598
+ usage: "final",
1599
+ toolResultOutcomes: ["success"]
1600
+ }
1601
+ };
1602
+ const mapper$3 = new NormalizedRequestMapper(profile$3);
1603
+ /** 已处理或可安全忽略的 Responses SSE 类型(未知类型会 warning 一次)。 */
1604
+ const KNOWN_RESPONSES_SSE_TYPES = /* @__PURE__ */ new Set([
1605
+ "response.output_item.added",
1606
+ "response.output_item.done",
1607
+ "response.output_text.delta",
1608
+ "response.output_text.done",
1609
+ "response.reasoning.delta",
1610
+ "response.reasoning.done",
1611
+ "response.tool_call.delta",
1612
+ "response.tool_call.done",
1613
+ "response.function_call_arguments.delta",
1614
+ "response.function_call_arguments.done",
1615
+ "response.content_part.added",
1616
+ "response.content_part.done",
1617
+ "response.refusal.delta",
1618
+ "response.refusal.done",
1619
+ "response.in_progress",
1620
+ "response.created",
1621
+ "response.completed",
1622
+ "response.failed",
1623
+ "response.incomplete",
1624
+ "error"
1625
+ ]);
1626
+ function isReplayCanonicalInput(item) {
1627
+ return item.type === "message" && item.role === "assistant" || item.type === "reasoning" || item.type === "function_call";
1628
+ }
1629
+ function hasReplayCanonicalInput(input) {
1630
+ return input.some(isReplayCanonicalInput);
1631
+ }
1632
+ function extractFailureMessage(response) {
1633
+ return response.error?.message ?? response.failure?.message ?? "unknown";
1634
+ }
1635
+ function canonicalToResponsesBlock(b) {
1636
+ if (b.type === "text") return {
1637
+ type: "text",
1638
+ text: b.text
1639
+ };
1640
+ if (b.type === "json") return {
1641
+ type: "text",
1642
+ text: JSON.stringify(b.json)
1643
+ };
1644
+ throw new AIRequestError(`responses does not support content block type "${b.type}" in canonical mapping`, "UNSUPPORTED_CONTENT_BLOCK");
1645
+ }
1646
+ var ResponsesAdapter = class extends AdapterBase {
1647
+ kind = "responses";
1648
+ capabilities = profile$3.capabilities;
1649
+ apiKey;
1201
1650
  baseUrl;
1202
1651
  fetchFn;
1203
1652
  constructor(options) {
@@ -1211,7 +1660,7 @@ var ResponsesAdapter = class extends AdapterBase {
1211
1660
  for (const item of request.input) switch (item.type) {
1212
1661
  case "message":
1213
1662
  if (item.role === "assistant") {
1214
- const blocks = ensureResponsesTextBlocks(item.content, `assistant message (${item.role}) content`).map(canonicalToResponsesBlock);
1663
+ const blocks = mapper$3.ensureTextBlocks(item.content, `assistant message (${item.role}) content`).map(canonicalToResponsesBlock);
1215
1664
  input.push({
1216
1665
  type: "message",
1217
1666
  role: item.role,
@@ -1220,11 +1669,11 @@ var ResponsesAdapter = class extends AdapterBase {
1220
1669
  } else input.push({
1221
1670
  type: "message",
1222
1671
  role: item.role,
1223
- content: contentBlocksToText(ensureResponsesTextBlocks(item.content, `input message (${item.role}) content`))
1672
+ content: contentBlocksToText(mapper$3.ensureTextBlocks(item.content, `input message (${item.role}) content`))
1224
1673
  });
1225
1674
  break;
1226
1675
  case "reasoning": {
1227
- const blocks = ensureResponsesReasoningBlocks(item.content, "reasoning content").map((b) => ({
1676
+ const blocks = mapper$3.ensureReasoningBlocks(item.content, "reasoning content").map((b) => ({
1228
1677
  type: "reasoning",
1229
1678
  text: b.text
1230
1679
  }));
@@ -1243,8 +1692,8 @@ var ResponsesAdapter = class extends AdapterBase {
1243
1692
  });
1244
1693
  break;
1245
1694
  case "tool_result": {
1246
- assertResponsesToolResultOutcome(item.outcome);
1247
- const output = ensureResponsesTextBlocks(item.content, `tool_result ${item.callId} content`).map(blockToText).join("\n");
1695
+ mapper$3.assertToolResultOutcome(item.outcome);
1696
+ const output = mapper$3.ensureTextBlocks(item.content, `tool_result ${item.callId} content`).map(blockToText).join("\n");
1248
1697
  input.push({
1249
1698
  type: "function_call_output",
1250
1699
  call_id: item.callId,
@@ -1252,25 +1701,26 @@ var ResponsesAdapter = class extends AdapterBase {
1252
1701
  });
1253
1702
  break;
1254
1703
  }
1255
- case "opaque":
1256
- if (item.source === "responses" && item.purpose === "replay" && typeof item.payload === "object" && item.payload !== null && "id" in item.payload) {
1257
- const { id } = item.payload;
1258
- if (typeof id === "string") {
1259
- rollbackTrailingReplayCanonicalItems(input);
1260
- input.push({
1261
- type: "item_reference",
1262
- id
1263
- });
1264
- }
1704
+ case "opaque": {
1705
+ if (item.source !== "responses" || item.purpose !== "replay") break;
1706
+ assertOpaqueReplayEnvelope(item.payload);
1707
+ const payload = item.payload;
1708
+ if ("id" in payload) {
1709
+ if (typeof payload.id !== "string" || payload.id.length === 0 || payload.id.length > 256) throw new AIRequestError("Invalid opaque replay payload: id must be a non-empty string (max 256)", "INVALID_OPAQUE_REPLAY");
1710
+ if (!hasReplayCanonicalInput(input)) input.push({
1711
+ type: "item_reference",
1712
+ id: payload.id
1713
+ });
1265
1714
  }
1266
1715
  break;
1716
+ }
1267
1717
  }
1268
1718
  const body = {
1269
1719
  model: request.model,
1270
1720
  input,
1271
1721
  stream: true
1272
1722
  };
1273
- if (request.instructions) body.instructions = instructionsToResponsesText(request.instructions);
1723
+ if (request.instructions) body.instructions = mapper$3.mapInstructions(request.instructions);
1274
1724
  if (request.tools && request.tools.length > 0) body.tools = request.tools.map((t) => ({
1275
1725
  type: "function",
1276
1726
  name: t.name,
@@ -1292,31 +1742,59 @@ var ResponsesAdapter = class extends AdapterBase {
1292
1742
  }
1293
1743
  async *runStream(providerRequest, factory, request) {
1294
1744
  const auxiliary = this.createAuxiliaryState(request);
1295
- const response = await this.fetchFn(`${this.baseUrl}/responses`, {
1296
- method: "POST",
1297
- headers: {
1298
- "Content-Type": "application/json",
1299
- Authorization: `Bearer ${this.apiKey}`
1300
- },
1301
- body: JSON.stringify(providerRequest)
1302
- });
1745
+ let response;
1746
+ try {
1747
+ response = await this.fetchFn(`${this.baseUrl}/responses`, {
1748
+ method: "POST",
1749
+ headers: {
1750
+ "Content-Type": "application/json",
1751
+ Authorization: `Bearer ${this.apiKey}`
1752
+ },
1753
+ body: JSON.stringify(providerRequest)
1754
+ });
1755
+ } catch (err) {
1756
+ throw new AIProviderError(err instanceof Error ? err.message : String(err), "PROVIDER_ERROR");
1757
+ }
1303
1758
  if (!response.ok) {
1304
- const errorText = await response.text().catch(() => "unknown error");
1305
- throw new Error(`Responses API error ${response.status}: ${errorText}`);
1759
+ const errorBody = await response.text().catch(() => "");
1760
+ throw providerHttpError(response.status, errorBody);
1306
1761
  }
1307
1762
  const reader = response.body?.getReader();
1308
- if (!reader) throw new Error("Response body is not readable");
1763
+ if (!reader) throw new AIStreamError("Response body is not readable", "STREAM_ERROR");
1764
+ const parser = new IncrementalStreamParser(splitSSEFrames, (frame) => {
1765
+ let eventType = "";
1766
+ let dataStr = "";
1767
+ for (const rawLine of frame.split("\n")) {
1768
+ const line = rawLine.trim();
1769
+ if (line.startsWith("event: ")) eventType = line.slice(7).trim();
1770
+ else if (line.startsWith("data: ")) dataStr += line.slice(6);
1771
+ }
1772
+ if (!eventType) return { status: "ignored" };
1773
+ try {
1774
+ const data = JSON.parse(dataStr);
1775
+ return {
1776
+ status: "parsed",
1777
+ value: {
1778
+ type: eventType,
1779
+ data
1780
+ }
1781
+ };
1782
+ } catch {
1783
+ return { status: "malformed" };
1784
+ }
1785
+ });
1309
1786
  const output = [];
1310
- const decoder = new TextDecoder();
1311
- let buffer = "";
1787
+ let streamDone = false;
1312
1788
  let completedResponse;
1789
+ let completedEmitted = false;
1790
+ let unknownEventsWarned = false;
1791
+ const messageItemsWithDelta = /* @__PURE__ */ new Set();
1313
1792
  try {
1314
1793
  while (true) {
1315
- const { done, value } = await reader.read();
1316
- if (done) break;
1317
- buffer += decoder.decode(value, { stream: true });
1318
- const { events, rest, malformedEvents } = parseSSE(buffer);
1319
- buffer = rest;
1794
+ const { done, value } = await reader.read().catch((err) => {
1795
+ throw new AIStreamError(`Failed to read response stream: ${err instanceof Error ? err.message : String(err)}`, "STREAM_ERROR");
1796
+ });
1797
+ const { items: events, malformed: malformedEvents } = done ? parser.flush() : parser.feed(value);
1320
1798
  const malformedWarning = emitMalformedStreamWarning(factory, {
1321
1799
  count: malformedEvents,
1322
1800
  providerLabel: "Responses",
@@ -1325,7 +1803,8 @@ var ResponsesAdapter = class extends AdapterBase {
1325
1803
  if (malformedWarning) yield malformedWarning;
1326
1804
  for (const sseEvent of events) {
1327
1805
  if (sseEvent.type === "error") {
1328
- yield factory.responseWarning(sseEvent.data.message, sseEvent.data.code);
1806
+ const data = sseEvent.data;
1807
+ yield factory.responseWarning(data.message ?? "Provider error event", data.code);
1329
1808
  continue;
1330
1809
  }
1331
1810
  if (sseEvent.type === "response.output_item.added") {
@@ -1344,40 +1823,69 @@ var ResponsesAdapter = class extends AdapterBase {
1344
1823
  continue;
1345
1824
  }
1346
1825
  if (sseEvent.type === "response.output_text.delta") {
1347
- yield factory.messageDelta(sseEvent.data.item_id, sseEvent.data.delta);
1826
+ const data = sseEvent.data;
1827
+ yield factory.messageDelta(data.item_id, textBlock(data.delta));
1828
+ messageItemsWithDelta.add(data.item_id);
1348
1829
  continue;
1349
1830
  }
1350
1831
  if (sseEvent.type === "response.output_text.done") {
1351
- yield factory.messageCompleted(messageItem([textBlock(sseEvent.data.text)], { id: sseEvent.data.item_id }));
1352
- output.push(messageItem([textBlock(sseEvent.data.text)], { id: sseEvent.data.item_id }));
1832
+ const data = sseEvent.data;
1833
+ if (!messageItemsWithDelta.has(data.item_id) && data.text) yield factory.messageDelta(data.item_id, textBlock(data.text));
1834
+ yield factory.messageCompleted(data.item_id);
1835
+ output.push(messageItem([textBlock(data.text)], { id: data.item_id }));
1353
1836
  continue;
1354
1837
  }
1355
1838
  if (sseEvent.type === "response.reasoning.delta") {
1356
- yield factory.reasoningDelta(sseEvent.data.item_id, textBlock(sseEvent.data.delta));
1839
+ const data = sseEvent.data;
1840
+ yield factory.reasoningDelta(data.item_id, textBlock(data.delta));
1357
1841
  continue;
1358
1842
  }
1359
1843
  if (sseEvent.type === "response.reasoning.done") {
1360
- yield factory.reasoningCompleted(reasoningItem([textBlock(sseEvent.data.text)], "full", sseEvent.data.item_id));
1361
- output.push(reasoningItem([textBlock(sseEvent.data.text)], "full", sseEvent.data.item_id));
1844
+ const data = sseEvent.data;
1845
+ yield factory.reasoningCompleted(data.item_id);
1846
+ output.push(reasoningItem([textBlock(data.text)], "full", data.item_id));
1362
1847
  continue;
1363
1848
  }
1364
1849
  if (sseEvent.type === "response.tool_call.delta") {
1365
- if (sseEvent.data.delta.arguments) yield factory.toolCallDelta(sseEvent.data.item_id, { argumentsText: sseEvent.data.delta.arguments });
1850
+ const data = sseEvent.data;
1851
+ if (data.delta.arguments) yield factory.toolCallDelta(data.item_id, { argumentsText: data.delta.arguments });
1366
1852
  continue;
1367
1853
  }
1368
1854
  if (sseEvent.type === "response.tool_call.done") {
1369
- const tcItem = toolCallItem(sseEvent.data.item_id, sseEvent.data.name ?? "unknown", sseEvent.data.arguments ?? "");
1370
- yield factory.toolCallCompleted(tcItem);
1855
+ const data = sseEvent.data;
1856
+ const tcItem = toolCallItem(data.item_id, data.name ?? "unknown", data.arguments ?? "");
1857
+ yield factory.toolCallCompleted(data.item_id);
1371
1858
  output.push(tcItem);
1372
1859
  continue;
1373
1860
  }
1374
- if (sseEvent.type === "response.completed") completedResponse = sseEvent.data.response;
1861
+ if (sseEvent.type === "response.completed" || sseEvent.type === "response.failed" || sseEvent.type === "response.incomplete") {
1862
+ const data = sseEvent.data;
1863
+ if (completedResponse) {
1864
+ yield factory.responseWarning("Duplicate finish signal ignored", "DUPLICATE_FINISH");
1865
+ continue;
1866
+ }
1867
+ completedResponse = data.response;
1868
+ if (sseEvent.type === "response.failed") yield factory.responseWarning(`Response failed: ${extractFailureMessage(data.response)}`, "PROVIDER_FAILURE");
1869
+ continue;
1870
+ }
1871
+ if (!KNOWN_RESPONSES_SSE_TYPES.has(sseEvent.type) && !unknownEventsWarned) {
1872
+ unknownEventsWarned = true;
1873
+ yield factory.responseWarning(`Responses API sent unknown event type "${sseEvent.type}"; this may indicate an incomplete integration`, "UNKNOWN_PROVIDER_EVENT");
1874
+ }
1875
+ }
1876
+ if (done) {
1877
+ streamDone = true;
1878
+ break;
1375
1879
  }
1376
1880
  }
1377
1881
  } finally {
1378
- reader.releaseLock();
1882
+ try {
1883
+ if (!streamDone) await reader.cancel().catch(() => void 0);
1884
+ } finally {
1885
+ reader.releaseLock();
1886
+ }
1379
1887
  }
1380
- if (buffer.trim().length > 0) yield factory.responseWarning("Stream ended with an incomplete Responses SSE frame", "STREAM_ERROR");
1888
+ if (parser.getRemaining().trim().length > 0) yield factory.responseWarning("Stream ended with an incomplete Responses SSE frame", "STREAM_ERROR");
1381
1889
  let rawResponseId;
1382
1890
  if (completedResponse) {
1383
1891
  rawResponseId = completedResponse.id;
@@ -1388,23 +1896,44 @@ var ResponsesAdapter = class extends AdapterBase {
1388
1896
  const stopReason = completedResponse ? this.inferStopReason(completedResponse) : void 0;
1389
1897
  const auxiliaryResult = await auxiliary.finalize(factory);
1390
1898
  for (const event of auxiliaryResult.events) yield event;
1391
- yield factory.responseCompleted(this.buildResponse(request, {
1392
- output,
1393
- replay,
1394
- stopReason,
1395
- usage: auxiliaryResult.usage,
1396
- billing: auxiliaryResult.billing,
1397
- auxiliary: auxiliaryResult.auxiliary,
1398
- warnings: auxiliaryResult.warnings,
1399
- metadataSources: auxiliaryResult.metadataSources,
1400
- rawResponseId
1401
- }, factory));
1899
+ if (!completedEmitted) {
1900
+ completedEmitted = true;
1901
+ const finalResponse = this.buildResponse(request, {
1902
+ output,
1903
+ replay,
1904
+ stopReason,
1905
+ usage: auxiliaryResult.usage,
1906
+ billing: auxiliaryResult.billing,
1907
+ auxiliary: auxiliaryResult.auxiliary,
1908
+ warnings: auxiliaryResult.warnings,
1909
+ metadataSources: auxiliaryResult.metadataSources,
1910
+ rawResponseId
1911
+ }, factory);
1912
+ yield factory.responseCompleted({
1913
+ replay: finalResponse.replay,
1914
+ stopReason: finalResponse.stopReason,
1915
+ trace: finalResponse.backend,
1916
+ usage: finalResponse.usage,
1917
+ billing: finalResponse.billing,
1918
+ auxiliary: finalResponse.auxiliary,
1919
+ warnings: finalResponse.warnings
1920
+ });
1921
+ }
1402
1922
  }
1403
1923
  inferStopReason(response) {
1924
+ if (response.status === "failed") return "error";
1925
+ if (response.status === "incomplete") {
1926
+ const reason = response.incomplete_details?.reason;
1927
+ if (reason === "content_filter") return "content_filter";
1928
+ if (reason === "max_output_tokens") return "max_output_tokens";
1929
+ return "max_output_tokens";
1930
+ }
1404
1931
  const output = response.output;
1405
- if (!output || output.length === 0) return "unknown";
1932
+ if (!output || output.length === 0) return response.status === "completed" ? "end_turn" : "unknown";
1406
1933
  if (output.some((item) => item.type === "function_call")) return "tool_call";
1407
- if (output[output.length - 1]?.status === "incomplete") return "max_output_tokens";
1934
+ const lastItem = output[output.length - 1];
1935
+ if (lastItem?.status === "failed") return "error";
1936
+ if (lastItem?.status === "incomplete") return "max_output_tokens";
1408
1937
  return "end_turn";
1409
1938
  }
1410
1939
  };
@@ -1421,36 +1950,41 @@ var ResponsesAdapter = class extends AdapterBase {
1421
1950
  * - 高保真 replay(含 opaque continuation)
1422
1951
  * - 能力降级 warning
1423
1952
  */
1424
- function ensureMessagesTextBlocks(blocks, field) {
1425
- for (let i = 0; i < blocks.length; i++) {
1426
- const block = blocks[i];
1427
- if (!block) continue;
1428
- if (block.type !== "text" && block.type !== "json") throw new AIRequestError(`messages does not support ${field}[${i}] of type "${block.type}"; only text/json blocks are supported`, "UNSUPPORTED_CONTENT_BLOCK");
1429
- }
1430
- return blocks;
1431
- }
1432
- function ensureMessagesReasoningBlocks(blocks, field) {
1433
- return blocks.map((block, index) => {
1434
- if (block.type !== "text") throw new AIRequestError(`messages does not support ${field}[${index}] of type "${block.type}"; reasoning only supports text blocks`, "UNSUPPORTED_CONTENT_BLOCK");
1435
- return block;
1436
- });
1437
- }
1438
- function instructionsToMessagesText(instructions) {
1439
- return typeof instructions === "string" ? instructions : contentBlocksToText(ensureMessagesTextBlocks(instructions, "instructions"));
1440
- }
1441
- function assertMessagesToolResultOutcome(outcome) {
1442
- if (outcome === "rejected") throw new AIRequestError("messages does not preserve tool_result outcome \"rejected\"; only \"success\" and \"error\" are supported", "UNSUPPORTED_TOOL_RESULT_OUTCOME");
1443
- }
1444
- function parseMessagesSSE(chunk) {
1445
- const result = parseSSEEvents(chunk);
1446
- return {
1447
- events: result.events,
1448
- rest: result.rest,
1449
- malformedEvents: result.malformedEvents
1450
- };
1953
+ const profile$2 = {
1954
+ kind: "messages",
1955
+ instructionsMode: "system_message",
1956
+ supportedBlockTypes: ["text", "json"],
1957
+ reasoningBlockTypes: ["text"],
1958
+ capabilities: {
1959
+ textStreaming: "native",
1960
+ reasoningStreaming: "native",
1961
+ toolCallStreaming: "synthetic",
1962
+ replay: "opaque",
1963
+ usage: "stream",
1964
+ toolResultOutcomes: ["success", "error"]
1965
+ }
1966
+ };
1967
+ const mapper$2 = new NormalizedRequestMapper(profile$2);
1968
+ function isMessagesReplayContentBlock(value) {
1969
+ if (!value || typeof value !== "object" || !("type" in value)) return false;
1970
+ const block = value;
1971
+ switch (block.type) {
1972
+ case "text": return typeof block.text === "string";
1973
+ case "thinking": return typeof block.thinking === "string" && (block.signature === void 0 || typeof block.signature === "string");
1974
+ case "redacted_thinking": return typeof block.data === "string";
1975
+ case "tool_use": return typeof block.id === "string" && typeof block.name === "string" && !!block.input && typeof block.input === "object" && !Array.isArray(block.input);
1976
+ case "tool_result":
1977
+ if (typeof block.tool_use_id !== "string") return false;
1978
+ if (block.is_error !== void 0 && typeof block.is_error !== "boolean") return false;
1979
+ if (typeof block.content === "string") return true;
1980
+ if (!Array.isArray(block.content)) return false;
1981
+ return block.content.every(isMessagesReplayContentBlock);
1982
+ default: return false;
1983
+ }
1451
1984
  }
1452
- function rollbackTrailingAssistantMessages$2(messages) {
1453
- while (messages.length > 0 && messages[messages.length - 1]?.role === "assistant") messages.pop();
1985
+ function assertMessagesReplayContent(content) {
1986
+ if (!Array.isArray(content)) throw new AIRequestError("Invalid opaque replay payload: content must be an array", "INVALID_OPAQUE_REPLAY");
1987
+ for (let i = 0; i < content.length; i++) if (!isMessagesReplayContentBlock(content[i])) throw new AIRequestError(`Invalid opaque replay payload: content[${i}] is not a valid Messages content block`, "INVALID_OPAQUE_REPLAY");
1454
1988
  }
1455
1989
  /** 用 response 级别的命名空间合成 content block 的 item ID,避免多轮工具循环 ID 碰撞 */
1456
1990
  function synthesizeItemId(kind, blockIndex, responseId) {
@@ -1500,34 +2034,29 @@ function buildStreamMetadata(options) {
1500
2034
  }
1501
2035
  var MessagesAdapter = class extends AdapterBase {
1502
2036
  kind = "messages";
1503
- nativeStreaming = true;
2037
+ capabilities = profile$2.capabilities;
1504
2038
  apiKey;
1505
2039
  apiVersion;
1506
2040
  baseUrl;
1507
2041
  fetchFn;
1508
- warningAccumulator;
1509
2042
  constructor(options) {
1510
2043
  super();
1511
2044
  this.apiKey = options.apiKey;
1512
2045
  this.apiVersion = options.apiVersion ?? "2023-06-01";
1513
2046
  this.baseUrl = options.baseUrl ?? "https://api.anthropic.com/v1";
1514
2047
  this.fetchFn = options.fetch ?? globalThis.fetch;
1515
- this.warningAccumulator = [];
1516
- }
1517
- warn(message, _code) {
1518
- this.warningAccumulator.push(message);
1519
2048
  }
1520
2049
  buildRequest(request) {
1521
2050
  const messages = [];
1522
2051
  let systemPrompt;
1523
2052
  let pendingToolResultMessage;
1524
- if (request.instructions) systemPrompt = instructionsToMessagesText(request.instructions);
2053
+ if (request.instructions) systemPrompt = mapper$2.mapInstructions(request.instructions);
1525
2054
  for (const item of request.input) {
1526
2055
  if (item.type !== "tool_result") pendingToolResultMessage = void 0;
1527
2056
  switch (item.type) {
1528
2057
  case "message": {
1529
2058
  const role = item.role === "user" ? "user" : "assistant";
1530
- const supportedContent = ensureMessagesTextBlocks(item.content, `input message (${item.role}) content`);
2059
+ const supportedContent = mapper$2.ensureTextBlocks(item.content, `input message (${item.role}) content`);
1531
2060
  if (supportedContent.length === 1 && supportedContent[0]?.type === "text") messages.push({
1532
2061
  role,
1533
2062
  content: supportedContent[0].text
@@ -1554,8 +2083,8 @@ var MessagesAdapter = class extends AdapterBase {
1554
2083
  break;
1555
2084
  }
1556
2085
  case "tool_result": {
1557
- assertMessagesToolResultOutcome(item.outcome);
1558
- const content = ensureMessagesTextBlocks(item.content, `tool_result ${item.callId} content`).map(blockToText).join("\n");
2086
+ mapper$2.assertToolResultOutcome(item.outcome);
2087
+ const content = mapper$2.ensureTextBlocks(item.content, `tool_result ${item.callId} content`).map(blockToText).join("\n");
1559
2088
  const block = {
1560
2089
  type: "tool_result",
1561
2090
  tool_use_id: item.callId,
@@ -1575,7 +2104,7 @@ var MessagesAdapter = class extends AdapterBase {
1575
2104
  case "reasoning": {
1576
2105
  const block = {
1577
2106
  type: "thinking",
1578
- thinking: contentBlocksToText(ensureMessagesReasoningBlocks(item.content, "reasoning content"))
2107
+ thinking: contentBlocksToText(mapper$2.ensureReasoningBlocks(item.content, "reasoning content"))
1579
2108
  };
1580
2109
  const lastMsg = messages[messages.length - 1];
1581
2110
  if (lastMsg && lastMsg.role === "assistant" && typeof lastMsg.content !== "string") lastMsg.content.push(block);
@@ -1585,20 +2114,20 @@ var MessagesAdapter = class extends AdapterBase {
1585
2114
  });
1586
2115
  break;
1587
2116
  }
1588
- case "opaque":
1589
- if (item.purpose === "replay" && typeof item.payload === "object" && item.payload !== null) {
1590
- const payload = item.payload;
1591
- if (payload.role === "assistant" && Array.isArray(payload.content)) {
1592
- if (payload.content.every((b) => typeof b === "object" && b !== null && "type" in b && (b.type === "text" || b.type === "thinking" || b.type === "redacted_thinking" || b.type === "tool_use" || b.type === "tool_result"))) {
1593
- rollbackTrailingAssistantMessages$2(messages);
1594
- messages.push({
1595
- role: "assistant",
1596
- content: payload.content
1597
- });
1598
- }
1599
- }
2117
+ case "opaque": {
2118
+ if (item.purpose !== "replay") break;
2119
+ assertOpaqueReplayEnvelope(item.payload);
2120
+ const payload = item.payload;
2121
+ if (payload.role === "assistant" && "content" in payload) {
2122
+ assertMessagesReplayContent(payload.content);
2123
+ mapper$2.rollbackTrailingAssistantMessages(messages);
2124
+ messages.push({
2125
+ role: "assistant",
2126
+ content: payload.content
2127
+ });
1600
2128
  }
1601
2129
  break;
2130
+ }
1602
2131
  }
1603
2132
  }
1604
2133
  const body = {
@@ -1625,27 +2154,53 @@ var MessagesAdapter = class extends AdapterBase {
1625
2154
  return body;
1626
2155
  }
1627
2156
  async *runStream(providerRequest, factory, request) {
1628
- this.warningAccumulator = [];
1629
2157
  const auxiliary = this.createAuxiliaryState(request);
2158
+ let completedEmitted = false;
1630
2159
  if (request.metadata) yield factory.responseWarning("Request metadata is not supported by the Messages adapter", "UNSUPPORTED_METADATA");
1631
- const response = await this.fetchFn(`${this.baseUrl}/messages`, {
1632
- method: "POST",
1633
- headers: {
1634
- "Content-Type": "application/json",
1635
- "x-api-key": this.apiKey,
1636
- "anthropic-version": this.apiVersion
1637
- },
1638
- body: JSON.stringify(providerRequest)
1639
- });
2160
+ let response;
2161
+ try {
2162
+ response = await this.fetchFn(`${this.baseUrl}/messages`, {
2163
+ method: "POST",
2164
+ headers: {
2165
+ "Content-Type": "application/json",
2166
+ "x-api-key": this.apiKey,
2167
+ "anthropic-version": this.apiVersion
2168
+ },
2169
+ body: JSON.stringify(providerRequest)
2170
+ });
2171
+ } catch (err) {
2172
+ throw new AIProviderError(err instanceof Error ? err.message : String(err), "PROVIDER_ERROR");
2173
+ }
1640
2174
  if (!response.ok) {
1641
- const errorText = await response.text().catch(() => "unknown error");
1642
- throw new Error(`Messages API error ${response.status}: ${errorText}`);
2175
+ const errorBody = await response.text().catch(() => "");
2176
+ throw providerHttpError(response.status, errorBody);
1643
2177
  }
1644
2178
  const reader = response.body?.getReader();
1645
- if (!reader) throw new Error("Response body is not readable");
2179
+ if (!reader) throw new AIStreamError("Response body is not readable", "STREAM_ERROR");
2180
+ const parser = new IncrementalStreamParser(splitSSEFrames, (frame) => {
2181
+ let eventType = "";
2182
+ let dataStr = "";
2183
+ for (const rawLine of frame.split("\n")) {
2184
+ const line = rawLine.trim();
2185
+ if (line.startsWith("event: ")) eventType = line.slice(7).trim();
2186
+ else if (line.startsWith("data: ")) dataStr += line.slice(6);
2187
+ }
2188
+ if (!eventType) return { status: "ignored" };
2189
+ try {
2190
+ const data = JSON.parse(dataStr);
2191
+ return {
2192
+ status: "parsed",
2193
+ value: {
2194
+ type: eventType,
2195
+ data
2196
+ }
2197
+ };
2198
+ } catch {
2199
+ return { status: "malformed" };
2200
+ }
2201
+ });
1646
2202
  const output = [];
1647
- const decoder = new TextDecoder();
1648
- let buffer = "";
2203
+ let streamDone = false;
1649
2204
  let messageResponse;
1650
2205
  let currentContentBlockIndex = -1;
1651
2206
  let currentItemType = null;
@@ -1667,11 +2222,10 @@ var MessagesAdapter = class extends AdapterBase {
1667
2222
  }
1668
2223
  try {
1669
2224
  while (true) {
1670
- const { done, value } = await reader.read();
1671
- if (done) break;
1672
- buffer += decoder.decode(value, { stream: true });
1673
- const { events, rest, malformedEvents } = parseMessagesSSE(buffer);
1674
- buffer = rest;
2225
+ const { done, value } = await reader.read().catch((err) => {
2226
+ throw new AIStreamError(`Failed to read response stream: ${err instanceof Error ? err.message : String(err)}`, "STREAM_ERROR");
2227
+ });
2228
+ const { items: events, malformed: malformedEvents } = done ? parser.flush() : parser.feed(value);
1675
2229
  const malformedWarning = emitMalformedStreamWarning(factory, {
1676
2230
  count: malformedEvents,
1677
2231
  providerLabel: "Messages",
@@ -1683,7 +2237,6 @@ var MessagesAdapter = class extends AdapterBase {
1683
2237
  case "error": {
1684
2238
  const err = sseEvent.data.error;
1685
2239
  yield factory.responseWarning(err.message, err.type);
1686
- this.warn(err.message, err.type);
1687
2240
  continue;
1688
2241
  }
1689
2242
  case "message_start":
@@ -1718,7 +2271,7 @@ var MessagesAdapter = class extends AdapterBase {
1718
2271
  yield factory.reasoningStarted(currentItemId, "redacted");
1719
2272
  yield factory.reasoningDelta(currentItemId, textBlock(data));
1720
2273
  const redactedItem = reasoningItem([textBlock(data)], "redacted", currentItemId);
1721
- yield factory.reasoningCompleted(redactedItem);
2274
+ yield factory.reasoningCompleted(currentItemId);
1722
2275
  output.push(redactedItem);
1723
2276
  rawReplayContent.push({
1724
2277
  type: "redacted_thinking",
@@ -1747,7 +2300,7 @@ var MessagesAdapter = class extends AdapterBase {
1747
2300
  if (currentItemType === "message" && currentItemId) {
1748
2301
  const txt = delta.text;
1749
2302
  textBuffer += txt;
1750
- yield factory.messageDelta(currentItemId, txt);
2303
+ yield factory.messageDelta(currentItemId, textBlock(txt));
1751
2304
  }
1752
2305
  break;
1753
2306
  case "thinking_delta":
@@ -1769,14 +2322,14 @@ var MessagesAdapter = class extends AdapterBase {
1769
2322
  }
1770
2323
  case "content_block_stop":
1771
2324
  if (currentItemType === "message" && currentItemId) {
1772
- yield factory.messageCompleted(messageItem([textBlock(textBuffer)], { id: currentItemId }));
2325
+ yield factory.messageCompleted(currentItemId);
1773
2326
  output.push(messageItem([textBlock(textBuffer)], { id: currentItemId }));
1774
2327
  rawReplayContent.push({
1775
2328
  type: "text",
1776
2329
  text: textBuffer
1777
2330
  });
1778
2331
  } else if (currentItemType === "reasoning" && currentItemId && currentThinkingVisibility !== "redacted") {
1779
- yield factory.reasoningCompleted(reasoningItem([textBlock(thinkingBuffer)], currentThinkingVisibility, currentItemId));
2332
+ yield factory.reasoningCompleted(currentItemId);
1780
2333
  output.push(reasoningItem([textBlock(thinkingBuffer)], currentThinkingVisibility, currentItemId));
1781
2334
  rawReplayContent.push({
1782
2335
  type: "thinking",
@@ -1784,7 +2337,7 @@ var MessagesAdapter = class extends AdapterBase {
1784
2337
  });
1785
2338
  } else if (currentItemType === "tool_call" && currentItemId) {
1786
2339
  const tcItem = toolCallItem(currentItemId, currentToolName, currentArgsText || argsBuffer);
1787
- yield factory.toolCallCompleted(tcItem);
2340
+ yield factory.toolCallCompleted(currentItemId);
1788
2341
  output.push(tcItem);
1789
2342
  rawReplayContent.push({
1790
2343
  type: "tool_use",
@@ -1805,11 +2358,19 @@ var MessagesAdapter = class extends AdapterBase {
1805
2358
  }
1806
2359
  case "message_stop": break;
1807
2360
  }
2361
+ if (done) {
2362
+ streamDone = true;
2363
+ break;
2364
+ }
1808
2365
  }
1809
2366
  } finally {
1810
- reader.releaseLock();
2367
+ try {
2368
+ if (!streamDone) await reader.cancel().catch(() => void 0);
2369
+ } finally {
2370
+ reader.releaseLock();
2371
+ }
1811
2372
  }
1812
- if (buffer.trim().length > 0) yield factory.responseWarning("Stream ended with an incomplete Messages SSE frame", "STREAM_ERROR");
2373
+ if (parser.getRemaining().trim().length > 0) yield factory.responseWarning("Stream ended with an incomplete Messages SSE frame", "STREAM_ERROR");
1813
2374
  const replay = [...replayFromOutput(output)];
1814
2375
  if (messageResponse) {
1815
2376
  const replayContent = rawReplayContent.length > 0 ? rawReplayContent : messageResponse.content;
@@ -1830,17 +2391,29 @@ var MessagesAdapter = class extends AdapterBase {
1830
2391
  if (!hasStreamedReasoning) {}
1831
2392
  const auxiliaryResult = await auxiliary.finalize(factory);
1832
2393
  for (const event of auxiliaryResult.events) yield event;
1833
- yield factory.responseCompleted(this.buildResponse(request, {
1834
- output,
1835
- replay,
1836
- stopReason: stopReason ? mapStopReason(stopReason) : void 0,
1837
- usage: auxiliaryResult.usage,
1838
- billing: auxiliaryResult.billing,
1839
- auxiliary: auxiliaryResult.auxiliary,
1840
- warnings: auxiliaryResult.warnings,
1841
- metadataSources: auxiliaryResult.metadataSources,
1842
- rawResponseId
1843
- }, factory));
2394
+ if (!completedEmitted) {
2395
+ completedEmitted = true;
2396
+ const finalResponse = this.buildResponse(request, {
2397
+ output,
2398
+ replay,
2399
+ stopReason: stopReason ? mapStopReason(stopReason) : void 0,
2400
+ usage: auxiliaryResult.usage,
2401
+ billing: auxiliaryResult.billing,
2402
+ auxiliary: auxiliaryResult.auxiliary,
2403
+ warnings: auxiliaryResult.warnings,
2404
+ metadataSources: auxiliaryResult.metadataSources,
2405
+ rawResponseId
2406
+ }, factory);
2407
+ yield factory.responseCompleted({
2408
+ replay: finalResponse.replay,
2409
+ stopReason: finalResponse.stopReason,
2410
+ trace: finalResponse.backend,
2411
+ usage: finalResponse.usage,
2412
+ billing: finalResponse.billing,
2413
+ auxiliary: finalResponse.auxiliary,
2414
+ warnings: finalResponse.warnings
2415
+ });
2416
+ }
1844
2417
  }
1845
2418
  };
1846
2419
  //#endregion
@@ -1855,51 +2428,21 @@ var MessagesAdapter = class extends AdapterBase {
1855
2428
  * - replay fidelity 依赖 provider 是否暴露可回放的 assistant turn 字段
1856
2429
  */
1857
2430
  const REASONING_FIELDS = ["reasoning_content", "reasoning"];
1858
- function assertChatToolResultOutcome(outcome) {
1859
- if (outcome !== "success") throw new AIRequestError(`chat-completions does not preserve tool_result outcome "${outcome}"; only "success" is supported`, "UNSUPPORTED_TOOL_RESULT_OUTCOME");
1860
- }
1861
- /**
1862
- * Chat Completions 的简化 SSE 解析器。
1863
- *
1864
- * 约束:
1865
- * - 每条 `data:` 行必须已经是一个完整 JSON 对象
1866
- * - 允许传输层把单行拆成多个 chunk,但不接受 provider 把一个 JSON event 改写成多条 `data:` 行
1867
- */
1868
- function parseChatSSE(buffer) {
1869
- const chunks = [];
1870
- let rest = buffer;
1871
- let malformedEvents = 0;
1872
- while (true) {
1873
- const lineEnd = rest.indexOf("\n");
1874
- if (lineEnd === -1) break;
1875
- const line = rest.slice(0, lineEnd).trim();
1876
- rest = rest.slice(lineEnd + 1);
1877
- if (!line.startsWith("data: ")) continue;
1878
- const data = line.slice(6).trim();
1879
- if (data === "[DONE]") continue;
1880
- try {
1881
- chunks.push(JSON.parse(data));
1882
- } catch {
1883
- malformedEvents++;
1884
- }
1885
- }
1886
- return {
1887
- chunks,
1888
- rest,
1889
- malformedEvents
1890
- };
1891
- }
1892
- function ensureTextCompatibleBlocks(blocks, field) {
1893
- for (let i = 0; i < blocks.length; i++) {
1894
- const block = blocks[i];
1895
- if (!block) continue;
1896
- if (block.type !== "text" && block.type !== "json") throw new AIRequestError(`chat-completions does not support ${field}[${i}] of type "${block.type}"; only text/json blocks are supported`, "UNSUPPORTED_CONTENT_BLOCK");
2431
+ const profile$1 = {
2432
+ kind: "chat-completions",
2433
+ instructionsMode: "system_message",
2434
+ supportedBlockTypes: ["text", "json"],
2435
+ reasoningBlockTypes: ["text"],
2436
+ capabilities: {
2437
+ textStreaming: "native",
2438
+ reasoningStreaming: "native",
2439
+ toolCallStreaming: "native",
2440
+ replay: "opaque",
2441
+ usage: "final",
2442
+ toolResultOutcomes: ["success"]
1897
2443
  }
1898
- return blocks;
1899
- }
1900
- function contentBlocksToChatText(blocks, field) {
1901
- return contentBlocksToText(ensureTextCompatibleBlocks(blocks, field));
1902
- }
2444
+ };
2445
+ const mapper$1 = new NormalizedRequestMapper(profile$1);
1903
2446
  function extractReasoningText(value) {
1904
2447
  if (typeof value === "string") return value;
1905
2448
  if (Array.isArray(value)) return value.map(extractReasoningText).join("");
@@ -1930,8 +2473,31 @@ function extractReasoningDeltas(delta) {
1930
2473
  }
1931
2474
  return deltas;
1932
2475
  }
1933
- function rollbackTrailingAssistantMessages$1(messages) {
1934
- while (messages.length > 0 && messages[messages.length - 1]?.role === "assistant") messages.pop();
2476
+ function isChatReplayToolCall(value) {
2477
+ if (!value || typeof value !== "object") return false;
2478
+ const entry = value;
2479
+ if (typeof entry.id !== "string" || entry.type !== "function") return false;
2480
+ const fn = entry.function;
2481
+ if (!fn || typeof fn !== "object") return false;
2482
+ const f = fn;
2483
+ return typeof f.name === "string" && typeof f.arguments === "string";
2484
+ }
2485
+ function isChatReplayMessage(value) {
2486
+ if (!value || typeof value !== "object") return false;
2487
+ const msg = value;
2488
+ const role = msg.role;
2489
+ if (role !== "system" && role !== "user" && role !== "assistant" && role !== "tool") return false;
2490
+ if (!(msg.content === null || typeof msg.content === "string")) return false;
2491
+ if (msg.tool_calls !== void 0) {
2492
+ if (!Array.isArray(msg.tool_calls) || !msg.tool_calls.every(isChatReplayToolCall)) return false;
2493
+ }
2494
+ if (msg.tool_call_id !== void 0 && typeof msg.tool_call_id !== "string") return false;
2495
+ if (msg.name !== void 0 && typeof msg.name !== "string") return false;
2496
+ return true;
2497
+ }
2498
+ function assertChatReplayMessages(messages, field) {
2499
+ if (!Array.isArray(messages)) throw new AIRequestError(`Invalid opaque replay payload: ${field} must be an array`, "INVALID_OPAQUE_REPLAY");
2500
+ for (let i = 0; i < messages.length; i++) if (!isChatReplayMessage(messages[i])) throw new AIRequestError(`Invalid opaque replay payload: ${field}[${i}] is not a valid chat message`, "INVALID_OPAQUE_REPLAY");
1935
2501
  }
1936
2502
  function buildAssistantReplayMessage(params) {
1937
2503
  const { content, reasoningByField, toolCalls } = params;
@@ -1953,7 +2519,7 @@ function buildAssistantReplayMessage(params) {
1953
2519
  }
1954
2520
  var ChatCompletionsAdapter = class extends AdapterBase {
1955
2521
  kind = "chat-completions";
1956
- nativeStreaming = true;
2522
+ capabilities = profile$1.capabilities;
1957
2523
  apiKey;
1958
2524
  baseUrl;
1959
2525
  fetchFn;
@@ -1965,17 +2531,14 @@ var ChatCompletionsAdapter = class extends AdapterBase {
1965
2531
  }
1966
2532
  buildRequest(request) {
1967
2533
  const messages = [];
1968
- if (request.instructions) {
1969
- const content = typeof request.instructions === "string" ? request.instructions : contentBlocksToChatText(request.instructions, "instructions");
1970
- messages.push({
1971
- role: "system",
1972
- content
1973
- });
1974
- }
2534
+ if (request.instructions) messages.push({
2535
+ role: "system",
2536
+ content: mapper$1.mapInstructions(request.instructions)
2537
+ });
1975
2538
  for (const item of request.input) switch (item.type) {
1976
2539
  case "message": {
1977
2540
  const role = item.role;
1978
- const text = contentBlocksToChatText(item.content, `input message (${item.role}) content`);
2541
+ const text = contentBlocksToText(mapper$1.ensureTextBlocks(item.content, `input message (${item.role}) content`));
1979
2542
  messages.push({
1980
2543
  role,
1981
2544
  content: text || null
@@ -2001,38 +2564,44 @@ var ChatCompletionsAdapter = class extends AdapterBase {
2001
2564
  break;
2002
2565
  }
2003
2566
  case "tool_result":
2004
- assertChatToolResultOutcome(item.outcome);
2567
+ mapper$1.assertToolResultOutcome(item.outcome);
2005
2568
  messages.push({
2006
2569
  role: "tool",
2007
2570
  tool_call_id: item.callId,
2008
2571
  name: item.toolName,
2009
- content: contentBlocksToChatText(item.content, `tool_result ${item.callId} content`)
2572
+ content: contentBlocksToText(mapper$1.ensureTextBlocks(item.content, `tool_result ${item.callId} content`))
2010
2573
  });
2011
2574
  break;
2012
2575
  case "reasoning":
2013
2576
  messages.push({
2014
2577
  role: "assistant",
2015
- content: contentBlocksToChatText(item.content, "reasoning content")
2578
+ content: contentBlocksToText(mapper$1.ensureTextBlocks(item.content, "reasoning content"))
2016
2579
  });
2017
2580
  break;
2018
- case "opaque":
2019
- if (item.purpose === "replay" && typeof item.payload === "object" && item.payload !== null) {
2020
- const payload = item.payload;
2021
- if (payload.role === "assistant" && typeof payload.content === "string") messages.push({
2022
- role: "assistant",
2023
- content: payload.content
2024
- });
2025
- else if (payload.replaceCanonical === true && Array.isArray(payload.messages)) {
2026
- rollbackTrailingAssistantMessages$1(messages);
2027
- for (const m of payload.messages) messages.push(m);
2028
- } else if (Array.isArray(payload.messages)) for (const m of payload.messages) messages.push(m);
2581
+ case "opaque": {
2582
+ if (item.purpose !== "replay") break;
2583
+ assertOpaqueReplayEnvelope(item.payload);
2584
+ const payload = item.payload;
2585
+ if (payload.role === "assistant" && typeof payload.content === "string") messages.push({
2586
+ role: "assistant",
2587
+ content: payload.content
2588
+ });
2589
+ else if (payload.replaceCanonical === true && "messages" in payload) {
2590
+ assertChatReplayMessages(payload.messages, "messages");
2591
+ mapper$1.rollbackTrailingAssistantMessages(messages);
2592
+ for (const m of payload.messages) messages.push(m);
2593
+ } else if ("messages" in payload) {
2594
+ assertChatReplayMessages(payload.messages, "messages");
2595
+ for (const m of payload.messages) messages.push(m);
2029
2596
  }
2030
2597
  break;
2598
+ }
2031
2599
  }
2032
2600
  const body = {
2033
2601
  model: request.model,
2034
2602
  messages,
2035
- stream: true
2603
+ stream: true,
2604
+ n: 1
2036
2605
  };
2037
2606
  if (request.tools && request.tools.length > 0) body.tools = request.tools.map((t) => ({
2038
2607
  type: "function",
@@ -2057,23 +2626,41 @@ var ChatCompletionsAdapter = class extends AdapterBase {
2057
2626
  }
2058
2627
  async *runStream(providerRequest, factory, request) {
2059
2628
  const auxiliary = this.createAuxiliaryState(request);
2060
- const response = await this.fetchFn(`${this.baseUrl}/chat/completions`, {
2061
- method: "POST",
2062
- headers: {
2063
- "Content-Type": "application/json",
2064
- Authorization: `Bearer ${this.apiKey}`
2065
- },
2066
- body: JSON.stringify(providerRequest)
2067
- });
2629
+ let response;
2630
+ try {
2631
+ response = await this.fetchFn(`${this.baseUrl}/chat/completions`, {
2632
+ method: "POST",
2633
+ headers: {
2634
+ "Content-Type": "application/json",
2635
+ Authorization: `Bearer ${this.apiKey}`
2636
+ },
2637
+ body: JSON.stringify(providerRequest)
2638
+ });
2639
+ } catch (err) {
2640
+ throw new AIProviderError(err instanceof Error ? err.message : String(err), "PROVIDER_ERROR");
2641
+ }
2068
2642
  if (!response.ok) {
2069
- const errorText = await response.text().catch(() => "unknown error");
2070
- throw new Error(`Chat Completions API error ${response.status}: ${errorText}`);
2643
+ const errorBody = await response.text().catch(() => "");
2644
+ throw providerHttpError(response.status, errorBody);
2071
2645
  }
2072
2646
  const reader = response.body?.getReader();
2073
- if (!reader) throw new Error("Response body is not readable");
2647
+ if (!reader) throw new AIStreamError("Response body is not readable", "STREAM_ERROR");
2648
+ const parser = new IncrementalStreamParser(splitLines, (item) => {
2649
+ const trimmed = item.trim();
2650
+ if (!trimmed.startsWith("data: ")) return { status: "ignored" };
2651
+ const data = trimmed.slice(6).trim();
2652
+ if (data === "[DONE]") return { status: "ignored" };
2653
+ try {
2654
+ return {
2655
+ status: "parsed",
2656
+ value: JSON.parse(data)
2657
+ };
2658
+ } catch {
2659
+ return { status: "malformed" };
2660
+ }
2661
+ });
2074
2662
  const output = [];
2075
- const decoder = new TextDecoder();
2076
- let buffer = "";
2663
+ let streamDone = false;
2077
2664
  let responseId;
2078
2665
  let accumulatedContent = "";
2079
2666
  let accumulatedReasoning = "";
@@ -2081,6 +2668,9 @@ var ChatCompletionsAdapter = class extends AdapterBase {
2081
2668
  let currentReasoningId = "";
2082
2669
  let hasMessageStarted = false;
2083
2670
  let hasReasoningStarted = false;
2671
+ let completedEmitted = false;
2672
+ let warnedNonZeroChoice = false;
2673
+ const buildResponse = this.buildResponse.bind(this);
2084
2674
  const pendingToolCalls = /* @__PURE__ */ new Map();
2085
2675
  const reasoningByField = /* @__PURE__ */ new Map();
2086
2676
  const finalizePendingTurn = () => {
@@ -2089,17 +2679,17 @@ var ChatCompletionsAdapter = class extends AdapterBase {
2089
2679
  const finalizedReasoningByField = new Map(reasoningByField);
2090
2680
  if (hasReasoningStarted && accumulatedReasoning) {
2091
2681
  const reasoning = reasoningItem([textBlock(accumulatedReasoning)], "full", currentReasoningId);
2092
- events.push(factory.reasoningCompleted(reasoning));
2682
+ events.push(factory.reasoningCompleted(currentReasoningId));
2093
2683
  output.push(reasoning);
2094
2684
  }
2095
- if (hasMessageStarted && accumulatedContent) {
2685
+ if (hasMessageStarted) {
2096
2686
  const message = messageItem([textBlock(accumulatedContent)], { id: currentMessageId });
2097
- events.push(factory.messageCompleted(message));
2687
+ events.push(factory.messageCompleted(currentMessageId));
2098
2688
  output.push(message);
2099
2689
  }
2100
2690
  for (const pending of finalizedToolCalls) {
2101
2691
  const toolCall = toolCallItem(pending.id, pending.name, pending.args);
2102
- events.push(factory.toolCallCompleted(toolCall));
2692
+ events.push(factory.toolCallCompleted(pending.id));
2103
2693
  output.push(toolCall);
2104
2694
  }
2105
2695
  const assistantReplayMessage = buildAssistantReplayMessage({
@@ -2120,13 +2710,46 @@ var ChatCompletionsAdapter = class extends AdapterBase {
2120
2710
  assistantReplayMessage
2121
2711
  };
2122
2712
  };
2713
+ const emitCompleted = async function* (stopReason, assistantReplayMessage, rawResponseId) {
2714
+ if (completedEmitted) {
2715
+ yield factory.responseWarning("Duplicate finish signal ignored", "DUPLICATE_FINISH");
2716
+ return;
2717
+ }
2718
+ completedEmitted = true;
2719
+ const replay = [...replayFromOutput(output)];
2720
+ if (assistantReplayMessage) replay.push(opaqueItem("chat.completions", "replay", {
2721
+ replaceCanonical: true,
2722
+ messages: [assistantReplayMessage]
2723
+ }));
2724
+ const auxiliaryResult = await auxiliary.finalize(factory);
2725
+ for (const event of auxiliaryResult.events) yield event;
2726
+ const finalResponse = buildResponse(request, {
2727
+ output,
2728
+ replay,
2729
+ stopReason,
2730
+ usage: auxiliaryResult.usage,
2731
+ billing: auxiliaryResult.billing,
2732
+ auxiliary: auxiliaryResult.auxiliary,
2733
+ warnings: auxiliaryResult.warnings,
2734
+ metadataSources: auxiliaryResult.metadataSources,
2735
+ rawResponseId
2736
+ }, factory);
2737
+ yield factory.responseCompleted({
2738
+ replay: finalResponse.replay,
2739
+ stopReason: finalResponse.stopReason,
2740
+ trace: finalResponse.backend,
2741
+ usage: finalResponse.usage,
2742
+ billing: finalResponse.billing,
2743
+ auxiliary: finalResponse.auxiliary,
2744
+ warnings: finalResponse.warnings
2745
+ });
2746
+ };
2123
2747
  try {
2124
2748
  while (true) {
2125
- const { done, value } = await reader.read();
2126
- if (done) break;
2127
- buffer += decoder.decode(value, { stream: true });
2128
- const { chunks, rest, malformedEvents } = parseChatSSE(buffer);
2129
- buffer = rest;
2749
+ const { done, value } = await reader.read().catch((err) => {
2750
+ throw new AIStreamError(`Failed to read response stream: ${err instanceof Error ? err.message : String(err)}`, "STREAM_ERROR");
2751
+ });
2752
+ const { items: chunks, malformed: malformedEvents } = done ? parser.flush() : parser.feed(value);
2130
2753
  const malformedWarning = emitMalformedStreamWarning(factory, {
2131
2754
  count: malformedEvents,
2132
2755
  providerLabel: "Chat Completions",
@@ -2137,16 +2760,26 @@ var ChatCompletionsAdapter = class extends AdapterBase {
2137
2760
  responseId = chunk.id;
2138
2761
  if (chunk.usage) auxiliary.recordUsage(usageFromChatCompletions(chunk.usage), "final", chunk.usage);
2139
2762
  for (const choice of chunk.choices) {
2140
- if (choice.index !== 0) continue;
2763
+ if (choice.index !== 0) {
2764
+ if (!warnedNonZeroChoice) {
2765
+ yield factory.responseWarning(`Chat Completions returned choice index ${choice.index}; only the first choice (index 0) is supported. This choice is ignored.`, "MULTIPLE_CHOICES_IGNORED");
2766
+ warnedNonZeroChoice = true;
2767
+ }
2768
+ continue;
2769
+ }
2770
+ if (completedEmitted) {
2771
+ if (choice.finish_reason) yield factory.responseWarning("Duplicate finish signal ignored", "DUPLICATE_FINISH");
2772
+ continue;
2773
+ }
2141
2774
  const delta = choice.delta;
2142
2775
  const finishReason = choice.finish_reason;
2143
2776
  const reasoningDeltas = extractReasoningDeltas(delta);
2144
- if (delta.role === "assistant" && typeof delta.content === "string" && !hasMessageStarted) {
2777
+ const ensureMessageStarted = () => {
2778
+ if (hasMessageStarted) return;
2145
2779
  currentMessageId = `msg-${chunk.id}`;
2146
2780
  hasMessageStarted = true;
2147
2781
  accumulatedContent = "";
2148
- yield factory.messageStarted(currentMessageId);
2149
- }
2782
+ };
2150
2783
  if (reasoningDeltas.length > 0) {
2151
2784
  if (!hasReasoningStarted) {
2152
2785
  currentReasoningId = `reason-${chunk.id}`;
@@ -2162,32 +2795,41 @@ var ChatCompletionsAdapter = class extends AdapterBase {
2162
2795
  }
2163
2796
  if (delta.content) {
2164
2797
  if (!hasMessageStarted) {
2165
- currentMessageId = `msg-${chunk.id}`;
2166
- hasMessageStarted = true;
2798
+ ensureMessageStarted();
2167
2799
  yield factory.messageStarted(currentMessageId);
2168
2800
  }
2169
2801
  accumulatedContent += delta.content;
2170
- yield factory.messageDelta(currentMessageId, delta.content);
2802
+ yield factory.messageDelta(currentMessageId, textBlock(delta.content));
2171
2803
  }
2172
- if (delta.tool_calls) for (const tc of delta.tool_calls) {
2173
- const idx = tc.index;
2174
- if (tc.id) {
2175
- pendingToolCalls.set(idx, {
2176
- id: tc.id,
2177
- name: tc.function?.name ?? "",
2178
- args: ""
2179
- });
2180
- yield factory.toolCallStarted(tc.id, tc.function?.name ?? "");
2804
+ if (delta.tool_calls) {
2805
+ if (!hasMessageStarted) {
2806
+ ensureMessageStarted();
2807
+ yield factory.messageStarted(currentMessageId);
2181
2808
  }
2182
- if (tc.function?.arguments) {
2183
- const pending = pendingToolCalls.get(idx);
2184
- if (pending) {
2185
- pending.args += tc.function.arguments;
2186
- yield factory.toolCallDelta(pending.id, { argumentsText: tc.function.arguments });
2809
+ for (const tc of delta.tool_calls) {
2810
+ const idx = tc.index;
2811
+ if (tc.id) {
2812
+ pendingToolCalls.set(idx, {
2813
+ id: tc.id,
2814
+ name: tc.function?.name ?? "",
2815
+ args: ""
2816
+ });
2817
+ yield factory.toolCallStarted(tc.id, tc.function?.name ?? "");
2818
+ }
2819
+ if (tc.function?.arguments) {
2820
+ const pending = pendingToolCalls.get(idx);
2821
+ if (pending) {
2822
+ pending.args += tc.function.arguments;
2823
+ yield factory.toolCallDelta(pending.id, { argumentsText: tc.function.arguments });
2824
+ }
2187
2825
  }
2188
2826
  }
2189
2827
  }
2190
2828
  if (delta.function_call) {
2829
+ if (!hasMessageStarted) {
2830
+ ensureMessageStarted();
2831
+ yield factory.messageStarted(currentMessageId);
2832
+ }
2191
2833
  if (delta.function_call.name) {
2192
2834
  const fcId = `fc-${chunk.id}-0`;
2193
2835
  pendingToolCalls.set(0, {
@@ -2208,54 +2850,28 @@ var ChatCompletionsAdapter = class extends AdapterBase {
2208
2850
  if (finishReason && finishReason !== null) {
2209
2851
  const { events, assistantReplayMessage } = finalizePendingTurn();
2210
2852
  for (const event of events) yield event;
2211
- const stopReason = mapStopReason(finishReason);
2212
- const replay = [...replayFromOutput(output)];
2213
- if (assistantReplayMessage) replay.push(opaqueItem("chat.completions", "replay", {
2214
- replaceCanonical: true,
2215
- messages: [assistantReplayMessage]
2216
- }));
2217
- const auxiliaryResult = await auxiliary.finalize(factory);
2218
- for (const event of auxiliaryResult.events) yield event;
2219
- yield factory.responseCompleted(this.buildResponse(request, {
2220
- output,
2221
- replay,
2222
- stopReason,
2223
- usage: auxiliaryResult.usage,
2224
- billing: auxiliaryResult.billing,
2225
- auxiliary: auxiliaryResult.auxiliary,
2226
- warnings: auxiliaryResult.warnings,
2227
- metadataSources: auxiliaryResult.metadataSources,
2228
- rawResponseId: chunk.id
2229
- }, factory));
2853
+ yield* emitCompleted(mapStopReason(finishReason), assistantReplayMessage, chunk.id);
2230
2854
  }
2231
2855
  }
2232
2856
  }
2857
+ if (done) {
2858
+ streamDone = true;
2859
+ break;
2860
+ }
2233
2861
  }
2234
2862
  } finally {
2235
- reader.releaseLock();
2863
+ try {
2864
+ if (!streamDone) await reader.cancel().catch(() => void 0);
2865
+ } finally {
2866
+ reader.releaseLock();
2867
+ }
2236
2868
  }
2237
- if (buffer.trim().length > 0) yield factory.responseWarning("Stream ended with an incomplete Chat Completions SSE frame", "STREAM_ERROR");
2238
- if (hasMessageStarted || hasReasoningStarted || pendingToolCalls.size > 0) {
2869
+ if (parser.getRemaining().trim().length > 0) yield factory.responseWarning("Stream ended with an incomplete Chat Completions SSE frame", "STREAM_ERROR");
2870
+ if (!completedEmitted && (hasMessageStarted || hasReasoningStarted || pendingToolCalls.size > 0)) {
2239
2871
  yield factory.responseWarning("Stream ended without a finish_reason", "INCOMPLETE_STREAM");
2240
2872
  const { events, assistantReplayMessage } = finalizePendingTurn();
2241
2873
  for (const event of events) yield event;
2242
- const replay = [...replayFromOutput(output)];
2243
- if (assistantReplayMessage) replay.push(opaqueItem("chat.completions", "replay", {
2244
- replaceCanonical: true,
2245
- messages: [assistantReplayMessage]
2246
- }));
2247
- const auxiliaryResult = await auxiliary.finalize(factory);
2248
- for (const event of auxiliaryResult.events) yield event;
2249
- yield factory.responseCompleted(this.buildResponse(request, {
2250
- output,
2251
- replay,
2252
- usage: auxiliaryResult.usage,
2253
- billing: auxiliaryResult.billing,
2254
- auxiliary: auxiliaryResult.auxiliary,
2255
- warnings: auxiliaryResult.warnings,
2256
- metadataSources: auxiliaryResult.metadataSources,
2257
- rawResponseId: responseId
2258
- }, factory));
2874
+ yield* emitCompleted(void 0, assistantReplayMessage, responseId);
2259
2875
  }
2260
2876
  }
2261
2877
  };
@@ -2277,23 +2893,21 @@ var ChatCompletionsAdapter = class extends AdapterBase {
2277
2893
  * - tool_call 不支持逐 token 流式
2278
2894
  * - replay 保真度低(无 opaque continuation 机制)
2279
2895
  */
2280
- function ensureOllamaTextBlocks(blocks, field) {
2281
- for (let i = 0; i < blocks.length; i++) {
2282
- const block = blocks[i];
2283
- if (!block) continue;
2284
- if (block.type !== "text" && block.type !== "json") throw new AIRequestError(`ollama does not support ${field}[${i}] of type "${block.type}"; only text/json blocks are supported`, "UNSUPPORTED_CONTENT_BLOCK");
2285
- }
2286
- return blocks;
2287
- }
2288
- function ensureOllamaReasoningBlocks(blocks, field) {
2289
- return blocks.map((block, index) => {
2290
- if (block.type !== "text") throw new AIRequestError(`ollama does not support ${field}[${index}] of type "${block.type}"; reasoning only supports text blocks`, "UNSUPPORTED_CONTENT_BLOCK");
2291
- return block;
2292
- });
2293
- }
2294
- function instructionsToOllamaText(instructions) {
2295
- return typeof instructions === "string" ? instructions : contentBlocksToText(ensureOllamaTextBlocks(instructions, "instructions"));
2296
- }
2896
+ const profile = {
2897
+ kind: "ollama",
2898
+ instructionsMode: "system_message",
2899
+ supportedBlockTypes: ["text", "json"],
2900
+ reasoningBlockTypes: ["text"],
2901
+ capabilities: {
2902
+ textStreaming: "native",
2903
+ reasoningStreaming: "none",
2904
+ toolCallStreaming: "synthetic",
2905
+ replay: "opaque",
2906
+ usage: "final",
2907
+ toolResultOutcomes: ["success"]
2908
+ }
2909
+ };
2910
+ const mapper = new NormalizedRequestMapper(profile);
2297
2911
  function parseOllamaToolArguments(item) {
2298
2912
  if (item.argumentsJson && typeof item.argumentsJson === "object" && item.argumentsJson !== null) return item.argumentsJson;
2299
2913
  try {
@@ -2302,46 +2916,24 @@ function parseOllamaToolArguments(item) {
2302
2916
  } catch {}
2303
2917
  throw new AIRequestError("ollama tool_call argumentsText must be valid JSON object when argumentsJson is absent", "TOOL_CALL_ARGUMENTS_INVALID");
2304
2918
  }
2305
- function assertOllamaToolResultOutcome(outcome) {
2306
- if (outcome !== "success") throw new AIRequestError(`ollama does not preserve tool_result outcome "${outcome}"; only "success" is supported`, "UNSUPPORTED_TOOL_RESULT_OUTCOME");
2307
- }
2308
- function parseOllamaNDJSON(buffer) {
2309
- const chunks = [];
2310
- let rest = buffer;
2311
- let malformedLines = 0;
2312
- while (true) {
2313
- const lineEnd = rest.indexOf("\n");
2314
- if (lineEnd === -1) break;
2315
- const line = rest.slice(0, lineEnd).trim();
2316
- rest = rest.slice(lineEnd + 1);
2317
- if (!line) continue;
2318
- try {
2319
- const parsed = JSON.parse(line);
2320
- if (parsed && typeof parsed === "object" && "message" in parsed) chunks.push(parsed);
2321
- else malformedLines++;
2322
- } catch {
2323
- malformedLines++;
2324
- }
2325
- }
2326
- return {
2327
- chunks,
2328
- rest,
2329
- malformedLines
2330
- };
2331
- }
2332
- function rollbackTrailingAssistantMessages(messages) {
2333
- while (messages.length > 0 && messages[messages.length - 1]?.role === "assistant") messages.pop();
2334
- }
2335
- function isOllamaToolCalls(value) {
2919
+ function isOllamaReplayToolCalls(value) {
2336
2920
  return Array.isArray(value) && value.every((entry) => {
2337
2921
  if (!entry || typeof entry !== "object" || !("function" in entry)) return false;
2338
2922
  const fn = entry.function;
2923
+ const id = entry.id;
2924
+ if (id !== void 0 && typeof id !== "string") return false;
2339
2925
  return !!fn && typeof fn === "object" && "name" in fn && typeof fn.name === "string" && "arguments" in fn && typeof fn.arguments === "object" && fn.arguments !== null;
2340
2926
  });
2341
2927
  }
2928
+ function toWireOllamaToolCalls(toolCalls) {
2929
+ return toolCalls.map((tc) => ({ function: {
2930
+ name: tc.function.name,
2931
+ arguments: tc.function.arguments
2932
+ } }));
2933
+ }
2342
2934
  var OllamaAdapter = class extends AdapterBase {
2343
2935
  kind = "ollama";
2344
- nativeStreaming = true;
2936
+ capabilities = profile.capabilities;
2345
2937
  baseUrl;
2346
2938
  apiKey;
2347
2939
  fetchFn;
@@ -2354,16 +2946,18 @@ var OllamaAdapter = class extends AdapterBase {
2354
2946
  buildRequest(request) {
2355
2947
  if (request.toolChoice && request.toolChoice !== "auto") throw new AIRequestError("ollama does not support explicit toolChoice", "UNSUPPORTED_TOOL_CHOICE");
2356
2948
  const messages = [];
2949
+ /** Local-only name → call id queue for best-effort tool_result association (not sent to Ollama). */
2950
+ const callIdsByName = /* @__PURE__ */ new Map();
2357
2951
  if (request.instructions) messages.push({
2358
2952
  role: "system",
2359
- content: instructionsToOllamaText(request.instructions)
2953
+ content: mapper.mapInstructions(request.instructions)
2360
2954
  });
2361
2955
  for (const item of request.input) switch (item.type) {
2362
2956
  case "message": {
2363
2957
  const role = item.role;
2364
2958
  messages.push({
2365
2959
  role,
2366
- content: contentBlocksToText(ensureOllamaTextBlocks(item.content, `input message (${item.role}) content`))
2960
+ content: contentBlocksToText(mapper.ensureTextBlocks(item.content, `input message (${item.role}) content`))
2367
2961
  });
2368
2962
  break;
2369
2963
  }
@@ -2373,6 +2967,9 @@ var OllamaAdapter = class extends AdapterBase {
2373
2967
  name: item.name,
2374
2968
  arguments: parseOllamaToolArguments(item)
2375
2969
  } };
2970
+ const queue = callIdsByName.get(item.name) ?? [];
2971
+ queue.push(item.id);
2972
+ callIdsByName.set(item.name, queue);
2376
2973
  if (lastAssistant) lastAssistant.tool_calls = [...lastAssistant.tool_calls ?? [], tc];
2377
2974
  else messages.push({
2378
2975
  role: "assistant",
@@ -2381,32 +2978,48 @@ var OllamaAdapter = class extends AdapterBase {
2381
2978
  });
2382
2979
  break;
2383
2980
  }
2384
- case "tool_result":
2385
- assertOllamaToolResultOutcome(item.outcome);
2981
+ case "tool_result": {
2982
+ mapper.assertToolResultOutcome(item.outcome);
2983
+ const queue = callIdsByName.get(item.toolName);
2984
+ if (queue && queue.length > 0) queue.shift();
2386
2985
  messages.push({
2387
2986
  role: "tool",
2388
- content: contentBlocksToText(ensureOllamaTextBlocks(item.content, `tool_result ${item.callId} content`))
2987
+ content: contentBlocksToText(mapper.ensureTextBlocks(item.content, `tool_result ${item.callId} content`))
2389
2988
  });
2390
2989
  break;
2990
+ }
2391
2991
  case "reasoning":
2392
2992
  messages.push({
2393
2993
  role: "assistant",
2394
- content: contentBlocksToText(ensureOllamaReasoningBlocks(item.content, "reasoning content"))
2994
+ content: contentBlocksToText(mapper.ensureReasoningBlocks(item.content, "reasoning content"))
2395
2995
  });
2396
2996
  break;
2397
- case "opaque":
2398
- if (item.source === "ollama" && item.purpose === "replay" && typeof item.payload === "object" && item.payload !== null) {
2399
- const payload = item.payload;
2400
- if (payload.role === "assistant" && typeof payload.content === "string") {
2401
- rollbackTrailingAssistantMessages(messages);
2402
- messages.push({
2403
- role: "assistant",
2404
- content: payload.content,
2405
- tool_calls: isOllamaToolCalls(payload.tool_calls) ? payload.tool_calls : void 0
2406
- });
2997
+ case "opaque": {
2998
+ if (item.source !== "ollama" || item.purpose !== "replay") break;
2999
+ assertOpaqueReplayEnvelope(item.payload);
3000
+ const payload = item.payload;
3001
+ if (payload.role === "assistant" && typeof payload.content === "string") {
3002
+ mapper.rollbackTrailingAssistantMessages(messages);
3003
+ let replayToolCalls;
3004
+ if ("tool_calls" in payload && payload.tool_calls !== void 0) {
3005
+ if (!isOllamaReplayToolCalls(payload.tool_calls)) throw new AIRequestError("Invalid opaque replay payload: tool_calls is not a valid ollama tool_calls array", "INVALID_OPAQUE_REPLAY");
3006
+ replayToolCalls = payload.tool_calls;
2407
3007
  }
3008
+ if (replayToolCalls) {
3009
+ for (const tc of replayToolCalls) if (tc.id) {
3010
+ const queue = callIdsByName.get(tc.function.name) ?? [];
3011
+ queue.push(tc.id);
3012
+ callIdsByName.set(tc.function.name, queue);
3013
+ }
3014
+ }
3015
+ messages.push({
3016
+ role: "assistant",
3017
+ content: payload.content,
3018
+ tool_calls: replayToolCalls ? toWireOllamaToolCalls(replayToolCalls) : void 0
3019
+ });
2408
3020
  }
2409
3021
  break;
3022
+ }
2410
3023
  }
2411
3024
  const body = {
2412
3025
  model: request.model,
@@ -2430,35 +3043,96 @@ var OllamaAdapter = class extends AdapterBase {
2430
3043
  }
2431
3044
  async *runStream(providerRequest, factory, request) {
2432
3045
  const auxiliary = this.createAuxiliaryState(request);
3046
+ let completedEmitted = false;
2433
3047
  if (request.metadata) yield factory.responseWarning("Request metadata is not supported by the Ollama adapter", "UNSUPPORTED_METADATA");
2434
3048
  const headers = { "Content-Type": "application/json" };
2435
3049
  if (this.apiKey) headers.Authorization = `Bearer ${this.apiKey}`;
2436
- const response = await this.fetchFn(`${this.baseUrl}/api/chat`, {
2437
- method: "POST",
2438
- headers,
2439
- body: JSON.stringify(providerRequest)
2440
- });
3050
+ let response;
3051
+ try {
3052
+ response = await this.fetchFn(`${this.baseUrl}/api/chat`, {
3053
+ method: "POST",
3054
+ headers,
3055
+ body: JSON.stringify(providerRequest)
3056
+ });
3057
+ } catch (err) {
3058
+ throw new AIProviderError(err instanceof Error ? err.message : String(err), "PROVIDER_ERROR");
3059
+ }
2441
3060
  if (!response.ok) {
2442
- const errorText = await response.text().catch(() => "unknown error");
2443
- throw new Error(`Ollama API error ${response.status}: ${errorText}`);
3061
+ const errorBody = await response.text().catch(() => "");
3062
+ throw providerHttpError(response.status, errorBody);
2444
3063
  }
2445
3064
  const reader = response.body?.getReader();
2446
- if (!reader) throw new Error("Response body is not readable");
3065
+ if (!reader) throw new AIStreamError("Response body is not readable", "STREAM_ERROR");
3066
+ const parser = new IncrementalStreamParser(splitLines, (item) => {
3067
+ const trimmed = item.trim();
3068
+ if (!trimmed) return { status: "ignored" };
3069
+ try {
3070
+ const parsed = JSON.parse(trimmed);
3071
+ if (parsed && typeof parsed === "object" && "message" in parsed) return {
3072
+ status: "parsed",
3073
+ value: parsed
3074
+ };
3075
+ return { status: "malformed" };
3076
+ } catch {
3077
+ return { status: "malformed" };
3078
+ }
3079
+ });
2447
3080
  const output = [];
2448
- const decoder = new TextDecoder();
2449
- let buffer = "";
3081
+ let streamDone = false;
2450
3082
  let responseId;
2451
3083
  let accumulatedContent = "";
2452
3084
  let currentMessageId = "";
2453
3085
  let hasMessageStarted = false;
2454
3086
  let pendingToolCalls = [];
3087
+ let toolCallIndex = 0;
3088
+ const buildResponse = this.buildResponse.bind(this);
3089
+ const emitCompleted = async function* (stopReason, rawResponseId) {
3090
+ if (completedEmitted) {
3091
+ yield factory.responseWarning("Duplicate finish signal ignored", "DUPLICATE_FINISH");
3092
+ return;
3093
+ }
3094
+ completedEmitted = true;
3095
+ const replay = replayFromOutput(output);
3096
+ if (accumulatedContent || pendingToolCalls.length > 0) replay.push(opaqueItem("ollama", "replay", {
3097
+ role: "assistant",
3098
+ content: accumulatedContent,
3099
+ tool_calls: pendingToolCalls.map((tc) => ({
3100
+ id: tc.id,
3101
+ function: {
3102
+ name: tc.name,
3103
+ arguments: tc.argumentsJson
3104
+ }
3105
+ }))
3106
+ }));
3107
+ const auxiliaryResult = await auxiliary.finalize(factory);
3108
+ for (const event of auxiliaryResult.events) yield event;
3109
+ const finalResponse = buildResponse(request, {
3110
+ output,
3111
+ replay,
3112
+ stopReason,
3113
+ usage: auxiliaryResult.usage,
3114
+ billing: auxiliaryResult.billing,
3115
+ auxiliary: auxiliaryResult.auxiliary,
3116
+ warnings: auxiliaryResult.warnings,
3117
+ metadataSources: auxiliaryResult.metadataSources,
3118
+ rawResponseId
3119
+ }, factory);
3120
+ yield factory.responseCompleted({
3121
+ replay: finalResponse.replay,
3122
+ stopReason: finalResponse.stopReason,
3123
+ trace: finalResponse.backend,
3124
+ usage: finalResponse.usage,
3125
+ billing: finalResponse.billing,
3126
+ auxiliary: finalResponse.auxiliary,
3127
+ warnings: finalResponse.warnings
3128
+ });
3129
+ };
2455
3130
  try {
2456
3131
  while (true) {
2457
- const { done, value } = await reader.read();
2458
- if (done) break;
2459
- buffer += decoder.decode(value, { stream: true });
2460
- const { chunks, rest, malformedLines } = parseOllamaNDJSON(buffer);
2461
- buffer = rest;
3132
+ const { done, value } = await reader.read().catch((err) => {
3133
+ throw new AIStreamError(`Failed to read response stream: ${err instanceof Error ? err.message : String(err)}`, "STREAM_ERROR");
3134
+ });
3135
+ const { items: chunks, malformed: malformedLines } = done ? parser.flush() : parser.feed(value);
2462
3136
  const malformedWarning = emitMalformedStreamWarning(factory, {
2463
3137
  count: malformedLines,
2464
3138
  providerLabel: "Ollama",
@@ -2467,6 +3141,10 @@ var OllamaAdapter = class extends AdapterBase {
2467
3141
  if (malformedWarning) yield malformedWarning;
2468
3142
  for (const chunk of chunks) {
2469
3143
  responseId = chunk.created_at;
3144
+ if (completedEmitted) {
3145
+ if (chunk.done) yield factory.responseWarning("Duplicate finish signal ignored", "DUPLICATE_FINISH");
3146
+ continue;
3147
+ }
2470
3148
  const msg = chunk.message;
2471
3149
  if (msg.content) {
2472
3150
  if (!hasMessageStarted) {
@@ -2475,10 +3153,10 @@ var OllamaAdapter = class extends AdapterBase {
2475
3153
  yield factory.messageStarted(currentMessageId);
2476
3154
  }
2477
3155
  accumulatedContent += msg.content;
2478
- yield factory.messageDelta(currentMessageId, msg.content);
3156
+ yield factory.messageDelta(currentMessageId, textBlock(msg.content));
2479
3157
  }
2480
3158
  if (msg.tool_calls && msg.tool_calls.length > 0) for (const tc of msg.tool_calls) {
2481
- const tcId = `tc-${chunk.created_at}-${tc.function.name}`;
3159
+ const tcId = `ollama-tc-${request.requestId}-${toolCallIndex++}`;
2482
3160
  const argsText = JSON.stringify(tc.function.arguments);
2483
3161
  pendingToolCalls.push({
2484
3162
  id: tcId,
@@ -2495,14 +3173,15 @@ var OllamaAdapter = class extends AdapterBase {
2495
3173
  }
2496
3174
  if (hasMessageStarted) {
2497
3175
  const message = messageItem([textBlock(accumulatedContent)], { id: currentMessageId });
2498
- yield factory.messageCompleted(message);
3176
+ yield factory.messageCompleted(currentMessageId);
2499
3177
  if (accumulatedContent) output.push(message);
2500
3178
  }
3179
+ if (pendingToolCalls.length > 0) yield factory.responseWarning(`Ollama delivered ${pendingToolCalls.length} tool call(s) as a batch; tool_call streaming is not supported`, WarningCode.TOOL_CALL_BATCHED);
2501
3180
  for (const pending of pendingToolCalls) {
2502
3181
  const toolCall = toolCallItem(pending.id, pending.name, pending.argumentsText, pending.argumentsJson);
2503
3182
  yield factory.toolCallStarted(pending.id, pending.name);
2504
3183
  yield factory.toolCallDelta(pending.id, { argumentsText: pending.argumentsText });
2505
- yield factory.toolCallCompleted(toolCall);
3184
+ yield factory.toolCallCompleted(pending.id);
2506
3185
  output.push(toolCall);
2507
3186
  }
2508
3187
  if (request.include?.usage !== "off" && (chunk.prompt_eval_count !== void 0 || chunk.eval_count !== void 0)) auxiliary.recordUsage(usageFromOllama({
@@ -2512,67 +3191,42 @@ var OllamaAdapter = class extends AdapterBase {
2512
3191
  prompt_eval_count: chunk.prompt_eval_count,
2513
3192
  eval_count: chunk.eval_count
2514
3193
  });
2515
- const stopReason = chunk.done_reason ? mapStopReason(chunk.done_reason) : void 0;
2516
- const replay = replayFromOutput(output);
2517
- if (accumulatedContent || pendingToolCalls.length > 0) replay.push(opaqueItem("ollama", "replay", {
2518
- role: "assistant",
2519
- content: accumulatedContent,
2520
- tool_calls: pendingToolCalls.map((tc) => ({ function: {
2521
- name: tc.name,
2522
- arguments: tc.argumentsJson
2523
- } }))
2524
- }));
2525
- const auxiliaryResult = await auxiliary.finalize(factory);
2526
- for (const event of auxiliaryResult.events) yield event;
2527
- yield factory.responseCompleted(this.buildResponse(request, {
2528
- output,
2529
- replay,
2530
- stopReason,
2531
- usage: auxiliaryResult.usage,
2532
- billing: auxiliaryResult.billing,
2533
- auxiliary: auxiliaryResult.auxiliary,
2534
- warnings: auxiliaryResult.warnings,
2535
- metadataSources: auxiliaryResult.metadataSources,
2536
- rawResponseId: chunk.created_at
2537
- }, factory));
3194
+ yield* emitCompleted(chunk.done_reason ? mapStopReason(chunk.done_reason) : void 0, chunk.created_at);
2538
3195
  accumulatedContent = "";
2539
3196
  currentMessageId = "";
2540
3197
  hasMessageStarted = false;
2541
3198
  pendingToolCalls = [];
2542
3199
  }
2543
3200
  }
3201
+ if (done) {
3202
+ streamDone = true;
3203
+ break;
3204
+ }
2544
3205
  }
2545
3206
  } finally {
2546
- reader.releaseLock();
3207
+ try {
3208
+ if (!streamDone) await reader.cancel().catch(() => void 0);
3209
+ } finally {
3210
+ reader.releaseLock();
3211
+ }
2547
3212
  }
2548
- if (buffer.trim().length > 0) yield factory.responseWarning("Stream ended with an incomplete Ollama NDJSON line", "STREAM_ERROR");
2549
- if (hasMessageStarted || pendingToolCalls.length > 0) {
3213
+ if (parser.getRemaining().trim().length > 0) yield factory.responseWarning("Stream ended with an incomplete Ollama NDJSON line", "STREAM_ERROR");
3214
+ if (!completedEmitted && (hasMessageStarted || pendingToolCalls.length > 0)) {
2550
3215
  yield factory.responseWarning("Stream ended without a done signal", "INCOMPLETE_STREAM");
2551
3216
  if (hasMessageStarted) {
2552
3217
  const message = messageItem([textBlock(accumulatedContent)], { id: currentMessageId });
2553
- yield factory.messageCompleted(message);
3218
+ yield factory.messageCompleted(currentMessageId);
2554
3219
  if (accumulatedContent) output.push(message);
2555
3220
  }
3221
+ if (pendingToolCalls.length > 0) yield factory.responseWarning(`Ollama delivered ${pendingToolCalls.length} tool call(s) as a batch; tool_call streaming is not supported`, WarningCode.TOOL_CALL_BATCHED);
2556
3222
  for (const pending of pendingToolCalls) {
2557
3223
  const toolCall = toolCallItem(pending.id, pending.name, pending.argumentsText, pending.argumentsJson);
2558
3224
  yield factory.toolCallStarted(pending.id, pending.name);
2559
3225
  yield factory.toolCallDelta(pending.id, { argumentsText: pending.argumentsText });
2560
- yield factory.toolCallCompleted(toolCall);
3226
+ yield factory.toolCallCompleted(pending.id);
2561
3227
  output.push(toolCall);
2562
3228
  }
2563
- const replay = replayFromOutput(output);
2564
- const auxiliaryResult = await auxiliary.finalize(factory);
2565
- for (const event of auxiliaryResult.events) yield event;
2566
- yield factory.responseCompleted(this.buildResponse(request, {
2567
- output,
2568
- replay,
2569
- usage: auxiliaryResult.usage,
2570
- billing: auxiliaryResult.billing,
2571
- auxiliary: auxiliaryResult.auxiliary,
2572
- warnings: auxiliaryResult.warnings,
2573
- metadataSources: auxiliaryResult.metadataSources,
2574
- rawResponseId: responseId
2575
- }, factory));
3229
+ yield* emitCompleted(void 0, responseId);
2576
3230
  }
2577
3231
  }
2578
3232
  };
@@ -2607,7 +3261,18 @@ function assertMockRequest(request, expectation, context) {
2607
3261
  }
2608
3262
  var MockAdapter = class extends AdapterBase {
2609
3263
  kind = "mock";
2610
- nativeStreaming = false;
3264
+ capabilities = {
3265
+ textStreaming: "synthetic",
3266
+ reasoningStreaming: "synthetic",
3267
+ toolCallStreaming: "synthetic",
3268
+ replay: "canonical",
3269
+ usage: "final",
3270
+ toolResultOutcomes: [
3271
+ "success",
3272
+ "error",
3273
+ "rejected"
3274
+ ]
3275
+ };
2611
3276
  handler;
2612
3277
  providerMetadata;
2613
3278
  cursor = 0;
@@ -2679,18 +3344,34 @@ var MockAdapter = class extends AdapterBase {
2679
3344
  break;
2680
3345
  }
2681
3346
  case "complete": {
2682
- const response = this.finalizeTurn(request, factory, mockRequest, output, step, stepCount);
2683
- yield factory.responseCompleted(response);
3347
+ const finalResponse = this.finalizeTurn(request, factory, mockRequest, output, step, stepCount);
3348
+ yield factory.responseCompleted({
3349
+ replay: finalResponse.replay,
3350
+ stopReason: finalResponse.stopReason,
3351
+ trace: finalResponse.backend,
3352
+ usage: finalResponse.usage,
3353
+ billing: finalResponse.billing,
3354
+ auxiliary: finalResponse.auxiliary,
3355
+ warnings: finalResponse.warnings
3356
+ });
2684
3357
  return;
2685
3358
  }
2686
3359
  case "error": {
2687
3360
  yield factory.responseWarning(step.message, step.code);
2688
- const response = this.finalizeTurn(request, factory, mockRequest, output, {
3361
+ const finalResponse = this.finalizeTurn(request, factory, mockRequest, output, {
2689
3362
  type: "complete",
2690
3363
  stopReason: step.stopReason ?? "error",
2691
3364
  providerMetadata: step.providerMetadata
2692
3365
  }, stepCount);
2693
- yield factory.responseCompleted(response);
3366
+ yield factory.responseCompleted({
3367
+ replay: finalResponse.replay,
3368
+ stopReason: finalResponse.stopReason,
3369
+ trace: finalResponse.backend,
3370
+ usage: finalResponse.usage,
3371
+ billing: finalResponse.billing,
3372
+ auxiliary: finalResponse.auxiliary,
3373
+ warnings: finalResponse.warnings
3374
+ });
2694
3375
  return;
2695
3376
  }
2696
3377
  case "interrupt":
@@ -2699,8 +3380,16 @@ var MockAdapter = class extends AdapterBase {
2699
3380
  case "throw": throw typeof step.error === "string" ? new Error(step.error) : step.error;
2700
3381
  }
2701
3382
  }
2702
- const response = this.finalizeTurn(request, factory, mockRequest, output, { type: "complete" }, stepCount);
2703
- yield factory.responseCompleted(response);
3383
+ const finalResponse = this.finalizeTurn(request, factory, mockRequest, output, { type: "complete" }, stepCount);
3384
+ yield factory.responseCompleted({
3385
+ replay: finalResponse.replay,
3386
+ stopReason: finalResponse.stopReason,
3387
+ trace: finalResponse.backend,
3388
+ usage: finalResponse.usage,
3389
+ billing: finalResponse.billing,
3390
+ auxiliary: finalResponse.auxiliary,
3391
+ warnings: finalResponse.warnings
3392
+ });
2704
3393
  } finally {
2705
3394
  this.activeStream = false;
2706
3395
  }
@@ -2828,10 +3517,11 @@ async function* emitMessage(factory, item, stream) {
2828
3517
  let chunkIndex = 0;
2829
3518
  for (const block of item.content) if (block.type === "text") for (const chunk of chunkText(block.text, stream)) {
2830
3519
  await delayForChunk(stream, chunkIndex, chunk.length);
2831
- yield factory.messageDelta(item.id, chunk);
3520
+ yield factory.messageDelta(item.id, textBlock(chunk));
2832
3521
  chunkIndex += 1;
2833
3522
  }
2834
- yield factory.messageCompleted(item);
3523
+ else yield factory.messageDelta(item.id, block);
3524
+ yield factory.messageCompleted(item.id);
2835
3525
  }
2836
3526
  async function* emitReasoning(factory, item, stream) {
2837
3527
  if (!item.id) throw new AIRequestError("Mock reasoning output requires an id after normalization", "MOCK_REASONING_ID_MISSING");
@@ -2848,7 +3538,7 @@ async function* emitReasoning(factory, item, stream) {
2848
3538
  chunkIndex += 1;
2849
3539
  }
2850
3540
  }
2851
- yield factory.reasoningCompleted(item);
3541
+ yield factory.reasoningCompleted(item.id);
2852
3542
  }
2853
3543
  async function* emitToolCall(factory, item, streamArguments, stream) {
2854
3544
  yield factory.toolCallStarted(item.id, item.name);
@@ -2860,7 +3550,7 @@ async function* emitToolCall(factory, item, streamArguments, stream) {
2860
3550
  chunkIndex += 1;
2861
3551
  }
2862
3552
  }
2863
- yield factory.toolCallCompleted(item);
3553
+ yield factory.toolCallCompleted(item.id);
2864
3554
  }
2865
3555
  function resolveStepStreamOptions(defaults, override, label) {
2866
3556
  if (override === false) return;
@@ -2971,108 +3661,6 @@ function cloneItem(item) {
2971
3661
  return structuredClone(item);
2972
3662
  }
2973
3663
  //#endregion
2974
- //#region src/helpers/synthetic-stream.ts
2975
- /**
2976
- * 模拟流式 (Synthetic Streaming)
2977
- *
2978
- * 将一组已解析的 canonical OutputItem 包装为规范事件流。
2979
- * 适用于非原生流式后端:adapter 拿到完整响应后,调用此函数
2980
- * 即可产出一致的事件序列,无需自己逐事件组装。
2981
- *
2982
- * 约束:
2983
- * - 每个 item 只发一块完整 delta(不模拟逐 token)
2984
- * - 保持 item 边界
2985
- * - 保持后端原始顺序
2986
- * - 不发明 reasoning
2987
- * - 不改写工具参数
2988
- */
2989
- /**
2990
- * 将已解析的 output items 包装为完整规范事件流。
2991
- *
2992
- * 用法示例(在 adapter 的 runStream 中):
2993
- * ```ts
2994
- * const result = parseNonStreamingResponse(data);
2995
- * yield* syntheticStream({
2996
- * model: request.model,
2997
- * responseId: request.requestId,
2998
- * backend: { kind: "chat-completions" },
2999
- * output: result.output,
3000
- * stopReason: result.stopReason,
3001
- * usage: result.usage,
3002
- * });
3003
- * ```
3004
- */
3005
- async function* syntheticStream(options) {
3006
- const { model, responseId, backend, output, replay, stopReason, usage, billing, providerMetadata, rawResponseId, warnings: extraWarnings } = options;
3007
- const factory = createEventFactory({
3008
- responseId,
3009
- backend: {
3010
- kind: backend.kind,
3011
- isSynthetic: true
3012
- }
3013
- });
3014
- yield factory.responseStarted(model);
3015
- for (const item of output) yield* emitItemEvents(item, factory);
3016
- if (usage || billing) yield factory.responseAuxiliary({
3017
- usage,
3018
- billing
3019
- });
3020
- const finalReplay = replay ?? replayFromOutput(output);
3021
- const allWarnings = [];
3022
- allWarnings.push("Response is synthetically streamed; delta granularity may differ from native streaming");
3023
- if (extraWarnings) allWarnings.push(...extraWarnings);
3024
- const response = {
3025
- id: responseId,
3026
- output,
3027
- replay: finalReplay,
3028
- text: extractText(output),
3029
- toolCalls: output.filter((item) => item.type === "tool_call"),
3030
- stopReason,
3031
- usage,
3032
- billing,
3033
- auxiliary: providerMetadata ? { providerMetadata } : void 0,
3034
- warnings: allWarnings.length > 0 ? allWarnings : void 0,
3035
- backend: {
3036
- requestId: responseId,
3037
- rawResponseId,
3038
- adapter: backend.kind,
3039
- isSyntheticStream: true
3040
- }
3041
- };
3042
- yield factory.responseCompleted(response);
3043
- }
3044
- function* emitItemEvents(item, factory) {
3045
- switch (item.type) {
3046
- case "message":
3047
- yield* emitMessageEvents(item, factory);
3048
- break;
3049
- case "reasoning":
3050
- yield* emitReasoningEvents(item, factory);
3051
- break;
3052
- case "tool_call":
3053
- yield* emitToolCallEvents(item, factory);
3054
- break;
3055
- case "opaque": break;
3056
- }
3057
- }
3058
- function* emitMessageEvents(item, factory) {
3059
- const id = item.id ?? `syn-msg-${crypto.randomUUID()}`;
3060
- yield factory.messageStarted(id);
3061
- for (const block of item.content) if (block.type === "text") yield factory.messageDelta(id, block.text);
3062
- yield factory.messageCompleted(item);
3063
- }
3064
- function* emitReasoningEvents(item, factory) {
3065
- const id = item.id ?? `syn-reason-${crypto.randomUUID()}`;
3066
- yield factory.reasoningStarted(id, item.visibility);
3067
- for (const block of item.content) if (block.type === "text") yield factory.reasoningDelta(id, block);
3068
- yield factory.reasoningCompleted(item);
3069
- }
3070
- function* emitToolCallEvents(item, factory) {
3071
- yield factory.toolCallStarted(item.id, item.name);
3072
- if (item.argumentsText) yield factory.toolCallDelta(item.id, { argumentsText: item.argumentsText });
3073
- yield factory.toolCallCompleted(item);
3074
- }
3075
- //#endregion
3076
- export { AIError, AIMappingError, AIProviderError, AIRequestError, AIStreamError, AdapterAuxiliaryState, AdapterBase, AuxiliaryCollector, ChatCompletionsAdapter, MessagesAdapter, MockAdapter, OllamaAdapter, ResponsesAdapter, WarningCode, aggregateEvents, assertMockRequest, assertValidRequest, blockToText, collectStream, contentBlocksToText, createAIClient, createEventFactory, emitMalformedStreamWarning, extractText, imageBlock, instructionsToText, jsonBlock, mapReasoningVisibility, mapStopReason, messageItem, metadataSourceList, normalizeRequest, opaqueBlock, opaqueItem, parseSSEEvents, reasoningItem, replayFromOutput, syntheticStream, textBlock, toolCallItem, toolResultItem, usageFromAnthropicMessages, usageFromChatCompletions, usageFromOllama, usageFromOpenAIResponses, validateRequest, withMockStreaming };
3664
+ export { AIError, AIMappingError, AIProviderError, AIRequestError, AIStreamError, AdapterAuxiliaryState, AdapterBase, AuxiliaryCollector, ChatCompletionsAdapter, IncrementalStreamParser, MAX_OPAQUE_JSON_DEPTH, MAX_OPAQUE_PAYLOAD_BYTES, MessagesAdapter, MockAdapter, NormalizedRequestMapper, OllamaAdapter, PROVIDER_ERROR_MESSAGE_MAX_LEN, PROVIDER_ERROR_RAW_BODY_THRESHOLD, ResponsesAdapter, WarningCode, aggregateEvents, assertMockRequest, assertOpaqueReplayEnvelope, assertValidRequest, blockToText, collectStream, contentBlocksToText, createAIClient, createEventFactory, emitMalformedStreamWarning, extractProviderErrorMessage, extractText, imageBlock, instructionsToText, jsonBlock, mapReasoningVisibility, mapStopReason, measureJsonDepth, messageItem, metadataSourceList, normalizeRequest, opaqueBlock, opaqueItem, parseSSEEvents, providerHttpError, reasoningItem, replayFromOutput, splitLines, splitSSEFrames, syntheticStream, textBlock, toolCallItem, toolResultItem, usageFromAnthropicMessages, usageFromChatCompletions, usageFromOllama, usageFromOpenAIResponses, validateOpaqueReplayEnvelope, validateRequest, withMockStreaming };
3077
3665
 
3078
3666
  //# sourceMappingURL=index.mjs.map